iOS底层 -- block原理

1,440 阅读18分钟

欢迎阅读iOS底层系列(建议按顺序)

iOS底层 - alloc和init探索

iOS底层 - 包罗万象的isa

iOS底层 - 类的本质分析

iOS底层 - cache_t流程分析

iOS底层 - 方法查找流程分析

iOS底层 - 消息转发流程分析

iOS底层 - dyld是如何加载app的

iOS底层 - 类的加载分析

iOS底层 - 分类的加载分析

iOS探索 - 多线程之相关原理

iOS探索 - 多线程之GCD应用

iOS探索 - 多线程之底层篇

本篇文章主要分析block的类型,循环引用的解决,以及block的底层原理。

1.block的类型

1.1 全局block

@property (nonatomic , copy) TextBlock textBlock;

self.textBlock = ^{
    
};

NSLog(@"Block is %@", textBlock);

截屏2021-03-30 下午4.35.48.png

全局block在编译期就被置于macho中的数据段。

block内部没有使用外部变量,或者只使用的是静态变量或全局变量时,为__NSGlobalBlock__即使声明的block属性使用了copy修饰也是如此

1.2 栈block

int textA = 0;
void (^__weak textBlock)(void) = ^void {
    NSLog(@"%d",textA);
};
NSLog(@"Block is %@", textBlock);

截屏2021-03-30 下午4.28.12.png

MRC下,捕获外界变量时,此block栈block,但是ARC下系统默认使用__strong修饰且会自动进行copy成堆block。故需要__weak声明.

内部使用了局部变量或者oc属性,但是没有赋值给强引用或者copy修饰的变量,为__NSStackBlock__

1.3 堆block

int textA = 0;
void (^__strong textBlock)(void) = ^void {
    NSLog(@"%d",textA);
};
NSLog(@"Block is %@", textBlock);

截屏2021-03-30 下午4.29.11.png

ARC下,捕获了外界变量(不需要__Block和手动调用copy),就会被拷贝到堆区。

内部使用了局部变量或者oc属性,且赋值给强引用或者copy修饰的变量,为__NSMallocBlock__

总的来说,block是什么类型,先看它是否使用了外部变量,没使用就是全局block,不管是否强引用;使用了就是栈或者堆,这时候在判断block是否使用了强引用,强引用就是堆,否则就是栈。

1.4 其他的block

这三种block是系统提供程序员使用的,实际上还有另外三种block的存在,只是系统自身使用。

libclosure源码中存在如下定义:

/********************
NSBlock support    //block的根类
...
**********************/

//以下为所有的block子类型

void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };

总结:一共有6种block,它们都属于NSBlock,只不过只有其中3种是能使用到的

2.循环引用

block使用不当容易循环引用,导致页面无法被释放。

2.1 循环引用产生

self.block = ^{
    NSLog(@"%@",self.name);
};

[UIView animateWithDuration:1 animations:^{
    NSLog(@"%@",self.name);
}];

以上两个代码很明显是第一个会产生循环引用。

因为self持有了block,在block内部又持有了self,导致了互相持有。第二段代码不存在互相持有的关系。

60171617101719_.pic.jpg

要想断开互相持有的关系,必定要有一边弱引用。如果是开头弱引用会造成对象提早被置空的尴尬,那只能是尾部的对象弱引用。

60111617101602_.pic.jpg

是否循环引用,主要就是看是否能画出互相持有的关系图。比如例子中的:self -> block -> self

2.2 循环引用的解决

2.2.1 第一种方式(强弱共舞)

__weak typeof(self) weakSelf = self;

self.block = ^{
   NSLog(@"%@",weakSelf.name);
};
self.block();

把后面使用的self指向weakSelfweakSelf是被添加到全局的弱引用表中,不会对引用计数做+1处理,当作用域结束时就会被释放。这样就达到目的了。

但只是这样处理是不保险的,如果在block内部执行的是耗时的方法,那在页面销毁时self就被释放,也就意味着弱应用表中的weakSelf被释放,因为它们是指向同一片内存地址,后续依赖weakSelf的操作自然也就毫无意义了。

正确的做法应该是在block内部在局部强引用下weakSelf,防止这种情况的发生。

因为这样操作后,在页面销毁时,self相当于还是被强持有,不会调用dealloc释放self,只有当2秒延时执行后,block内部的作用域结束,局部变量strongSelf是在栈空间,被系统回收,强持有结束,才会调用dealloc释放self

__weak typeof(self) weakSelf = self; 
self.block = ^{        
    __strong typeof(weakSelf) strongSelf = weakSelf;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@",strongSelf.name);
    });
};
self.block();

所以最终正确的做法,应该是外部__weak修饰一下,内部在用__strong修饰一下。

2.2.2 第二种方式(手动置空)

__block ViewController *vc = self;
self.block = ^{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@",vc.name);
        vc = nil;
    });
};
self.block();

既然系统在weakSelf作用域结束时会自动置空,那也可以仿造此行为:

  • 局部强应用self,在不需要时手动将其置空。

2.2.3 第三种方式(参数形式)

self.block = ^(ViewController * vc) {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@",vc.name);
    });
};
self.block(self);

前两种方法是持有已经形成,采用对尾部的对象弱引用的思想。而参数形式的方法是直接不让selfblock持有,自然就没循环引用的可能了。

2.3 哪些页面有循环引用

实际开发中,页面没有及时释放,多半就是因为发生了循环引用。

这里提供一种检查页面是否释放的方式,在基类的dealloc方法中打印,打印的内容可以夸张些(表情符号之类),易于识别。

- (void)dealloc {
KSLog(@"%@==========dealloc", NSStringFromClass([self class]));
}

当发现哪个页面pop之后却没有打印信息,就找找这个控制器中哪里存在循环引用吧。

总结:推荐使用的处理方式

  • 手动置空的方式依赖于程序员的调用,如果忘记调用就存在循环引用,明显这是不可取的

  • 参数形式很优雅,但是可能会导致block过重,可以使用

  • 推荐使用第一种强弱共舞的方式,把对象生命周期交给系统管理是最可靠的,如果觉得写那两句代码很烦,可以用宏简化下

3.Block的clang分析

block本质是什么为什么能自动捕获变量为什么需要调用等问题是我们关心的,这一切都需要看看block的底层结构。

block语法看起来好像很特别,其实它实际上是被做为普通的c语言源代码处理的。通过clang可以把block语法的源代码转化为c++源代码。虽说是c++源代码,但底层也是struct结构体,本质上还是c的源代码。

转换如下:

clang -rewrite-objc 源文件名称 -o 目标文件名称.cpp

如果头文件有用到系统库,需要指定下路径:

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk 源文件名称 -o 目标文件名称.cpp

编写一个极为简单的block.c文件,

int main(){
    int textA = 10;
    void(^block)(void) = ^{
        printf("textA - %d",textA);
    };
    block();
    return 0;
}

转换后得到block.cpp,截取其中重要内容并简化后得到:

截屏2021-03-31 上午11.13.11.png

1.block的本质是什么

block被转化为__main_block_impl_0类型的结构体,内部还有__block_impl类型的结构体和__main_block_desc_0类型的结构体做为属性。

所以block的本质就是个结构体,是个对象,这也是为什么它能被%@打印的原因。

2.block为什么能捕获自动变量

__main_block_impl_0内部的结构和main函数源代码。

block在使用自动变量时,把自动变量textA声明为自身__main_block_impl_0结构体内的一个属性,并在__main_block_impl_0构造函数传入textA将其赋值,所以能捕获自动变量。

3.block为什么需要调用

block的实现函数,__main_block_impl_0内部的构造函数。

block的实现在底层被隐式的声明为__main_block_func_0函数,入参是__main_block_impl_0 类型的__cself,也就是自身。

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

__main_block_impl_0构造函数中,把__main_block_func_0当做入参,保存在__block_impl结构体内的FuncPtr属性。

所以需要外界调用block,也就是调用被保存的在自身的FuncPtr

4.block为什么不能直接修改自动变量

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int textA = __cself->textA;
        printf("textA - %d",textA);
}

在使用自动变量时,把结构体内部的textA值以__cself->textA形式赋值给新声明的textA中,再对这个新声明的textA做操作。

如果直接修改自动变量,相当于修改的是新声明的这个textA,可外界希望修改的是原textA,显然这毫无意义,且会产生代码歧义,编译器不知道你要修改哪个textA,产生报错。

5.__block为什么能修改自动变量

增加__block修饰后,继续使用clang转换成新的.cpp文件。

截屏2021-03-31 下午3.16.24.png

转换后的代码增加了不少,和原先最主要的区别在于:

  • textA被声明成__Block_byref_textA_0类型的结构体,不再是单纯的int

  • 新增了__main_block_copy_0__main_block_dispose_0函数,它们分别调用了_Block_object_assign_Block_object_dispose(这两个函数后续分析)

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_textA_0 *textA = __cself->textA;
        (textA->__forwarding->textA)++;
        printf("textA - %d",(textA->__forwarding->textA));
}

在调用函数(图片写成结构体了)__main_block_func_0时,textA是使用__cself->textA方式获取到的被保存在__main_block_impl_0结构体内的textA指针。也就是原先传入的textA的,后续对textA的操作,就是对外界textA的操作。所以使用__block修饰后的自动变量可以被修改。

__Block_byref_textA_0 * __forwarding;

使用__block后,会生产__Block_byref类型的结构体,并且在结构体内部保存了指向自身的__forwarding指针,外界修改时,就是通过指针获取原自动变量进行修改

6.栈block何时拷贝到堆区

可以看到,__main_block_impl_0构造函数内的blockisa被定义为_NSConcreteStackBlock,可是在第一节block的类型中说明ARC捕获自动变量后为_NSConcreteMallocBlock

那么可以肯定的是,一定是存在某种操作,使block产生从栈到堆的改变。

block汇编源码中的第一次跳转objc_retainBlock时,打个断点,然后读取下x0寄存器的值

截屏2021-03-31 下午2.35.19.png

register read x0

截屏2021-03-31 下午2.36.58.png

证实此时的确是个_NSConcreteStackBlock,单步执行跟进去,

截屏2021-03-31 下午2.38.03.png

发现又跳转到_Block_copy这个函数,这个关键函数记住,后续还要分析。

libObjc源码中搜索,也能确认objc_retainBlock就是调用了_Block_copy

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

进到_Block_copy的汇编,在最后准备return的地方打上断点,再次读取x0寄存器的值

截屏2021-03-31 下午2.42.04.png

证实在调用_Block_copy前,block还是_NSConcreteStackBlock,调用之后变成了_NSConcreteMallocBlock

block在构造函数初始化阶段确实是_NSConcreteStackBlock类型,只不过因为是ARC(未使用__block时),在合适的时机,系统会自动做拷贝到堆的操作,导致结果打印的类型却是_NSConcreteMallocBlock

这里两次都读取x0寄存器的原因是不同的。第一次是因为做为函数的第一个参数,block被存放在x0寄存器;第二次是因为block做为返回值被存放在x0寄存器。

有以下的情况栈上的block都会被拷贝到堆上:

  • 调用blockcopy实例方法时

  • block做为函数返回值返回时

  • block赋值给带有__strong修饰符id类型的类或block类型成员变量时

  • 在方法名中带有usingBlock的系统方法时

  • 使用GCDAPI传递的block

不过这些情况都是因为调用了_Block_copy函数。

需要注意的是:并非所有的block执行了_Block_copy都会变成堆block,比如全局block就不行。只有当是栈block时,这些情况才能成立。

7.block的签名是什么

block的打印信息中可以看到:

截屏2021-04-02 下午3.20.08.png

signature: "v8@?0"

其中,v代表返回值是void8代表8字节,而@?就代表block类型。

可以使用代码验证下:

[NSMethodSignature signatureWithObjCTypes:"@?"]

截屏2021-04-02 下午3.25.12.png

flags {isObject,isBlock}

打印信息证明@?isBlock,也是isObject

总结:

  • block本质是个结构体,是个对象

  • block内部使用自动变量时,会把自动变量声明为自身结构体的属性,产生捕获的功能

  • block的实现被保存在自身结构体中,需要外界调用

  • 不能直接修改自动变量是因为:block内部会生成新的变量,这个变量和捕获的自动变量不是同一个(指向的地址不同,只是值相同),

  • 而使用__block修饰后,会生成相应的结构体,保存原始变量的指针,修改的就是原始的变量

  • 调用_Block_copy后,栈block被拷贝到堆区,在堆区生成对应的block(必须是栈)

有趣的冷门小知识:

我们常说,block是带有自动变量的匿名函数。

但经过clang的分析,发现在.cpp底层中block其实也是有函数名称的,匿名是指外界不需要声明函数名。

并且它的名称有一定的规律:

block在函数中时格式为:

__函数名_block_impl_此block是函数中的第几个block

比如:__main_block_impl_0

block在方法中时格式为:

__文件名_方法名_block_impl_此block是方法中的第几个block

比如:__ViewController__addblcok__block_impl_0

多层嵌套时可以通过最后的参数查看是第几个block,这个小知识是本人测试所得,不敢保证全部情况试用。

4.block的底层原理

经过clang分析后,能大概明白block在底层是以什么样的方式存在的,但是它还不能说明一些东西。

比如:_Block_copy做了什么,__block是怎么实现的,_Block_object_assign_Block_object_dispose又是什么。

这一切只能打开block的源码libclosure探索了。

这源码还是比较好理解的,内容也较少,推荐阅读

截屏2021-04-01 下午4.25.42.png

一共只有四个文件,且只需要关注Block_private.hruntime.cpp这两个文件即可。

Block_private.h:主要是block相关的结构体声明和枚举值的定义

runtime.cpp:具体的实现

试想

block的结构体在.cpp中命名是动态的,可真实的底层结构体命名不可能是动态的,一定有个与之匹配固定的结构体名称。

来到Block_private.h寻找,果然存在和.cpp中相同的结构体,只是名称不同。

4.1 block的结构体Block_layout

block真实的结构体如下:

struct Block_layout {
    void *isa;  //8字节
    volatile int32_t flags;  //4字节
    int32_t reserved;  //4字节
    BlockInvokeFunction invoke;  //8字节
    struct Block_descriptor_1 *descriptor;  // 8字节
};

struct Block_descriptor_1 {
    uintptr_t reserved;   //4字节
    uintptr_t size;   //4字节
};

struct Block_descriptor_2 {
    BlockCopyFunction copy;   //8字节
    BlockDisposeFunction dispose;   //8字节
};

struct Block_descriptor_3 {
    const char *signature;   //8字节
    const char *layout;   //8字节
};
  • isablock的类型指向,类似对象中的isa

  • flags:标志位。且因为flags影响block的诸多操作,需要谨慎读取。所以使用volatile关键字以确保本条指令不会因编译器的优化而省略,且要求每次直接读值

  • reserved:系统保留字段,暂不使用

  • invoke:保存block的实现部分

  • Block_descriptor_1block的大小信息,必有的

  • Block_descriptor_2block是否有copydispose函数,可选的

  • Block_descriptor_3block的签名和拓展,可选的

4.2 _Block_copy分析

之前分析发现Block_copy是个重要函数,现在来跟进分析下。

截屏2021-04-02 下午3.38.52.png

内部只是根据标志位flags做对应的操作

BLOCK_IS_GLOBAL

如果是GLOBALBLOCK直接返回,这证明了调用Block_copy时,并不是都会被拷贝到堆上

// Its a stack block. Make a copy.

系统给出的注释,说明处理栈block

调用malloc在堆上开辟空间,大小就是传入block的大小,把原block内存上的数据全部复制到新开辟的block上,然后设置新block的一些属性,最后把isa置为_NSConcreteMallocBlock。其中了调用_Block_call_copy_helper会做拷贝成员变量的工作,并且内部调用_Block_object_assign

BLOCK_NEEDS_FREE

不确定此标志位的意思,但是反推下。已经有栈block和全局block的处理了,那它肯定包含堆block的处理。

static int32_t latching_incr_int(volatile int32_t *where) {
    while (1) {
        int32_t old_value = *where;
        if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
            return BLOCK_REFCOUNT_MASK;
        }
        if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
            return old_value+2;
        }
    }
}

只是对block的引用计数做处理。

_Block_copy所做的事,以下表格基本可以说明:

block的类型原blcok存储域复制效果
_NSConcreteStackBlock从栈复制到堆
_NSConcreteGlobalBlock数据区什么也不做
_NSConcreteMallocBlock引用计数增加

4.3 _Block_object_assign分析

__block修饰一个对象,然后重新生成.cpp

截屏2021-04-06 上午11.19.22.png

发现__block会生成的对应结构体

而源码中也给出了Block_byref的结构体

struct Block_byref {
    void *isa;
    struct Block_byref *forwarding;
    volatile int32_t flags; // contains ref count
    uint32_t size;
};

struct Block_byref_2 {
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    BlockByrefKeepFunction byref_keep;
    BlockByrefDestroyFunction byref_destroy;
};

struct Block_byref_3 {
    // requires BLOCK_BYREF_LAYOUT_EXTENDED
    const char *layout;
};
  • isa:可能是结构体的类型,但是源码中赋值总是NULL

  • forwarding:自身指向的指针

  • flags:标志位

  • size:大小

  • byref_keep__Block_byref_id_object_copy_131函数,也就是_Block_object_assign

  • byref_destroy__Block_byref_id_object_dispose_131函数,也就是_Block_object_dispose

截屏2021-04-06 上午11.19.27.png

__block对应结构体的构造函数

搜索构造函数中的第五个参数,

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

发现直接调用了_Block_object_assign函数。

来到源码,

9f9d9fc7f0fb4a90911bcc8ac7a24e8b_tplv-k3u1fbpfcp-watermark.png

第一句是重点代码,但很容易被忽略:

const void **dest = (const void **)destArg;

block捕获变量时,使用二级指针指向真正的目标指针,这是block能使用__weak解决循环引用的关键所在。

不同的flags代表传入的是不同类型:

  • BLOCK_FIELD_IS_OBJECT: 表示是一个对象
  • BLOCK_FIELD_IS_BLOCK :表示是一个block
  • BLOCK_FIELD_IS_BYREF :表示是一个byref,一个被__block修饰的变量
  • BLOCK_FIELD_IS_WEAK:__block 变量还被 __weak 修饰时

① 如果是对象类型,调用_Block_retain_object

static void _Block_retain_object_default(const void *ptr __unused) { }

可是_Block_retain_object是个空实现,其实就是直接以指针的形式赋值*dest = object

这是因为,对象的引用计数是由ARC管理的,不需要block插手,只要通过指针获取即可。

② 如果是block类型,调用_Block_copy(已分析)

③ 如果是被__block修饰,调用_Block_byref_copy(新函数,下面分析)

④和⑤ 如果是其他类型,也是直接指针赋值即可。

大概可以看得出来,_Block_object_assign函数是根据捕获自动变量的类型做对应的内存管理。

至于为什么入参是硬编码+40

截屏2021-04-06 上午11.48.11.png

__block生成的结构体可以略知一二,

截屏2021-04-06 上午11.51.01.png

捕获的自动变量在结构体的偏移值为40字节,+40即可得到此自动变量。

4.3.1 捕获的变量类型

当用__weak修饰变量时:

截屏2021-04-14 上午10.07.29.png

当用__strong修饰变量时:

截屏2021-04-14 上午10.08.22.png

block捕获变量时还有一个特点:遇到强引用捕获的就是强引用,遇到弱引用捕获的就是弱引用。意味着在block结构体内声明的属性类型和修饰符与捕获的变量一致。而这会导致block捕获变量后引用计数产生区别。

4.4 _Block_byref_copy分析

当使用__block修饰,会调用_Block_byref_copy函数。

截屏2021-04-06 下午3.56.33.png

① 新生成一个Block_byref类型的结构体,并赋初始值,其中这两句代码至关重要

copy->forwarding = copy; 
src->forwarding = copy;  

堆上的结构体的forwarding指向自身,栈上的结构体的forwarding指向堆上

WechatIMG6032.png

通过该功能,无论是在block语法中,还是block语法外使用__block变量,还是__block变量配置在栈上还是堆上,都可以顺利访问到同一个__block变量。

这也就说明了,为什么使用__block修饰的变量具有修改能力

② 如果结构体的标志位为BLOCK_BYREF_HAS_COPY_DISPOSE,则向Block_byref结构体的byref_keepbyref_destroy赋值(也就是copyDISPOSE函数),然后调用

(*src2->byref_keep)(copy, src);

其实也就是调用_Block_object_assign,对捕获的变量的内存进行操作。

③ 如果__block结构体本身已在堆上,直接对引用计数操作即可。

4.5 _Block_object_dispose分析

copy函数和dispose函数是堆block生命周期的开始和结束,他们对应的调用时机:

函数调用时机
copy函数栈上的block复制到堆时
dispose函数堆上的block被废弃时

截屏2021-04-06 下午4.26.49.png

按释放顺序依次说明:

调用_Block_release_object释放捕获的自动变量,不过其也是个空实现,变量的释放也是ARC管理的

调用_Block_byref_release减少引用计数

static void _Block_byref_release(const void *arg) {
    struct Block_byref *byref = (struct Block_byref *)arg;

    byref = byref->forwarding;
    
    if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
        int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
        os_assert(refcount);
        if (latching_decr_int_should_deallocate(&byref->flags)) {
            if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
                struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
                (*byref2->byref_destroy)(byref);
            }
            free(byref);
        }
    }
}
 byref = byref->forwarding;

通过结构体内部指向自身的forwarding指针找到自身(如果byref已经被拷贝,则取得是堆上的byref,否则是栈上的,栈上的不需要 release,也没有引用计数),如果是堆则减少引用计数,如果引用计数减到了0调用free释放(因为如果之前已经在堆上调用_Block_byref_copy只是引用计数加1,调用_Block_byref_release时也要一次一次减1)。

调用_Block_release减少引用计数

截屏2021-04-14 上午10.28.25.png

  • 2.1 block在堆上,才需要release,在全局区和栈区直接返回.

  • 2.2 引用计数减1,如果引用计数减到了0,调用free释放block

类似于拷贝时的一层层拷贝,释放也是一层层释放。

总结:

block从栈复制到堆上时,先复制自身到堆,在堆上生成对应的__block结构体(如果有使用__block),在拷贝捕获的自动变量。

这个过程相当于三层拷贝对应三个函数,block自身拷贝(_Block_copy函数),__block拷贝(_Block_byref_copy函数),自动变量拷贝(_Block_object_assign函数)

而释放过程亦是如此。_Block_release_object处理捕获的自动变量,_Block_byref_release处理__block对应的结构体,__Block_release处理block自身。

写在后面

block的面试题,首先需要判断block的类型,因为作用域对它的影响是很大的,然后搞清楚不同作用域的block生命周期和block的函数处理,以及捕获变量时是啥捕啥的原理。

block的分析到此结束。后续会更新其他底层系列文章,敬请关注。