重识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 编程

推荐阅读更多精彩内容