iOS由浅入深认识Block

1,132 阅读12分钟

1.什么是block

在日常的iOS开发中,我们经常会看到如下形式的代码:

image.png

这些包含在'^{}'或'^(){}'的大括号里面的代码块就是block。

2.block的使用

2.1 block的设置

image.png

这里设置了一个全局变量'aBlock',并且在viewDidLoad里面进行了设置

2.2 block的调用

image.png

在touchesBegan点击屏幕的时候调用了block

2.3 总结

block可以在一个地方设置,在另外一个地方调用,非常的灵活。

3.block的分类

我们通过如下代码打印三种block image.png

  • 1.block1没有捕获外部变量,它的类型是__NSGlobalBlock__,属于全局block;
  • 2.block2捕获了外部变量,并且变量block2对其进行了强引用,它的类型是__NSMallocBlock__,属于堆block;
  • 3.block3捕获了外部变量,但是变量block3对其进行了弱引用,它的类型是__NSStackBlock__,属于栈block;

4.block的循环引用解决

4.1 为什么会造成循环引用

4.1.1 正常释放

image.png

正常释放是指对象A持有对象B,A在释放时调用Dealloc方法时,并向B发送release信号;B接收到信号后,retainCount引用计数-1;如果此时B的引用计数为0,那么B也调用Dealloc方法进行释放。

image.png

4.1.2 循环引用

循环引用是指对象A持有对象B,B也持有A;所以A的引用计数一直都大于0,所以A无法调用Dealloc,并给B发送release信号;同理,B的引用计数也一直大于0,无法调用Dealloc,并给A发送release信号。循环引用导致两个对象之前互相持有,无法释放.

4.2 如何解决循环引用

block的循环引用时对象与block的持有情况

image.png

分析:

  • 1.打破self对block的强引用;这是不可行的,因为如果打破了这层关系,就没有对象持有block了,那么block一被创建出来就会被销毁;
  • 2.打破block对self的强引用,就是用这种方式.

4.2.1 方式一:自动释放

image.png

  • 1.这种方式我们称之为强弱共舞--weak-strong-dance;
  • 2.__weak修饰的weakSelf会被添加到一张弱引用表中,weakSelf和self会指向同一内存地址,不会导致self的引用计数+1,并且weakSelf会自动释放;
  • 3.__strong修改的strongSelf是一个对weaSelf强引用的临时变量,会在block执行完时自动释放,如果这里不声明strongSelf的话,当调用dealloc后再执行dispatch_after,NSLog打印的weakSelf会变成nil.

此时self、block、weakSelf、strongSelf四者的持有关系如下图:

image.png

这种方式打破了block对self的强引用,依赖于中介者模式,block执行完后,strongSelf会自动置为nil,从而self得以释放。这种方式属于自动释放

4.2.2 方式二:手动释放

image.png

通过__block修改一个临时变量vc去持有self,block对vc进行持有,当block持行完时,将vc进行释放,从而打破block对self的强持有

image.png

4.2.3 方式三:self作为参数传入block

image.png

将self作为参数传入block,block不会持有self,也就不会有循环引用的问题。

5.block的底层原理

5.1 block的本质

1.实现如下block

image.png 2.打开终端,cd到main.m文件所在的目录,执行如下clang命令xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m将main.m文件进行编译,得到main.cpp文件

image.png 3.打开main.cpp文件,找到main函数的实现

image.png

可以看到block = &__main_block_impl_0,传入了两信参数(__main_block_func_0,&__main_block_desc_0_DATA),即block指向了一个函数指针地址

4.查看__main_block_impl_0的实现,传入的参数__main_block_func_0__main_block_desc_0_DATA

image.png

  • 1.从上图我们可以看到__main_block_impl_0其实是一个结构体,它有两个属性implDesc,以及一个同名造构函数__main_block_impl_0;
  • 2.参数1__main_block_func_0被赋值给了__main_block_impl_0函数fp,最终被赋值给了impl.FuncPtr;
  • 3.impl.isa = &_NSConcreteStackBlock,由此说明block是一个结构体的对象;
  • 4.参数2__main_block_desc_0_DATA被赋值给了Desc. 所以block的本质是对象、结构体和函数,由于block函数没有名称,也称为匿名函数,也可以称block为闭包.

5.再来看一下block的调用被编译成了什么样的代码

image.png

简而写之:block->FuncPtr(block),block在调用的时候之所以传入block,类似与所有oc方法都会自带'self'和‘sel’两个隐藏参数是一个道理。

编译前后block的代码关系如下

image.png

5.2 block为什么需要调用

通过上面对block本质的探索我们已经知道,在block创建的时候,底层其实是__main_block_impl_0类型的结构体,通过其同名的构造函数进行创建,传入的参数1,对应的形参是fp,最终被赋值给了impl->FuncPtr;所以如果不执行FuncPtr的调用,block的代码是不会被执行的。总结如下:

  • 1.函数的声明:block内部声明了一个函数__main_block_func_0,此函数的代码即为block的{}中的代码;
  • 2.函数的调用:block对应的函数被保存到了FuncPtr属性中,所以需要执行block->FuncPtr(block)来调用block。

5.3 block是如何捕获外部变量的

将如下代码通过clang编译成c++的代码

image.png

其中定义了一个变量a,我们来看看变量a在编译后是如何被block捕获的

image.png

  • 1.可以看到变量a的值通过函数__main_block_impl_0传入了block的同名构造函数;
  • 2.在__main_block_impl_0结构体定义了一个int a;来接入外部变量a的值,从而实现了对外部变量的捕获.

总结:

block是通过在其对应的结构体中定义同类型同名和变量来捕获外部变量的。

5.4 __block原理

实现如下代码并执行

image.png

可以发现被__block修饰的变量a,在block内部a++后,在block外部的值也被改变了,这是什么原理呢?

同样,编译此代码

image.png

image.png

  • 1.可以发现,被__block修饰的a ,变编译成了__Block_byref_a_0的结构体;
  • 2.a的地址被__Block_byref_a_0类型的__forwarding记录;
  • 3.a的值被int a接收.

再来看看block的构造函数__main_block_impl_0和block对应的匿名函数__main_block_func_0 image.png

  • 1.block对应的结构体定义了__Block_byref_a_0 *a属性来接收外部传入的_a__forwarding指针。也就是说block的a外部变量a指向同一内存地址;
  • 2.再来看看__main_block_func_0函数中(a->__forwarding->a) ++;是对__block修饰的变量a的地址对应的值进行的修改,操作的是同一内存地址。所以当block内部对变量a进行++后,block的外部也能接收到a的值的改变.

总结:

  • __block原理是在编译时,将__block修饰的变量编译成了__Block_byref_变量名_0类型的结构体;
  • 在block内部通过__Block_byref_变量名_0类型的__forwarding指针指向外部的__Block_byref_变量名_0类型的变量;
  • 在修改__block修饰的变量时,是通过block->a->__forwarding->a来对变量进行修改的;
  • 对__block修饰的变量的修改,不管是在block内部还是在block外部都是通过__forwarding指针找到同一内存地址来进行修改。

5.5 block底层真正的类型

下面我们通过符号断点和汇编来block的源码所在,探索block底层的真正类型

1.在block处打断点,并开启汇编,运行代码

image.png 2.通过汇编分析,找到objc_retainBlock,打下符号断点并运行

image.png

3.继续打_Block_copy的符号断点并运行

image.png

由此我们找到了block的源码所在的库libsystem_blocks.dylib.

前往苹果开源网站下载最新的libclosure-78源码,打开并搜索_Block_copy

image.png

image.png

可以发现, _Block_copy的实现,传入的参数arg被赋值给了Block_layout类型的结构体对象

_Block_copy结构体中的属性:

1.isa,指向block所属的类;

2.flag,记录block对象的一些状态,它是一个枚举:

// 茕茕孑立注释: flags 标识
// Values for Block_layout->flags to describe block objects
enum {
 //释放标记,一般常用于BLOCK_BYREF_NEEDS_FREE做位与运算,一同传入flags,告知该block可释放
 BLOCK_DEALLOCATING =      (0x0001),  // runtime
 //存储引用引用计数的 值,是一个可选用参数
 BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
 //低16位是否有效的标志,程序根据它来决定是否增加或者减少引用计数位的值
 BLOCK_NEEDS_FREE =        (1 << 24), // runtime
 //是否拥有拷贝辅助函数,(a copy helper function)决定block_description_2
 BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
 //是否拥有block C++析构函数
 BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
 //标志是否有垃圾回收,OSX
 BLOCK_IS_GC =             (1 << 27), // runtime
 //标志是否是全局block
 BLOCK_IS_GLOBAL =         (1 << 28), // compiler
 //与BLOCK_HAS_SIGNATURE相对,判断是否当前block拥有一个签名,用于runtime时动态调用
 BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
 //是否有签名
 BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
 //使用有拓展,决定block_description_3
 BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

我们主要关注BLOCK_DEALLOCATING、BLOCK_HAS_COPY_DISPOSE和 BLOCK_HAS_SIGNATURE

3.reserved:保留信息,可以理解预留位置,猜测是用于存储block内部变量信息

4.invoke:函数指针,指向block的执行代码

5.descriptor:block的保留信息,保留位数、block的大小、copy和dispose的辅助函数指针,它有三种:

image.png

这三种descriptor的取值方式如下:

image.png

  • 1.descriptor1是一定会有的,直接返回;
  • 2.descriptor2是通过flags&BLOCK_HAS_COPY_DISPOSE判断是否有值的,并通过内存平移获取的;
  • 3.descriptor3是通过flags & BLOCK_HAS_SIGNATURE判断是否有值的,并通过内存平移获取的;

总结:block底层的真正类型是Block_layout

5.6 block的内存变化

image.png

通过_Block_copy函数的实现我们可以发现:

  • 1.如果需要释放,则去释放;
  • 1.如果是堆block,直接reture;
  • 2.else的情况是栈block,因为堆block需求手动分配内存;这里的处理是申请一块内存,将栈block通过memmove拷贝到Block_layout类型的result 对象中,并将isa指向_NSConcreteMallocBlock.

通过源码的分析,我们清楚的看到了block是如何从栈block变成堆block的。

5.7 block的签名

Block_layout内存布局,通过 内存平移 3*8 就可获得Block_layout的属性descriptor,主要是为了查看是否有Block_descriptor_2Block_descriptor_3,其中的属性descriptor3中有block的签名.

image.png

signature即可block的签名.

5.8 block的三层copy

5.8.1 第一层copy:_Block_copy

image.png

这里将栈block copy 为堆block

5.8.1 第二层copy:_Block_object_assign

首先需要知道外部变量的种类有哪些,其中用的最多的是BLOCK_FIELD_IS_OBJECTBLOCK_FIELD_IS_BYREF

enum {
    // see function implementation for a more complete description of these fields and combinations
    //普通对象,即没有其他的引用类型
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...
    //block类型作为变量
    BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable
    //经过__block修饰的变量
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable
    //weak 弱引用变量
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers
    //返回的调用对象 - 处理block_byref内部对象内存会加的一个额外标记,配合flags一起使用
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};

分析_Block_object_assign的源码

image.png

  • 1.BLOCK_FIELD_IS_OBJECT:如果是普通对象,则交给系统arc处理,并拷贝对象指针,即引用计数+1,所以外界变量不能释放;
  • 2.BLOCK_FIELD_IS_BLOCK:如果是block类型的变量,则通过_Block_copy操作,将block从栈区拷贝到堆区;
  • 3.BLOCK_FIELD_IS_BYREF:如果是__block修饰的变量,调用_Block_byref_copy函数 进行内存拷贝以及常规处理;

这里发生的第二copy,就是在_Block_byref_copy函数中,下面我们分析其源码

static struct Block_byref *_Block_byref_copy(const void *arg) {
    
    // Block_byref  结构体
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        
        // 问题 - block 内部 持有的 Block_byref 锁持有的对象 是不是同一个
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        
        copy->size = src->size;

        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3->layout = src3->layout;
            }

            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}

image.png

这里的Block_byref类型就是__block修饰的变量编译后的真正类型 将外部传进来的arg拷贝一份到'copy'中,并用__forwarding指针指向同一内存地址,这里是对外部变量的copy

5.8.3 第三层copy

定义一个__block修饰的NSString对象

 __block NSString *name = [NSString stringWithFormat:@"xjh"];
void (^block1)(void) = ^{ // block_copy
    lg_name = @"xjh";
    NSLog(@"xjh - %@",lg_name);
    
    // block 内存
};
block1();

xcrun编译结果如下,

  • 编译后的name比普通变量多了__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131;
  • __Block_byref_cjl_name_0结构体中多了__Block_byref_id_object_copy__Block_byref_id_object_dispose;
//********编译后的name********
 __Block_byref_name_0 name =
        {(void*)0,
            (__Block_byref_name_0 *)&name,
            33554432,
            sizeof(__Block_byref_name_0),
            __Block_byref_id_object_copy_131,
            __Block_byref_id_object_dispose_131,
            ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_hr_l_56yp8j4y11491njzqx6f880000gn_T_main_9f330d_mi_0)};
            
//********__Block_byref_name_0结构体********
struct __Block_byref_name_0 {
  void *__isa;
__Block_byref_name_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);  // 5*8 = 40
 NSString *name;
};
 
 //********__Block_byref_id_object_copy_131********
 //block自身拷贝(_Block_copy) -- __block bref结构体拷贝(_Block_object_assign) -- _Block_object_assign中对外部变量(存储在bref)拷贝一份到内存
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
    //dst 外部捕获的变量,即结构体 - 5*8 = 40,然后就找到了name(name在bref初始化时就赋值了)
    _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

 //********__Block_byref_id_object_dispose_131********
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

通过libclosure-74可编译源码断点调试,关键方法的执行顺序为:_Block_copy -> _Block_byref_copy -> _Block_object_assign,正好对应上述的三层copy

综上所述,block是如何取到name的呢?

  • 1.通过_Block_copy方法,将block拷贝一份至堆区;
  • 2.通过_Block_object_assign方法正常拷贝,因为__block修饰的外界变量在底层是 Block_byref结构体;
  • 3.发现外部变量是一个__block修饰的对象,从bref中取出相应对象name,拷贝至block空间,才能使用。最后通过内存平移就得到了name,此时的name 和 外界的name是同一片内存空间(从_Block_object_assign方法中的*dest = object;看出)。

总结:

  • 【第1层】通过_Block_copy实现对象的自身拷贝,从栈区拷贝至堆区;
  • 【第2层】通过_Block_byref_copy方法,将外部__block修饰的对象编译后的真实类型的结构体对象拷贝为Block_byref结构体类型;
  • 【第3层】调用_Block_object_assign方法,对__block修饰的实际类型变量的拷贝。

6.总结

在日常的开发中,我们要先充分认识block,精通block的使用,特别是循环引用的解决。理解block的本质,理解底层的原理更有助于帮助我们写出高质量的代码,以及灵活的使用block。

7.参考:月月大神的博客:iOS-底层原理 30:Block底层原理