Objective-C Block Part2 - 实现原理

Block 的本质

Objective-C Block Part1 中总结了 Block 的一些使用规则,但是为什么要遵循这些规则,还有这些规则是怎么来的? 这就需要探寻 Block 的本质,明白它的实现原理。

注: 我们通过 clang 把包含 Block 的 Objective-C 的代码转换成 C++ 实现代码,以此来分析 Block 的实现原理。通过 clang 重写的代码仅供我们分析和参考,在极少数地方和实际运行时有细微出入。


简单 Block 转换后的代码分析

下面通过 clang -rewrite-objc main.m 把一个简单的 Block 转换成 C++ 代码 main.cpp :

/** 转换前的 Objective-C 代码: */
int main(int argc, const char * argv[]) {
    char *name = "Steve Jobs";
    ^() {
        printf("hello %s :)", name);
    }();
    return 0;
}
/** 转换后裁剪出的重要代码: */
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;
  char *name;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_name, int flags=0) : name(_name) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    char *name = __cself->name; // bound by copy
    printf("hello %s :)", name);
}

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, const char * argv[]) {
    char *name = "Steve Jobs";
    ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, name))();
    return 0;
}

我们看到 Block 的本质其实就是一个名为 __main_block_impl_0 的结构体。这个结构体包含的主要内容:

  1. __block_impl 结构体的变量。 (block 结构体的第一个成员都是 __block_impl, 所以 __block_impl 是 block 的基础结构体。
  2. __main_block_desc_0 结构体的变量。( __main_block_desc_0 是描述 block 的结构体。
  3. 可选的 被截获的成员,例如本例的 char *name ,这一块下一节细述。
  4. __main_block_impl_0 结构体的构造函数


再来分析一下 __main_block_impl_0 结构体的第一个成员 __block_impl 结构体里的内容:

  1. void *isa; (所有的 OC 对象的都有 isa指针,这也说明 Block 是一个 OC 对象。
  2. int Flags; (用于按 bit 位表示一些 block 的附加信息。
  3. int Reserved; (保留变量。
  4. void *FuncPtr; (block 执行的函数指针,这个函数指针包含的是 OC 里面写在 Block 里面的代码。


__main_block_desc_0 的内容:

  1. size_t reserved; (保留变量
  2. size_t Block_size; (保存 Block 的大小


Block 截获变量

上面一节大概了解了 Block 是个什么东西,这一节则会进行更深入的探索,搞清楚 Block 对各种类型的变量在内部是如何处理。下面通过 clang 转换一个包含各种变量的 Block 来分析这些问题:

/** 转换前的 Objective-C 代码(ARC): */
typedef void(^blk_t)();

static int static_global_val = 1;       // 静态全局变量(C
static NSObject *static_global_obj;     // 静态全局变量(OC
int global_val = 1;                     // 全局变量(C
NSObject *global_obj;                   // 全局变量(OC

int main(int argc, const char * argv[]) {
    int automatic_val = 1;                                   // 自动变量(C
    NSObject *automatic_obj = [NSObject new];               // 自动变量(OC
    
    __block int __block_val = 1;                             // __block变量(C
    __block NSObject *__block_obj = [NSObject new];         // __block变量(OC
    
    static int static_val = 1;                               // 静态变量(C
    static NSObject *static_obj;                             // 静态变量(OC
    
    static_global_obj = [NSObject new];
    global_obj = [NSObject new];
    static_obj = [NSObject new];
    
    blk_t block = ^{
      static_global_val = 1;
      static_global_obj = [NSArray array];
      global_val = 1;
      global_obj = [NSArray array];  
      static_val = 1;
      static_obj = [NSArray array];
        
      __block_val = 1;
      __block_obj = [NSArray array];
      printf("%d,%p", automatic_val, automatic_obj);
      //automatic_val = 1;              // 报错
      //automatic_obj = [NSArray array]; // 报错
    };
    block();
}


下面是转换成 C++ 的代码, 其中裁剪出重要的内容来显示,代码后面是分析上面问题的答案:

/** 转换后的C++代码(ARC): */
typedef void(*blk_t)();

static int static_global_val = 1;
static NSObject *static_global_obj;
int global_val = 1;
NSObject *global_obj;

struct __Block_byref___block_val_0 {
  void *__isa;
  __Block_byref___block_val_0 *__forwarding;
  int __flags;
  int __size;
  int __block_val;
};
struct __Block_byref___block_obj_1 {
  void *__isa;
  __Block_byref___block_obj_1 *__forwarding;
  int __flags;
  int __size;
  void (*__Block_byref_id_object_copy)(void*, void*);
  void (*__Block_byref_id_object_dispose)(void*);
  NSObject *__block_obj;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;
  NSObject **static_obj;
  int automatic_val;
  NSObject *automatic_obj;
  __Block_byref___block_val_0 *__block_val; // by ref
  __Block_byref___block_obj_1 *__block_obj; // by ref
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, NSObject **_static_obj, int _automatic_val, NSObject *_automatic_obj, __Block_byref___block_val_0 *___block_val, __Block_byref___block_obj_1 *___block_obj, int flags=0) : static_val(_static_val), static_obj(_static_obj), automatic_val(_automatic_val), automatic_obj(_automatic_obj), __block_val(___block_val->__forwarding), __block_obj(___block_obj->__forwarding) { 
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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*);
    void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = {0,sizeof(struct __main_block_impl_0),__main_block_copy_0,__main_block_dispose_0};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref___block_val_0 *__block_val = __cself->__block_val; // bound by ref
  __Block_byref___block_obj_1 *__block_obj = __cself->__block_obj; // bound by ref
  int *static_val = __cself->static_val;       // bound by copy
  NSObject **static_obj = __cself->static_obj; // bound by copy

    static_global_val = 1;
    static_global_obj = objc_msgSend(objc_getClass("NSArray"), sel_registerName("array"));
    global_val = 1;
    global_obj = objc_msgSend(objc_getClass("NSArray"), sel_registerName("array"));

    (*static_val) = 1;
    (*static_obj) = objc_msgSend(objc_getClass("NSArray"), sel_registerName("array"));
    (__block_val->__forwarding->__block_val) = 1;
    (__block_obj->__forwarding->__block_obj) = objc_msgSend(objc_getClass("NSArray"), sel_registerName("new"));
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src{...}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {...}

int main(int argc, const char * argv[]) {
    int automatic_val = 1;
    NSObject *automatic_obj = objc_msgSend((id)objc_getClass("NSObject"), sel_registerName("new"));
  
    __Block_byref___block_val_0 __block_val = {(void*)0,&__block_val,0,sizeof(__Block_byref___block_val_0),1};
    __Block_byref___block_obj_1 __block_obj = {(void*)0, &__block_obj, 33554432, sizeof(__Block_byref___block_obj_1), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, objc_msgSend(objc_getClass("NSObject"), sel_registerName("new"))};

    static int static_val = 1;
    static NSObject *static_obj;

    static_global_obj = objc_msgSend(objc_getClass("NSObject"), sel_registerName("new"));
    global_obj = objc_msgSend(objc_getClass("NSObject"), sel_registerName("new"));
    static_obj = objc_msgSend(objc_getClass("NSObject"), sel_registerName("new"));
    
    blk_t block = ((void (*)())&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &static_val, &static_obj, automatic_val, automatic_obj, &__block_val, &__block_obj, 570425344));
  
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

1. Block 怎么处理自动变量?

从上面的 C++ 代码中可以看出,在 Block 对应的 __main_block_impl_0 struct 中有和自动变量 automatic_val automatic_obj 相同的类型的结构体成员。 __main_block_impl_0 struct 的构造方法 __main_block_impl_0 的参数中也会接受 automatic_val automatic_obj 同类型的参数,然后把其赋值给对应的 strcut 成员。 可以看下在 main 函数中对 __main_block_impl_0 构造函数的调用。 这里有几个值得注意的点:

  1. 对于 C 基础类型 automatic_val 直接传递的是值 1 , 所以 mian 函数中定义的 int automatic_val 和 Block 对应结构体中的 automatic_val 结构体成员不是同一个变量。通过下面的小例子可以证明:

    int main(int argc, const char * argv[]) {
      int automatic_val = 1;
      blk_t block = ^ { printf("inside  in room val:%d \n",automatic_val);};
      automatic_val = 2;
      printf("outside in room val:%d \n",automatic_val);
      
      block();
    }
    

    下面是上面代码的输出,可以看到在 Block 外改变 val 的值根本影响不到 Block 内的值,因为他们不在同一块内存上

    outside in room val:2 
    inside  in room val:1 
    
  2. 对于 OC 对象类型 automatic_obj 是直接传递指针,和 automatic_val 同理,如果在 Block 外新建一个 OC 对象类型 的指针,再赋值给 automatic_obj 变量也对 Block 内的 automatic_obj 结构体成员是没有影响的,因为这两个变量里面此时装的已经是不同的指针了。 但是当它们装的是同一个指针时,是可以通过调用对象的方法来相互影响的, 举个栗子,在 Block 外更改可变数组里的内容是会影响到 Block 内部的可变数组的,因为此时这两个变量是装的同一个指针:

    int main(int argc, const char * argv[]) {
        NSMutableArray *automatic_obj = [NSMutableArray arrayWithObjects:@"1", @"2", nil];
        blk_t block = ^ {
            NSLog(@"inside  in room obj: %@", automatic_obj);
        };
        [automatic_obj addObject:@"3"];
        NSLog(@"outside in room obj: %@", automatic_obj);
      
        block();
    }
    
  3. 在 Block 对应的 __main_block_impl_0 struct 中,修饰 automatic_obj 的是 strong 所有权修饰符,所以 Block 会对这个对象进行持有, 为什么系统这么去设计让 Block 持有自动变量? 是因为 Block 能够超出其所在的函数作用域存在,而 OC 对象类型 的自动变量在超出函数作用域时就会被释放被释放,Block 此时执行时这个对象已经被销毁了。为了避免这种情况 Block 持有了 OC 对象类型 的自动变量。


2. Block 怎么处理 静态全局变量 / 全局变量 / 静态变量 ?

之所以把这三种变量归纳到一起,是因为它们的生命周期在一个程序中会一直存在。 Block 中使用 静态全局变量全局变量 时,因为它们的作用域是全局的,并且是在程序中一直存在的,所以转换后这部分没有任何变化可以直接使用,并且可以在 Block 中重新赋值。 Block 中使用 静态变量 转换后会在 Block 对应的 __main_block_impl_0 struct 中追加这个 静态变量 的类型的指针,然后通过 strcut 的构造函数传递进去,通过这种方式扩大了这个静态变量可访问的作用域, 使其可以在 __main_block_func_0 函数中访问。同时也因为传递的是 静态变量 类型的指针所以具备了重新赋值的能力。


3. Block 怎么处理 __block 修饰的自动变量 ?

上面的例子中 __block 修饰的自动变量 __block_val __block_obj 在转换后分别变成了 __Block_byref___block_val_0__Block_byref___block_val_1 结构体,并且它们第一个成员都是 isa 指针,这说明它们都是 OC 对象。第二个成员是 __forwarding 指针 目前是指向这个结构体的本身。 这两个结构体分别还包含着对应的 __block 自动变量类型的 结构体成员。


Block 的储存域

因为 Block 对应的结构体第一个成员是 isa 指针 ,所以 Block 也是个 OC 对象 ,那么 isa 指针 指向的就是 Block 的类了。以前我们接触到的都是 _NSConcreteStackBlock ,其实还有另外两种: _NSConcreteGlobalBlock _NSConcreteMallocBlock 。 这一节就是来探讨这三种 Block 的不同和作用。


三种 Block 在内存的存储区域

  • _NSConcreteStackBlock 类的 Block 对象是设置栈区上的,超出其所在的函数作用域就会被释放。
  • _NSConcreteGlobalBlock 类的 Block 对象是设置在 .data 区上,在程序运行时永久存在的。
  • _NSConcreteMallocBlock 类的 Block 对象是设置在堆区上。

怎么区分这三种 Block

_NSConcreteGlobalBlock

  • 在所有方法外定义的 Block 为 Global Block
  • 当 Block 中没有截获自动变量是为 Global Block

_NSConcreteMallocBlock

Malloc BlockStack Block 被执行 copy 操作后得到的。它能让 Block 超出函数/方法的作用域而存在。

_NSConcreteStackBlock

除了上面的情况,剩下的就都是 Stack Block 了。当其所在的函数作用域结束时,这个 Block 就会被回收。


那些情况系统会自动帮你调用 copy 方法

通过 copy 操作可以让 Stack Block 拷贝成 Malloc Block,但是在一些情况下,系统会自动的帮我们执行 copy 操作:

  • ARC Block 作为函数返回值返回时会自动对 Block 执行 copy 操作。
  • ARC 下将 Block 赋值给附有 __strong 修饰符的变量时。 所以在 ARC 下不用调用 copy 操作,直接把它赋值给 __strong 变量就可以达到效果,还有对于 Block 类型的 @property 的 attribute 不用写 copy。直接使用默认的 __strong ,有太多人在 ARC 下还是用 copy 去修饰。 完全没有必要
  • 在方法名中含有 usingBlock 的 Cocoa 框架方法或 Grand Central Dispatch 的 API 中传递 Block 时。


Block copy 时会发生什么? 会造成什么影响?

大部分的 Block 对象是设置在栈内存上的,为了使 Block 能够超出其函数作用域的范围。可以使用 copy 操作将其从栈内存拷贝到堆内存中。对于 Block 中的 __block 变量一开始它也是配置在栈内存中的,在超出函数作用域时它也会被释放。所以在拷贝 Block 对象到堆内存中时,也会同时拷贝这个 Block 使用的 __block 变量到堆内存中。 当多个 Block 对象同时使用一个 __block 变量时,如果其中有个 Block 已经把 __block 变量拷贝到堆内存上了。后面的 Block 再次对这个 __block 变量执行 copy 操作时只会增加这个 __block 变量的持有。 等 Block 销毁时就会减少 __block 变量的持有。当没有 Block 持有 __block 变量时它就会被回收。这和我们一直使用的引用计数的内存管理方式相同。


为什么要设计 __forwarding 这个东西?

上一节的例子中 __block_val __block_obj 这两个 __block 变量在被转换后,分别变成了 __Block_byref___block_val_0 __Block_byref___block_val_1 结构体,并且结构体的第二个成员都是 __forwarding 指针。 这个 __forwarding 指针目前是指向自己的。为什么要去设计 __forwarding 指针这个东西?这需要刚才讨论的 Block 对象的拷贝结合在一起看。

__forwarding 指针存在的意义是不管 __block 变量是配置在栈上还是堆上,都能够正确的访问变量。当 Block 对象被拷贝到堆内存中是, __block 变量也被拷贝到堆内存中。那么此时可以同时访问栈上的 __block 变量 和 堆上的 __block 变量。他们都是通过下面的方式访问的, 都是通过 __forarding 指针:

__block_val->__forwarding->__block_val
__block_obj->__forwarding->__block_obj

栈上的 __block 在被拷贝到堆内存时,会改变栈内存的 __forwarding 指针,让其指向堆内存的 __block 变量。 所以通过这个设计让 访问的 __block 变量无论在 Block 中还是 Block外,__block变量是在堆内存还是栈内存上,访问的都是同一个 __block 变量。


为什么 Block 中的静态变量可以修改, 而自动变量不能修改?

上面讲到 Block 对应的结构体因为保存的是 静态变量 的类型的指针,所以 静态变量 可以在 Block 中被重新赋值。那自动变量为什么不也设计成这样,使其拥有在 Block 中被重新赋值的能力呢? 这是因为自动变量的超出其所在的函数作用域时就会被销毁掉。但是 Block 又可以超出其自身作用域而存在。如果像对待 静态变量 那样去对待 自动变量 ,很可能出现的情况就是当 Block 去操作/访问 自动变量 时。自动变量已经被销毁。


小测验

这个关于 Block 的小测试 这是在唐巧的 blog 《谈Objective-C block的实现》 中发现的, 觉得很能考察对 Block 的理解程度,做到全对感觉对 Block 了解就很清楚了。


参考资源

Objective-C 高级编程

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

推荐阅读更多精彩内容