iOS 底层原理06: Block原理

212 阅读6分钟

一、Block的本质

通过将OC代码编译, 我们可以看到Block编译后其实是一个OC对象(有isa指针), 我们就通过编译后的代码来分析一下Block的本质

  • 编译后的block整体是一个 __main_block_impl_0 结构体, 他包含了2个成员, impl 和 Desc.
  • 在impl中我们可以看到 isa指针, Flags, Reserved和FuncPtr. isa指针表明了block实际也是一个OC对象, 而FuncPtr指向叫做 __main_block_func_0() 函数的一个指针.
  • 而desc中目前只有 reserved 和 Block_size 2个成员

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;
    }
};

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
}

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

}

所以我们可以说, Block的本质是: 封装了函数调用和函数调用环境的OC对象

二、Block中修改局部变量的值为何无效? (Block的变量捕获)

block捕获值类型

    int height = 10;
    void(^blcok)(void) = ^{
        NSLog(@"height: %d", height); // height: 10
    };
    height = 20;
    blcok();

我们知道, 上面的代码打印结果height = 10, 那么为什么会这样呢? 我们将修改后的block代码再次编译, 查找原因

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int height;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _height, int flags=0) : height(_height) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int height = __cself->height; // bound by copy
    NSLog(height);
}
  • 首先我们可以看到block中多了一个 int height, 并且在构造时从外界传入
  • 在FuncPtr的函数中, 我们可以看到, 打印的height是block自己的height

block中多的int height 我们就叫做block的变量捕获. 当block代码块中使用到外部变量时, 会将变量捕获到block内部存储起来(值传递). 所以, 当外部的height修改为20时, block打印的依然是是自己的height, 所以输出10.

block捕获指针类型

我们将int类型换成对象, 再次实验

    Person *p = [[Person alloc] init];
    p.age = 10;
    
    void(^blcok)(void) = ^{
        NSLog(@"age: %d, age: %d", p.age); // age: 20
    };
    p.age = 20;
    blcok();

可以看到, 这次输出的是20, 我们根据变量捕获可以猜测, block这次是将对象p的指针捕获到了内部, 通过指针调用时, 输出的是修改之后的age值. 编译代码看我们是否猜测的正确.

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    Person *p = __cself->p; // bound by copy
    NSLog(objc_msgSend((id)p, sel_registerName("age"));
}

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*);
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

可以看到, 跟我们猜测的一样, block将p对象的指针捕获到了内部, 通过指针进行调用. 特别的: 跟OC对象一样, 使用的是objc_msgSend() 来调用方法. 另外, 我们可以看到desc中,多了 copy和dispose2个函数. 这2个函数其实是对捕获的p指针进行内存管理

block内部使用外部变量时, 会根据使用变量的类型, 捕获到自己内部. 然后使用时, 使用的是自己捕获的变量/指针. 同时, 在捕获指针类型变量时, 同样会添加内存管理相关的代码

三、Block的修饰词为什么要用copy?

在正常情况下, block是存放在栈上的, 如果block内部引用了auto变量, 那么block是不会对auto变量产生强引用的, 那么在auto变量离开作用域后就会自动释放, 那么block在调用是, 就会发生错误.

当对block使用了copy时, 会将block从栈copy到堆中, 此时, block内部会调用 _Block_object_assign() 根据auto的强弱修饰符对auto进行相应的引用. 这样, block就可以保证在使用时, auto变量没有被释放. 当block释放时, 会调用 _Block_object_dispose() 对自己捕获的变量进行内存管理.

在第二步中, 我们测试的捕获p变量, 编译器增加的代码就是 _Block_object_assign/_Block_object_dispose 相关的操作. 这是因为在ARC环境下, 编译器在某些时机会自动将我们的block copy到堆上

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况

  1. block作为函数返回值时
  2. 将block赋值给 __strong 指针时 (第二步的情况)
  3. block作为Cocoa API中方法名含有usingBlock的参数时 (如数组的枚举block)
  4. block作为GCD API方法参数时

四、Block中如何修改auto变量的值? (__block修饰符)

我们都知道, 在block中是无法修改auto变量的值, 编译器会直接报错. 通过我们上边代码的分析, 我们很容易明白. 因为在block体中使用的auto变量时block自己捕获的, 与block外声明的变量无关(block体内的函数与外部的变量也不在一个作用域), 肯定是无法修改的. 同时, 我们只要给auto变量加上 __block修饰符, 就可以修改auto变量, 这是为什么呢? 我们编译代码, 分析一下

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_b_height_0 *b_height; // by ref
    __Block_byref_b_p_1 *b_p; // by ref

    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_b_height_0 *_b_height, int flags=0) : b_height(_b_height->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

struct __Block_byref_b_height_0 {
    void *__isa;
    __Block_byref_b_height_0 *__forwarding;
    int __flags;
    int __size;
    int b_height;
};

struct __Block_byref_b_p_1 {
    void *__isa;
    __Block_byref_b_p_1 *__forwarding;
    int __flags;
    int __size;
    void (*__Block_byref_id_object_copy)(void*, void*);
    void (*__Block_byref_id_object_dispose)(void*);
    Person *b_p;
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_b_height_0 *b_height = __cself->b_height; // bound by ref
    __Block_byref_b_p_1 *b_p = __cself->b_p; // bound by ref

    (b_height->__forwarding->b_height) = 20;
    NSLog(b_height->__forwarding->b_height);
    NSLog((void *)objc_msgSend)((id)(b_p->__forwarding->b_p), sel_registerName("age")));
}

我们可以看到, 如果使用__block来修饰时, block捕获的变量由 int 变成了 __Block_byref_b_height_0类型. __Block_byref_b_height_0中包含了isa指针, 指向自己的__forwarding, __flags, __size以及真正存储变量的b_height.
当我们在修改b_height时, 是通过指针拿到__Block_byref_b_height_0结构体, 然后修改内部的b_height. 同样的, 对于对象p指针, 也是捕获一个__Block_byref_b_p_1结构体, 通过结构体内部的b_p指针来访问.

使用__block修饰auto变量以后, block会将变量进行封装, 封装成一个对象以后, 在使用指针进行对象的访问, 来达到在block内部修改auto变量的效果

五、Block中的循环引用? (__weak)

在使用block时, 如果不小心就会发生循环引用, 导致对象不能释放. 循环引用是怎么产生的呢? 一个对象只有block对象, block对象中又因为变量捕获机制持有了当前对象, 这就产生了循环引用, 导致block和当前对象都无法释放.

我们可以在block中使用当前对象时, 改为使用__weak修饰过的对象, 来打破这个循环. 为什么使用__weak就可以打破循环呢?

通过第三章节的讲解, 我们知道block在捕获变量时, 会同时生成 __main_block_copy_0()/__main_block_dispose_0()函数. 调用_Block_object_assign()函数时, 如果当前捕获的变量是使用__weak修饰的, 那么在函数内部就不会对当前对象进行强引用, 这样block内部就不会持有当前对象, 这个循环就从中间截断了.