16、iOS底层探索-Block

1、Block类型

  • 全局block
    • 如果没有使用外部变量,或者只使用全局变量或静态变量,则是全局block
  • 栈block
    • 如果使用了外部变量,赋值弱引用,则是栈block
  • 堆block
    • 如果使用了外部变量,赋值强引用,则是堆block

2、底层探索

小测验

- (void)viewDidLoad {
    [super viewDidLoad];

    NSObject *obj = [NSObject new];
    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));

    void (^block1)(void) = ^{
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));
    };
    block1();

    void (^__weak block2)(void) = ^{
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));
    };
    block2();
}

// 打印结果
134
复制代码
  • 在第一次打印时obj引用计数为1

  • block1是一个堆block捕获obj时会先将obj捕获到栈上,再copy到堆上,因此引用计数会被两次+1,所以block1打印为3

  • 弱引用的 block2是一个栈block,所以obj被捕获到栈上,不再向堆上copy,引用计数+1,打印为4

2.1、cpp文件分析block

image.png 转译成.cpp文件 image.png

  • 可以看到先创建为 栈block,并捕获了外部obj变量
  • .cpp中,block会被转换成_block_impl的结构体
    struct __block_impl {
      void *isa;          // 指针
      int Flags;
      int Reserved;
      void *FuncPtr;      // 存储代码块
    };
    复制代码
  • block本质是结构体

2.2、汇编探索

  1. 我们在block1处打上断点运行,并进入汇编调试 image.png
  2. 在汇编中找callq看到,block1中调用了objc_retainBlock符号(用__weak修饰后不会调用该符号),并且 此时是一个_NSConcreteStackBlock,也就是 栈block,那么我们可以尝试对 objc_retainBlock 下一个符号断点: image.png
  3. 可以看到其中使用了一个_Block_copy image.png
  4. 我们继续对 _Block_copy 下符号断点 image.png
  5. _Block_copy 中,看callq指令我们知道了,系统调用了malloc方法,而最终生成了__NSMallocBlock__也就是 堆block

2.3、源码探索

借用上边汇编探索的部分内容,我们知道了block是在 _Block_copy由栈block变为了堆block,那么我们在Block源码中搜索 _Block_copy

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
// 拷贝 block,
// 如果原来就在堆上,就将引用计数加 1;
// 如果原来在栈上,会拷贝到堆上,引用计数初始化为 1,并且会调用 copy helper 方法(如果存在的话);
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
// 参数 arg 就是 Block_layout 对象,
// 返回值是拷贝后的 block 的地址
// 运行?stack -> malloc
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    // 如果 argNULL,直接返回 NULL

    if (!arg) return NULL;

    // The following would be better done as a switch statement
    // 强转为 Block_layout 类型
    aBlock = (struct Block_layout *)arg;
    const char *signature = _Block_descriptor_3(aBlock)->signature;

    // 如果现在已经在堆上
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        // 就只将引用计数加 1
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        // block 现在在栈上,现在需要将其拷贝到堆上
        // 在堆上重新开辟一块和 aBlock 相同大小的内存
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        // 开辟失败,返回 NULL
        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;
#endif
        // reset refcount
        // 将 flags 中的 BLOCK_REFCOUNT_MASKBLOCK_DEALLOCATING 部分的位全部清为 0
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        // 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        // copy 方法中会调用做拷贝成员变量的工作
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        // isa 指向 _NSConcreteMallocBlock
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}
复制代码
  • block在全局:直接返回block本身
  • block在堆上:引用计数+1
  • block在栈上:将栈上的所有数据(包括block捕获后成为其成员变量的外部变量)全部copy到新开辟的Block_layout类型的 result 上
    struct Block_layout {
        void *isa;
        volatile int32_t flags; // contains ref count
        int32_t reserved;
        // libffi ->
        BlockInvokeFunction invoke;
        struct Block_descriptor_1 *descriptor;
        // imported variables
    };
    复制代码

3、循环引用

准备一份循环引用代码

#import "LZViewController.h"

typedef void(^LZBlock)(void);

@interface LZViewController ()

@property (nonatomic, copy) LZBlock block;
@property (nonatomic, copy) NSString *name;

@end

@implementation LZViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor greenColor];

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

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)dealloc {
    NSLog(@"%s",__func__);
}
复制代码

3.1、weak打破循环

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor greenColor];

    self.name = @"lz";
    
    // weak打破循环
    __weak typeof(self)weakSelf = self;
    self.block = ^{
        // strong保证block中代码执行后才会被释放
        __strong typeof(weakSelf)strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.name);
        });
    };
    self.block();
}
复制代码

3.2、手动释放

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor greenColor];

    self.name = @"lz";
    // 声明变量vc指向self
    __block LZViewController *vc = self;
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.name);
            // 使用后手动释放vc
            vc = nil;
        });
    };
    self.block();
}
复制代码

3.3、改为block参数

block不会捕获自身的参数,参数是栈自动管理的

#import "LZViewController.h"

typedef void(^LZBlock)(void);
// 在block中增加参数
typedef void(^LZBlock1)(LZViewController *);

@interface LZViewController ()

@property (nonatomic, copy) LZBlock block;
@property (nonatomic, copy) LZBlock1 block1;
@property (nonatomic, copy) NSString *name;

@end

@implementation LZViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor greenColor];

    self.name = @"lz";

    self.block1 = ^(LZViewController *vc) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
        });
    };
    self.block1(self);
}
复制代码

4、需要调用block()的原因

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
    }
    int a = 10;
    void (^block)(void) = ^{
        NSLog(@"%d",a);
    };
    block();
    return NSApplicationMain(argc, argv);
}
复制代码

转译.cpp文件:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
复制代码

image.png

  • 在.cpp中,block是一个__block_impl类型的结构体 impl
  • block执行__main_block_impl_0函数进行构造初始化
  • __main_block_impl_0 中第一个参数__main_block_func_0是代码块内容执行构造方法时被存储在 impl 的 FuncPtr 中
  • 当调用block()时,系统会调取 impl 的 FuncPtr 执行其中的代码块内容

5、__block

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
    }
    // 当想在block内部改变外部变量值时,需要添加__block修饰变量
    __block int num = 10;
    void (^block)(void) = ^{
        NSLog(@"%d",--num);
    };
    // 调用block
    block();
    return NSApplicationMain(argc, argv);
}
复制代码

转译.cpp文件: image.png

  • 对比于不加__block关键字,被捕获的外部变量变成了__Block_byref_num_0结构体

    struct __Block_byref_num_0 {
      void *__isa;
      // 默认指向结构体本身
    __Block_byref_num_0 *__forwarding;
     int __flags;
     int __size;
     int num;
    };
    复制代码
  • __main_block_func_0中代码块,打印--num的地方变成了--(num->__forwarding->num),也就是说我们需要知道num结构体 中的__forwarding指向

  • __main_block_copy_0中的_Block_object_assign会改变__forwarding指向

_Block_object_assign

image.png

  • switch case 的值: image.png 看注释可以知道 __block 修饰时会走 BLOCK_FIELD_IS_BYREF 分支

  • __Block_byref_copy image.png

小结

  • 当外部变量num使用__block修饰时,底层cpp中变量num会被变成 __Block_byref_num_0结构体num,其内部有一个__forwarding,默认先指向栈上的结构体num自身,代码块中使用这个num的地方也会变成使用num->__forwarding

  • __main_block_copy_0 中的 _Block_object_assign 方法 在block由栈copy到堆上的过程中,也将指向结构体num自身的__forwarding改为指向了堆上新copy的内容

  • 因此如果修改已被copy到堆上的block外部变量的值,也会影响指向这个堆的原本栈上的值

6、block底层结构

本文 2.3 中我们查看 _Block_copy 时我们看到: image.png

Block_layout

struct Block_layout {
    void *isa;              // 指向堆、栈、全局
    volatile int32_t flags; // 附加信息,类似指针的MASK掩码,和不同的参数取 与操作能得到不同的值      
    int32_t reserved;       // 保留变量
    // libffi ->
    BlockInvokeFunction invoke;  //实现的函数指针
    struct Block_descriptor_1 *descriptor;  // 存放copydispose、大小、签名等信息
};
复制代码
  • block的本质是 Block_layout
  • 对比.cpp文件中__block_impl类型的结构体 impl,可以发现内容很相似
  • flags:附加信息,类似指针的MASK掩码,和不同的参数取 与操作 能得到不同的内容(在Block_descriptor_1中就使用了flags获取copy、dispose与签名等内容
    // Values for Block_layout->flags to describe block objects
    enum {
        BLOCK_DEALLOCATING =      (0x0001),  // runtime
        BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
        BLOCK_NEEDS_FREE =        (1 << 24), // runtime
        BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
        BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
        BLOCK_IS_GC =             (1 << 27), // runtime
        BLOCK_IS_GLOBAL =         (1 << 28), // compiler
        BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
        BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
        BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31// compiler
    };
    复制代码
Block_descriptor_1

image.png 其中部分内容存放在 Block_descriptor_2Block_descriptor_3 中,它们的空间连续,使用时 通过内存平移调取

image.png

  • block的签名是@?

猜你喜欢

转载自juejin.im/post/7109115175223394341