OC_Block内部实现原理分析

序言:翻阅资料,学习,探究,总结,借鉴,谢谢探路者,我只是个搬运工。
参考、转发资料:
http://www.cnblogs.com/chenxianming/p/5554395.html
http://www.jianshu.com/p/ca6ac0ae93ad#
书籍:Objective-C高级编程

1. Block的本质

Block的本质其实是object对象

2. 内部分析

在百度到的知识基本上都是使用clang(LLVM编译器,和GCC类似)来解析编译的。总结一下自己的理解。
以下代码为例:

int main(int argc, char * argv[]) {
    void (^test)() = ^(){
    };
    test();
}

接下来我要用到一个命令clang src.m -rewrite-objc -o dest.cpp.这个意思是用clang编译器对源文件src.m中的objective-c代码转换成C代码放在dest.cpp文件。其实xcode编译时也会帮我们转换。我们这样就可以dest.cpp在看到我们定义和调用的block转换成C是怎么样的。执行命令后查看这个dest.cpp会发现有一大堆代码。下面我把对我们有用并能够说清楚原理的关键贴上来并加以注释:

//__block_imp:  这个是编译器给我们生成的结构体,每一个block都会用到这个结构体
struct __block_impl {
  void *isa;         //对于本文可以忽略
  int Flags;          //对于本文可以忽略
  int Reserved;       //对于本文可以忽略       
  void *FuncPtr;       //函数指针,这个会指向编译器给我们生成的下面的静态函数__main_block_func_0
};
/*__main_block_impl_0: 
是编译器给我们在main函数中定义的block
void (^test)() = ^(){
};
生成的对应的结构体
*/
struct __main_block_impl_0 {
struct __block_impl impl;          //__block_impl 变量impl
struct __main_block_desc_0* Desc;    //__main_block_desc_0 指针,指向编译器给我们生成的结构体变量__main_block_desc_0_DATA __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {  //结构体的构造函数
    impl.isa = &_NSConcreteStackBlock;  //说明block是栈blockimpl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;}
};
//__main_block_func_0: 编译器根据block代码生成的全局态函数,会被赋值给impl.FuncPtr
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}
//__main_block_desc_0: 编译器根据block代码生成的block描述,主要是记录下__main_block_impl_0结构体大小
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; //这里就生成了__main_block_desc_0的变量__main_block_desc_0_DATA
//这里就是main函数了
int main(int argc, char * argv[]) {
// 以下代码转换之后,void (* test)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
    void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); //下面单独讲
//  以下代码转换之后,test->FuncPtr(test);
    ((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);                          //下面单独讲
}
  • 总结:
    好了讲到这里,就可以进行一个中途简单性的总结:忽略中间的复杂分支,留下主线,当我们声明一个Block变量a并为它赋值时,其实就是创建一个函数指针ptrA,再根据Block a赋值的代码生成一个静态函数,而指针ptrA就指向这个静态函数。Block a调用时就是使用函数指ptrA调用生成的静态函数。

3. 获取自动变量的瞬间值

  • block的定义:带有自动变量的匿名函数。然而怎么获取自动变量的瞬间值呢?
    我们对最初的代码进行修改添加变量
int main(int argc, char * argv[]) {
    int value = 1;
    void (^test)() = ^(){
        int valueTest = value;
    };
    test();
}

如上的内部实现代码:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
// 这里是不同于上面的,多出来的,但是其中使用的全局变量(包括全局静态变量)不会生成对应的参数对象。只有自动变量才会。
  int value;               
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int value = __cself->value; // bound by copy
        int valueTest = value;              // 这里是不同于上面的,多出来的
    }
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
    int value = 1;
    void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, value));
    ((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);
}
  • 理解一下:在Block外定义的自动变量(Block中不使用的自动变量不做处理),将会随着Block的创建,以形参的形式传递进Block,在创建对应Block方法的时候,结构体中会生成对应形参的变量用于储存记录形参的值,通过构造方法赋值。
    你会发现,在block生成的匿名函数中,block是用定义的变量去接收自动变量的值并存储,在使用block生成的静态函数static void __main_block_func_0(struct __main_block_impl_0 *__cself)时,传入了是Block对象,通过拿取的是Block中的值。

3. __block 的实现原理(下一模块就再对它的作用情况进行细致的讲解)

以下代码为例:

int main(int argc, char * argv[]) {
    __block int value = 1;
    void (^test)() = ^(){
        value = 2;
    };
    test();
    int value1 = value;
}

一如既往,我们看一下添加了_block内部发生了什么变化。

//根据带__block修饰符的变量value,编译器给我们生成了个结构体
struct __Block_byref_value_0 {
  void *__isa;
__Block_byref_value_0 *__forwarding;   //这个会指向被创建出来的__Block_byref_value_0实例
int __flags;
 int __size;
 int value;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_value_0 *value;  //保存__Block_byref_value_0变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_value_0 *value = __cself->value; // bound by ref
        // 注意这里
        (value->__forwarding->value) = 2;
    }

//这两个函数分别会在test block 被拷贝到堆和释构时调用的,作用是对__Block_byref_value_0实例的内存进行管理,至于怎么管理,这里就不讨论了,这里就会调用上面导出来的接口。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); //回调函数指针,会被赋值为__main_block_copy_0

 void (*dispose)(struct __main_block_impl_0*);            //回调函数指针,会被赋值为__main_block_dispose_0
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; /*{ 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0},这句就是创建一个例的意思,这是结构体的一种构造方式。*/


int main(int argc, char * argv[]) {
    /*我们定义的__block int value转换后并不是一个简单的栈变量,而会是新建的__Block_byref_value_0堆变量*/
     __attribute__((__blocks__(byref))) __Block_byref_value_0 value = {(void*)0,(__Block_byref_value_0 *)&value, 0, sizeof(__Block_byref_value_0), 1};

void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_value_0 *)&value, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);
  //最后面使这句int value1 = value;使用value时,在我们表面看到是好像是使用main函数里的一个局部栈变量,其实不是,使用的是堆里面的容int value1 = (value.__forwarding->value);
}
  • 总结:我们不能看出,使用了_block就多了修饰value的结构体_Block_byref_value_0,而最根本的原因是用了_block修饰的变量被从栈区copy到堆区中了,这样,就不会被立即释放了(而static修饰的静态变量存储在静态区、全局变量不需要用_block修饰是一个道理)。
  • 注意:其中有个细节需要理解,为什么在__Block_byref_value_0的结构体中还有一个一样__Block_byref_value_0结构体变量。其实是有原因的,__Block_byref_value_0中的_forwarding有什么作用?_forwarding,指向自己的指针,当从栈copy到堆时,指向堆上的block,这也是为什么 (value->_forwarding->value) = 2反复指向的原因。
    _forwarding 的原理如下图
1420513789717076.jpg

4. Block的存储区域

  1. Block和_block变量的实质
  • Block:栈上Block的结构体实例。
  • _block:栈上_block变量的结构体实例。
  1. Block的类:
  • _NSConcreteStackBlock 栈区
    之前我们使用的都是_NSConcreteStackBlock(栈区)类型的Block。
  • _NSConcreteGlobalBlock 数据区(静态区)
    _NSConcreteGlobalBlock稍微解释一下:在记述全局变量的地方使用Block语法时,生成的Block。
    满足条件:
    • 记述全局变量的地方有Block语法时。
    • Block语法的表达式中不使用应截获的自动变量时。
      例如:
void (^blk)(void) = ^(printf("HelloWorld")) ;
int main(){
}
  • _NSConcreteMallocBlock 堆区
    Blocks提供了将Block和_block变量从栈上赋值到堆上的方法来解决这个问题。将配置在栈上的Block赋值到栈上,这样即使Block语法计数的变量作用域结束,堆上的Block还可以继续存在,
    例子:
/**
     * _Block_copy函数
     * 将栈上的Block复制到堆上
     * 复制后,将堆上的地址作为指针赋值给变量tmp
     */
    tmp = _Block_copy(tmp) ;

注意:Block从栈区copy到堆区是相当消耗CPU的。

不同类型Block执行_Block_copy的图标说明。

Block的类 副本源的配置存储域 赋值效果
__NSConcreteStackBlock 从栈区复制到堆区
__NSConcreteGlobalBlock 程序的数据区域(静态区) 什么都不做
__NSConcreteMallocBlock 堆区 引用计数增加

5. 具体例子的分析,并用以上讲解的知识解释

  1. 为什么在Block方法外修改自动变量不会影响Block内部的使用。
    以下代码为例:
int value = 0 ;
    void (^test)() = ^(){
        // 结果:value = 0
        NSLog(@"value = %d",value) ;
    };
    value = 2 ;
    test();

Block方法获取的是自动变量的瞬间值。

  1. 为什么Block不能直接修改自动变量。
    以下代码为例:
int value = 0 ;
    void (^test)() = ^(){
        // 结果:value = 0
        value = 4 ;
    };
    test();

这段代码在编写编译的时候就会报错,因为自动变量值截获只能保存执行Block语法瞬间的值,保存后就不能改写该值。其实这段代码在编写的规范上没有任何错误,但是因为在实现上不能改写被截获自动变量的值,所以当编译器在编译过程中检出给被截获自动变量赋值的操作时,便产生编译错误。

  1. 虽然在Block中不能对自动变量直接修改,但是能对自动变量做其他操作。
    例如:
NSMutableArray *array_m = [NSMutableArray array] ;
    [array_m addObject:@1] ;
    
    void (^test)() = ^(){
        // 结果array_m: (1,2)
        [array_m addObject:@2] ;
        NSLog(@"array_m: %@",array_m) ;
    };
    test();

因为array_m是个指针对象,和Block中value都同时指向同一个类的实例化区域,所以可以在不改变value指针地址的基础上进行操作。但是你要是改变array_m的指针地址例如
array_m = nil 就会出现错误。

  1. 使用_blcok解决不能修改Block外自动变量的值。
    以下代码为例:
__block int value = 0 ;
    void (^test)() = ^(){
        value = 4 ;
        // 结果:value: 4
        NSLog(@"value: %d",value) ;
    };
    test();

我们可以参照上面简述的代码内容:

//根据带__block修饰符的变量value,编译器给我们生成了个结构体
struct __Block_byref_value_0 {
  void *__isa;
__Block_byref_value_0 *__forwarding;   //这个会指向被创建出来的__Block_byref_value_0实例
int __flags;
 int __size;
 int value;
};
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_value_0 *value;  //保存__Block_byref_value_0变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_value_0 *value = __cself->value; // bound by ref
        // 注意这里
        (value->__forwarding->value) = 4;
    }

你会发现对了一个value在Block中变成了一个结构体,这就是奥秘,从_main_block_func_0方法来看,你在Block的操作结构体指针永远都不会变和自动变量一致,而是对_Block_byref_value_0的value进行操作,所以可以赋值。

  • 疑惑点:
    • 为什么自动变量值截获只能保存执行Block语法瞬间的值,保存后就不能改写该值?
      首先Block中的value(用来记录的对象我用value统称)对象的地址是不允许改变的。

    • 为什么静态变量的方式也适用于自动变量的访问,但是我们没有这样做呢?
      实际上,在由Block语法生成的值Block上,可以存在超过其变量作用域的被截获对象的自动变量。变量作用于结束的同时,原来的自动变量被废弃,因此Block超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的自动变量。而这个可以存在指的就是NSConcreteMallocBlock。NSConcreteMallocBlock的访问是通过_forwarding实现的,只要栈上的结构体实例成员变量_forwarding指向堆上的结构体实例,那么不管是在栈上的_block变量还是从堆上的_block变量都能访问。

    • 为什么局部静态变量不需要任何操作就能够在Block块中使用操作,而自动变量需要?
      从广义的角度讲的话,就是和变量在内存当中的存储位置和其作用域。从内部实现原理来说,请看下面代码,转换之后挑取需要的代码:

      • 自动变量方法
// 方法
int main(int argc, char * argv[]) {
    int value = 1 ;
    void (^test)() = ^(){
      printf(value) ;
    };
      test();
}
// 转换之后__main_block_impl_0结构体中
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 注意这里 value变量不带*
  int value;               
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
- 静态局部变量
// 方法
int main(int argc, char * argv[]) {
    static int value = 1 ;
    void (^test)() = ^(){
      printf(value) ;
    };
      test();
}
// 转换之后__main_block_impl_0结构体中
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 注意这里 value变量带*
  int *value;               
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

二者的区别:value对象,一个带*是指针,一个不带指针是个基本变量类型。

  • 总结:
    • 在都是局部变量的情况下(这里不考虑全局变量、全局静态变量,为了在Block块中使用这些变量时,Block内部是不做如何处理都没问题的),我们不能修改赋值定义在Block结构体_main_block_impl_0中的变量value(我有时候会用value统称变量名),弄清楚基础变量和指针变量对你理解这句话,甚至对各种情况的执行的理解都有帮助。
    • 要明白不做任何处理和修饰的自动变量和Block中的value(Block中用来存储自动变量值的变量,我统称value)都是存储在栈区的基本变量,这两个变量的存储区域的指针地址不同,所以即使我们在Block中修改了value的值,但是也不能修改自动变量。但是不管是static修饰自动变量还是_block修饰自动变量都是_block中存储自动变量的value变成了一个指针变量。所以你修改的指针地址,而是值。
    • 要想在Block中操作局部变量,需要保存局部变量和Block中存储变量的指针地址一致,不然不能进行复制操作,但是可以不改变指针地址的操作例如数组添加元素。

6. Block的循环引用

  1. 说明:
    如果在Block中使用附有_strong修饰符的对象类型自动变量,那么当Block从栈复制到堆区时,该对象为Block所持有,这样容易引起循环引用。
typedef void (^Test)(void);
@interface ViewController ()
{
    Test test ;
}
@end
@implementation ViewController
  - (void)viewDidLoad {
    [super viewDidLoad];
    test = ^{
        NSLog(@"self: %@",self) ;
    } ;
}

执行的Block语法使用附有_strong修饰符的id类型变量self,因此通过Block语法生成在栈上的Block此时由栈区复制到堆区,并持有所使用的self。
如图:

使用Block成员变量循环引用

接下来解释一下上面一句话:

// 转换之前的代码
NSMutableArray *array_m = [[NSMutableArray alloc]init] ;
    void (^test)() = ^(){
        [array_m addObject:@(1)] ;
    };
    test();
// 转换之后的代码,省略部分代码
__main_block_impl_0{
  id _strong array_m ;
}

解释:Block中使用的赋值给附有_strong修饰符的自动变量的对象和复制到堆上的_block变量由于被堆上的Block持有,因此可超出其变量作用域而存在(有点复杂,不做解释)。
解决:用_weak修饰self,来解决循环引用。

  1. 解决方式
  • 使用_weak修饰self,来解决循环引用。
typedef void (^Test)(void);
@interface ViewController ()
{
  Test test ;
}
@end
@implementation ViewController
  - (void)viewDidLoad {
  [super viewDidLoad];
  __weak ViewController *weakSelf = self ;
  test = ^{
      NSLog(@"self: %@", weakSelf) ;
  } ;
}
  • 使用_blcok修饰self,来解决循环引用。
  typedef void (^Test)(void);
  @interface ViewController ()
{
    Test test ;
}
@end
@implementation ViewController
    - (void)viewDidLoad {
    [super viewDidLoad];
    __block ViewController *blockSelf = self ;
    test = ^{
        NSLog(@"self: %@", blockSelf) ;
        // 注意
        blockSelf = nil ;
    } ;
    // 注意
    test() ;
}

注意:需要注意两点。
1、一定要在Block中把_block变量置nil 。
2、一定要执行这个Block方法 。

原理图:

__block避免循环引用.png
  1. __block的特点
-  通过_block变量可控制对象的持有时间。优点
- 为了避免循环引用必须执行Block。缺点
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • Blocks Blocks Blocks 是带有局部变量的匿名函数 截取自动变量值 int main(){ ...
    南京小伙阅读 882评论 1 3
  • 摘要block是2010年WWDC苹果为Objective-C提供的一个新特性,它为我们开发提供了便利,比如GCD...
    西门吹雪123阅读 881评论 0 4
  • 前言 Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这...
    小人不才阅读 3,727评论 0 23
  • Block基础回顾 1.什么是Block? 带有局部变量的匿名函数(名字不重要,知道怎么用就行),差不多就与C语言...
    YeeChain阅读 6,683评论 5 61
  • iOS代码块Block 概述 代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性,B...
    smile刺客阅读 2,245评论 2 26