OC底层原理三十:block详解

OC底层原理 学习大纲

本节将深入探索block底层原理

  1. 循环引用 & 解决方案
  2. block结构分析
  3. 源码探索
  4. block的参数处理(多次拷贝)

1. 循环引用 & 解决方案

  • 正常释放:


    image.png
  • 循环引用:


    image.png
  • 解决循环引用的方法:
  1. weakSelf弱引用self,搭配strongSelf
  2. 使用__block修饰对象(必须在block中置空对象,且block必须被调用
  3. 对象self参数,提供给代码块使用
  • 测试代码:
typedef void(^HTBlock)(void);
typedef void(^HTBlock2)(ViewController *);

@interface ViewController ()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) HTBlock block;
@property (nonatomic, copy) HTBlock2 block2;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.name = @"ht";
    
//    // 循环引用 [self持有block,block持有self]
//    self.block = ^(void){
//        NSLog(@"%@",self.name);
//    };
//    self.block();
//
//    // 没有循环引用 (UIView的block持有self,self与UIView的block无关)
//    [UIView animateWithDuration:1 animations:^{
//        NSLog(@"%@",self.name);
//    }];
    
    // 方法1: `weakSelf`弱引用`self`,搭配`strongSelf`
    // weak不会让self的引用计数+1,所以不影响self的释放。 而strongSelf持有的是weakSelf。
    // 当self释放时,如果block执行完了,strongSelf局部变量就会被释放,此时weakSelf也被释放。所以不会造成循环引用
//    __weak typeof(self) weakSelf = self;
//    self.block = ^{
//        __strong typeof(self) strongSelf = weakSelf;
//        NSLog(@"%@",strongSelf.name);
//    };
//    self.block();
    
//    // 方法2:使用`__block`修饰对象(`必须`在block中`置空对象`,且block必须`被调用`)
//    __block ViewController * vc = self;
//    self.block = ^(void){
//        NSLog(@"%@",vc.name);
//        vc = nil; // 必须手动释放。  因为vc持有了self,vc不释放,self永远不可能释放。
//    };
//    self.block(); // 必须调用block。 因为vc持有了self,不过不执行block,vc永远没有nil,self永远不会释放
    
    // 方法3:`传`对象`self`作`参数`,提供`给代码块使用`
    // 最佳的使用方式,因为不会影响self的正常释放。(调用时,引用计数会+1,但是调用完后,就会-1,不影响self的生命周期)
    self.block2 = ^(ViewController * vc) {
        NSLog(@"%@",vc.name);
    };
    self.block2(self);
}
@end

2. block结构分析

2.1 block的结构

  • main.m文件中添加测试代码:
#import 

int main(int argc, const char * argv[]) {
    void(^block)(void) = ^{
        printf("HT");
    };
    block();
    return 0;
}

-clang编译main.m文件:

clang -rewrite-objc main.m -o main.cpp
  • 打开main.cpp文件:
    image.png
  • 可以发现block实际上是一个结构体,所以block支持%@打印。
  1. 函数声明(创建):
  • block是个结构体,初始化时,存储执行代码块匿名函数)和基础描述信息
  1. 函数调用:
  • 调用了FuncPtr,实际就是__main_block_func_0函数,入参block自己(这是为了捕获变量

2.2 block入参分析

2.2.1 直接加入变量(值拷贝)
  • 加入变量int a,重新编译:
image.png
  • 发现编译时,就生成了对应的变量__main_block_func_0匿名函数多了一个局部变量a,这个a是读取了cself内存值,是值拷贝。是只读变量

为了证明是a值拷贝只读变量,我们在测试代码block中添加a++赋值代码编译器立马报错提示:(造成了代码歧义

image.png

2.2.2 __block声明变量(指针拷贝)

int a使用__block声明:

image.png

与上面直接使用int a不同:

  • 上面传入的是a的值执行block函数时,是进行值拷贝只读
  • __block修饰后,会生成a的结构体对象传入的是对象指针地址执行block函数时,是进行指针拷贝可读可写,通过修改指针指向的内容。完成block内外通讯

3. 源码探索

3.1 Block的三种类型

Block有【3种类型】:

  1. __NSGlobalBlock__无入参时,是全局Block
  2. __NSMallocBlock__ :有外部变量时,变成堆区Block
  3. __NSStackBlock__:有外部变量,使用__weak修饰时,变成栈区Block
  • 测试代码:
- (void)demo {
    
    // 【3种block类型】
    // 1. __NSGlobalBlock__  (无入参时,是全局Block)
    void(^block1)(void) = ^{
        NSLog(@"HT_Block");
    };
    NSLog(@"%@",block1); // 打印: <__NSGlobalBlock__: 0x102239040>
    
    // 2. __NSMallocBlock__ (有外部变量时,变成堆区Block)
    int a = 10;
    void(^block2)(void) = ^{
        NSLog(@"HT_Block %d",a);
    };
    NSLog(@"%@",block2); // 打印: <__NSMallocBlock__: 0x600000f970c0>
    
    //3. __NSStackBlock__ (有外部变量,使用__weak修饰,变成栈区Block)
    int b = 20;
    void(^__weak block3)(void) = ^{
        NSLog(@"HT_Block %d",b);
    };
    NSLog(@"%@",block3); // 打印 <__NSStackBlock__: 0x7ffeedae8240>
}
image.png
  • 为弄清楚底层原理,首先得确定block源码哪个库中:

block创建前,加入断点

image.png
  • 打开汇编模式
image.png
  • 运行代码,加入objc_retainBlock符号断点:

    image.png

  • 继续运行,加入_Block_copy符号断点:

    image.png

  • 发现Block相关操作,是在libsystem_blocks.dylib库中:

image.png

源码地址 ,搜索libclosure-74,点击右边下载按钮。

  • 查看源码,可以关注到block结构类型Block_layout:(后面明白为什么是Block_layout
    image.png

Flag标识

image.png

  • 第1 位,释放标记,一般常用BLOCK_NEEDS_FREE& 位与操作,一同传入Flags,告知该 block 可释放
  • 第2-16位,存储引用计数的值;是一个可选用参数 (0xfffe二进制为1111 1111 1111 1110
  • 第25位,低16位是否有效标志,程序根据它来决定是否增加或是减少引用计数位的值;
  • 第26位,是否拥有拷贝辅助函数(是否调用_Block_call_copy_helper函数); 决定是否有block_description_2
  • 第27位,是否拥有block 析构函数;
  • 第28位,标志是否有垃圾回收; //OS X
  • 第29位,标志是否是全局block;
  • 第30位,与 BLOCK_HAS_SIGNATURE 相对判断是否当前 block拥有一个签名。用于runtime动态调用
  • 第31位,是否有签名
  • 第32位,标志是否有Layout,使用有拓展,决定block_description_3
  • Block基础结构梳理图:
Block基础结构
  • 下面,我们通过案例分析验证上面结构图

3.2 Block的类型转变

  • 我们从最简单无入参无返参全局block开始分析:
- (void)demo {
    void(^block)(void) = ^{
        NSLog(@"HT_Block");
    };
    block();
    NSLog(@"%@",block);
}
  • 为了便于寄存器读取操作,我这里使用真机进行演示,在void(^block1)(void) = ^{这一行加入断点,打开汇编模式运行代码至断点处

    image.png

  • 当前读取的是全局Block

    image.png

  • 我们给block添加入参,让block捕获外界变量:

- (void)demo {
    NSArray * arr = @[@"1"]; 
    void(^block)(void) = ^{
        NSLog(@"HT_Block %@",arr);  // 捕获外界变量:arr
    };
    block();
    NSLog(@"%@",block);
}
  • 断点位置不变运行代码,进入汇编页面后,断点在objc_retainBlock这行,打印x0(当前对象):
    image.png

Q: 按照上面打印结果来说,有外界变量修饰符时,应该是堆区Block,为什么这里是栈区Block?
A: Block栈区拷贝到堆区,是 _Block_copy操作的。(我们往下看)

  • 往下验证,加入objc_retainBlock符号断点,往下执行进入objc_retainBlock函数内部:

    image.png

  • 再次读取x0,发现此时没变。我们继续control+ 鼠标左键,点击进入按钮。不断使用register read x0p读取打印当前对象:

    image.png

  • 发现进入_Block_copy时是栈区Block,但是return出来时,却变成了堆区Block:

    image.png

  • 所以block真正从栈区拷贝到堆区,是_Block_copy进行的。

回顾上面Block基础结构梳理图,我们可以通过Block对象的首地址进行内存平移获取到invoke的。

  • 通过Block对象的首地址偏移,取到了_block_invoke

    image.png

  • control + 鼠标左键点击进入,发现_block_invoke内部就是block的函数执行内容

    image.png

【深入探究】

  • 既然我们可以通过内存平移,取到invoke函数。那顺便可以验证上面Block基础结构梳理图的完整结构:
  • objc_retainBlock之后,加断点,此时完成了_Block_copy 操作,完成栈区堆区block拷贝:
    image.png
  • 对照右边图,慢慢看吧 整个结构非常清晰。


    image.png
  • block类型@?,签名中包含参数个数入参返参具体内容占用内存大小。

3.3 _Block_copy

  • 进入源码。搜索_Block_copy:
// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
    
    // block都是`Block_layout`类型
    struct Block_layout *aBlock;

    // 没有内容,直接返回空
    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    // 将内容转变为`Block_layout`结构体格式
    aBlock = (struct Block_layout *)arg;
    // 检查是否需要释放
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 如果是全局Block,直接返回
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    //
    else {
        // Its a stack block.  Make a copy.
        // 进入的是栈区block,拷贝一份
        // 开辟一个大小空间的result对象
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        // 开辟失败,就返回
        if (!result) return NULL;
        // 内存拷贝:将aBlock内容拷贝到result中
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        //result的invoke指向aBlock的invoke。
        result->invoke = aBlock->invoke;
#endif
        // reset refcount
        // BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING :前16位都为1
        // ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING):前16位都为0
        // 与操作,结果为前16位都为0 应用计数为0
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        // 设置为需要释放,引用计数为1
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        // 生成desc,并记录了result和aBlock
        _Block_call_copy_helper(result, aBlock); //
        // Set isa last so memory analysis tools see a fully-initialized object.
        // 设置isa为堆区Block
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}
  1. 如果block需要释放(表示已经在堆区),就增加引用计数
  2. 如果是全局Block,直接返回Block_layout结构的aBlock
  3. 其他情况,都是从栈区拷贝到堆区
    malloc申请空间 -> memove内存拷贝 -> invoke指针拷贝 ->
    flag引用计数设为1 -> 生成desc -> 设置isa堆Block -> 返回堆区Block

4. block的参数处理(多次拷贝)

Q: block外界捕获变量怎么管理的?有哪些操作

  • __block修饰的NSArray为例,测试代码如下:
- (void)demo {
    __block NSArray * arr = @[@"1"];
    void(^block)(void) = ^{
        NSLog(@"HT_Block %@",arr);  // 捕获外界变量:arr
    };
    block();
    NSLog(@"%@",block);
}
  • 使用clangViewController.m文件生成ViewController.cpp文件,分析:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
image.png
  • 进入源码,搜索_Block_object_assign:
image.png
  • 不同枚举类型组合,有不同引用方式,其中最复杂的,是_Block_copy_Block_byref_copy

  • _Block_copy上面已分析过了,我们搜索_Block_byref_copy

    image.png

重点:

此处有2次拷贝+更深层次拷贝

  • 【第一次拷贝】:Block自身 (拷贝一份到)-> Block_byref结构体(src栈区结构体 )
  • 【第二次拷贝】: src栈区结构体 (拷贝一份到)-> copy堆区结构体
  • 【更深次拷贝】:调用byref_keep方法,内部又执行_Block_object_assign函数,再判断是否继续往下拷贝嵌套
  • 以上,就是Block创建调用Block释放调用_Block_object_dispose函数:
    image.png

关于Block探索,都在这里了。有兴趣可以一个个类型测试监测

你可能感兴趣的:(OC底层原理三十:block详解)