重识Objective-C:Block底层实现

本文主要整理了Objective-C的Block实现方式。
iOS 其他相关博文链接iOS-day-by-day

目录

  • 1.Objective-C与其他语言中对比
  • 2.Block模式
  • 3.Block底层实现

一. Objective-C和其他语言对比

在其他许多编程语言中,也存在Block被称为闭包(Closure)、lambda计算等。

程序语言 Block的名称
C+ Blocks Block
Smalltalk Block
Ruby Block
LISP Lambda
Python Lambda
C++ Lambda
Javascript Anonymous function

二.Block常用术语

1.Block语法结构

Block是带有自动变量值得匿名函数。

Block语法.jpg

例如:

// 1
^ int (int count) { return count + 1;}
// 2
^ (int count) {printf("Block");}
// 3
^{printf("Block");}

Block字面值的写法:

^ (double firstValue, double secondValue) {
     return firstValue *secondValue;
}

上面写法省略了返回值的类型,可以显示的指出返回值类型。通过使用typedef可以声明“blk_t”类型变量

 typedef double (^blk_t)(double, double);
    blk_t blk = ^(double firstValue, double secondValue){
        return firstValue * secondValue;
    };
    NSLog(@"%f", blk(3, 4));

Block也是一个Objective-C对象,可以用于赋值,当参数传递,也可以放在集合中。

2.截获自动变量值

int val = 10;
void (^blk_t) (void) = ^ {
     NSLog(@"val = %d", val);
};
val = 2;
blk_t(); // val = 10

执行结果,不是val = 2,而是val = 10。捕获的是执行Block语法时的的瞬间值。这就是自动变量值的捕获。

3.__block说明符

自动变量值截获只能保存执行Block语法瞬间值。保存后就不能修改该值。否则,会产生编译错误。如果想要修改截获的自动变量,需要使用__block修饰。

__block int val = 10;
void (^blk_t) (void) = ^ {
      val = 11;
     NSLog(@"val = %d", val );
};
blk_t(); // val = 11

使用附有__block说明符的自动变量可在Block中赋值,该变量称为_block变量。

三.Block底层实现

1. Block的实质

Block是“带有自动变量值得匿名函数”。但Block究竟是什么呢?接下来我们一起来分析。

为了研究编译器是如何实现block的,需要使用clang命令,可以将Objective-C的源码改写成C++源代码,命令如下:

clang -rewrite-objc xxx.c

新建一个main.c的源文件:

int main(int argc, const char * argv[]) {
    void (^blk) (void) = ^ {
         NSLog(@"Hello, Block!");
    };
    blk();  
    return 0;
}

在文件所在的文件夹,使用命令:clang -rewrite-objc main.m,在目录中得到一个名为main.cpp的文件,区区七八行代码,转换后变成10万行,其中关键代码:

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;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//实现
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d3_9wynvv910yz9wv20fw56jz0h0000gn_T_main_dd5e8c_mi_0);
        }
//附加信息,如Block大小
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
int main(int argc, const char * argv[]) {
        void (*blk) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

接下来我们分析一下,这几行关键代码是什么意思?
int main()中开始。简化一下发现:

// block
void (*blk)(void) = __main_block_impl_0(_main_block_func_0, &_mian_block_desc_0_DATA);
//调用
(((*blk)->FuncPtr)blk);

第一条语句中出现的__main_block_impl_0(),__main_block_func_0__mian_block_desc_0_DATA是什么意思?别急,我们一条条来分析。
__main_block_impl_0()实际上是__main_block_impl_0结构体的构造函数。结构体声明如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};

第一个成员变量是impl。先看看__block_impl结构体的声明:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

从这里我们可能会想到某种标记,预留区域和函数指针。具体内容我们会在后面详细讲解。第二变量是Desc指针, __main_block_desc_0声明如下:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

预留区域和Block的大小。接下来是构造函数:

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

通过前面调用构造函数,可以简化为:

isa = &__NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;

其中:

isa = &__NSConcreteStackBlock;

将Block指针赋给Block的结构体成员变量isa。其实,Block就是Objective-C对象。想要理解上句代码的意思,需要理解Objective-C类和对象的实质。

id 类型

typedef struct objc_object *id;
typedef struct objc_class *Class;

id为objc_object结构体的指针类型。Class 为objc_class结构体的指针类型。objc_class结构体在objc4/runtime.h中声明:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

两者很相似,objc_object结构体和objc_class结构体归根结底实在各个对象和类的实现中使用的最基本的结构体。
下面通过简单的MyObject来看一下。

@implementation MyObject
{
    int val0;
    int val1;
}
@end

转换为C++源代码,MyObject结构体

struct MyObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int val0;
    int val1;
};

实例变量val0和val1被直接声明为结构体的成员。上述中,结构体都是基于objc_class结构体的class_t结构体。class_t声明如下:

struct _class_t {
    struct _class_t *isa;
    struct _class_t *superclass;
    void *cache;
    void *vtable;
    struct _class_ro_t *ro;
};

该结构体实例中,持有声明的成员变量、方法的名称、方法的实现、属性以及父类的指针。回到

isa = &__NSConcreteStackBlock;

__NSConcreteStackBlock相当于class_t结构体实例,关于该类的信息都存放在__NSConcreteStackBlock中。

2. 截获自动变量

int main(int argc, const char * argv[]) {
    
    int val = 10;
    void (^blk) (void) = ^ {
        printf("%d", val);
    };
    blk();
    return 0;
}

使用clang命令转为C++代码,看看和之前的有什么区别。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val; // <== val 
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy

        printf("%d", val);
    }

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[]) {

    int val = 10;
    void (*blk) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

对比之前的代码发现,Block语法表达式中使用的自动变量被作为成员变量追加到__main_block_impl_0结构体中。并且类型完全相同,另外注意,未使用的变量并不会追加。

__main_block_impl_0结构体实例初始化:

isa = &__NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
val = 10;

这时,在__main_block_impl_0结构体实例中,自动变量值被截获。
总的来说,所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例中。

3.__block说明符

在Block中修改捕获的外部变量,需要使用__block修饰符(__block存储域类说明符)。在C中存储域类:typedef、extern、static、auto、register。__block与他们的作用相同,用来指定将变量值设置到哪个存储域中。
下面看一个例子:

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        __block int val = 10;
        void (^blk) (void) = ^{
            val = 11;
        };
        blk();
    }
    return 0;
}

使用clang命令,转为C++源码如下:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref  <=======注意
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref

            (val->__forwarding->val) = 11;
        }
// <====== 相当于retain
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
// <====== 相当于release
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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*);
  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};

int main(int argc, const char * argv[]) {
        __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};

        void (*blk) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
 
    return 0;
}

上面是含有__block修饰符的代码。对比一下和之前的差别。

 __block int val = 10;

__block修饰的变量变成结构体

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding; // 
 int __flags;
 int __size;
 int val; // <=== val 作为结构体的成员变量
};

变量val被作为结构体的成员变量。_forwarding指针什么作用?后面在分析。

那变量赋值的代码呢?

^{val = 11;}

如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

            (val->__forwarding->val) = 11;
        }

变量的复制

  • 对于block外部变量的引用,block默认是将其复制到其数据结构中来实现访问的。
  • 对于__block修饰的外部变量引用,block是复制其引用地址来实现访问的。

另外,可以参考《招聘一个靠谱的iOS (下)13、14题》

ARC,MRC 对Block类型的影响

在ARC开启的情况下,将只会有NSConcreteGlobalBlock和NSConcreteMallocBlock类型的block。原本NSConreteStackBlock被NSConcreteMallocBlock类型替代。

在Block中,如果只使用全局或静态变量或者不是用外部变量,那么Block块的代码会存储在全局区。
在ARC中

  • 如果使用外部变量,Block块的代码会存储在堆区。

在MRC中

  • 如果使用外部变量,Block块的代码会存储在栈区。

Block默认情况下不能修改外部变量,只能读取外部变量。
在ARC中

  • 外部变量在堆中,这个变量在Block块内与在Block块外地址相同。
  • 外部变量在栈中,这个变量会被copy到为Block代码块所分配的堆中。

在MRC中

  • 外部变量在堆中,这个变量在Block块内与在Block块外地址相同。
  • 外部变量在栈中,这个变量会被copy到为Block代码块所分配的栈中

如果需要修改外部变量,需要在外部变量前声明__block。
在ARC中

  • 外部变量存在堆中,这个变量在Block块内与Block块外地址相同。
  • 外部变量存在栈中,这个变量会被转移到堆中,不是复制。

在MRC中

  • 外部变量存在堆中,这个变量在Block块内与Block块外地址相同。
  • 外部变量存在栈中,这个变量在Block块内与Block块外地址相同。

关于Block的面试题

  1. 使用block时什么情况会发生引用循环,如何解决?
  2. 在block内如何修改block外部变量?
  3. 使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?

参考链接

谈Objective-C block的实现
Block教程系列
对Objective-C中Block的追探
iOS中block实现的探究
Block 编程

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

推荐阅读更多精彩内容