问题: 阐述一下你所了解的block所有相关内容.


  • block的本质

  • block的分类以及copy情况

  • block的变量捕获

  • block中 auto变量的强弱引用问题

  • __block的作用以及实现原理

  • block中的循环引用问题.

问题: 讲一下block的本质.


block的本质是OC对象.它的底层结构中也含有isa指针.

block 是封装了函数调用以及函数调用环境的OC对象.

实例, 在Demo中main函数中直接写一个 block,如下所示.

int main(int argc, const char *argv[]) {
    @autoreleasepool{
        void(^block)(void) = ^{
            NSLog(@"Test");
        };
        block();
    } 
    return 0;
}

block 会生成一个 __main_block_impl_0 的结构体, 结构体除了构造函数之外 还有两个成员变量.

一个是 __block_impl 类型的 impl. 另外一个是 __main_block_desc_0 类型的 Desc指针.

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.FunPtr = fp; // 函数结构体指针赋值
        Desc = desc;
    }
};

其中 Desc中主要包含了block的数据大小.这个是在构建构成中就确认了.

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __main_block_desc_0_DATA = {0, sizeof(struct __main_block_impl_0)};

如果block内部访问了外部的auto对象类型,那么还有存在copy函数指针和dispose函数指针.

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*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

__block_impl 结构体中主要包含了 isa 指针,以及函数结构体的指针FunPtr,结构如下所示.

函数结构体的指针FunPtr 会在 __main_block_impl_0 的构造函数中进行赋值.

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

调用过程我们发现, __main_block_impl_0类型的结构体 直接强制转换成了 __block_impl 类型的结构体 然后直接调用内部的函数指针 FunPtr. 完成block的调用过程.

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

注: 为啥可以直接强转?

由于 __block_impl 类型的 impl 是 __main_block_impl_0 中得首页成员变量,所以两者的内存地址是一致的,故可以强转.


问题: 讲一下block的变量捕获.


在block中,变量捕获情况如下所示.

  • 被auto修饰的局部自动变量是值捕获
  • 被static修复的局部变量为指针捕获.
  • 全局变量不捕获而是直接访问.
变量类型捕获到block内部访问方式
局部变量 (auto)值访问
局部变量 (static)指针访问
全局变量直接访问

对于类中方法,self是实例方法的隐式参数,属于局部变量,所以会进行捕获.


问题: 讲一下block的几种类型.


block 主要分为三种类型 __NSGlobalBlock____NSStackBlock____NSMallocBlock__ .

三者的形成条件如下所示.

block类型形成条件
__NSGlobalBlock__没有访问 auto变量
__NSStackBlock__访问了 auto 变量
__NSMallocBlock____NSStackBlock__ 调用了copy

三者的调用copy方法的本质如下表格所示.

block类型调用 copy 之后
__NSGlobalBlock__无任何变化
__NSStackBlock__从栈复制到堆
__NSMallocBlock__引用计数+1

在MRC环境下,如果需要改变Stack类型Block的存储位置,可以手动Copy.

在ARC环境下,编译器会根据情况自动对Block进行一次Copy操作.有以下几种情况.

  • block作为函数返回值.
  • block被 __storge 指针引用.
  • block作为Cocoa API 方法名为usingBlock的参数时.
  • block作为GCD的入参时.

问题: 讲一下block引用auto修饰的对象相关内容.


  1. 当栈上的block(__NSStackBlock__)访问了对象类型的auto变量时.
  • 将不会对auto变量进行强引用.
  1. 当堆上的block(__NSMallocBlock__)访问对象类型的auto变量时.
  • 在ARC环境下,会调用copy函数.
  • copy函数的内部会调用__Block_object_assign函数.
  • __Block_object_assign函数会根据auto变量的修饰符( __strong 、__weak 、 __unsafe_unretained)做出相应的操作,类似于retain操作(强引用、弱引用).
  1. 当堆上的block(__NSMallocBlock__) 访问对象类型的auto变量并且在释放时.
  • 会调用dispose函数.
  • dispose函数内部会调用 __Block_object_dispose 函数,
  • _Block_object_dispose 函数会自动释放引用的auto变量,相当于release操作.

问题: 讲一下__block修饰符的作用以及具体底层实现.


  • 作用1: __block 主要用于修改外部的auto自动变量的一个修饰符.
  • 作用2: 在MRC环境下,可用于解决循环引用(__block在MRC下会形成__weak修饰的数据结构体).

__block不能修饰全局变量、静态变量.

__block修饰符内部实际生成了一个带有isa指针的结构体.

auto变量添加了 __block 修饰符之后, 底层实际上 __main_block_impl_0 持有的是一个 __block的结构体指针.

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0 * Desc;
    __Block_byref_age_0 *age; // __block修饰产生的结构体指针
    
    ...

};

__block的底层结构体中,主要包含一下几个重要结构.

  1. isa指针.
  2. 指向 "本身" 的__forwarding指针.
  3. __block结构体的大小 __size.
  4. auto的成员变量.
struct __Block_byref_age_0 {
    void * __isa;
    __Block_byref_age_0 * __forwarding;
    int __flags;
    int __size;
    int age;
};
  1. 如果是被__block修饰的对象类型,还会存在内存管理的 copy函数指针 和 dispose函数指针.
struct __Block_byref_test_0 {
    void *__isa;
    __Block_byref_test_0 *__forwarding;
    int __flags;
    int __size;
    void (*__Block_byref_id_object_copy)(void*, void*);
    void (*__Block_byref_id_object_dispose)(void*);
    NSObject *__strong test;
}

__block内存管理问题:

  • 如果是栈上的block,那么它不会对__block修饰的变量产生强引用.

  • 如果把block复制到堆上时,会调用copy函数,函数内部会调用 __Block_object_assign 函数对其__block生成的结构体进行一次copy处理,把__block变量结构体拷贝到堆上并产生强引用.并且 __block变量结构体内部也会同时调用内部copy函数,根据指针类型来添加强弱引用(PS: 强引用的话,引用计数会 + 1)

  • 复制过程中 栈上的__block修饰的变量 __forwarding指针 指向的是堆上__block修饰的变量.

  • 如果堆上的block释放时,会调用 dispose函数, dispose函数内部会调用 __Block_object_dispose 函数对其进行__block形成的结构体释放操作(release操作).__block生成的结构体内部也会调用一次dispose函数,如果是强引用,那么就会进行release操作.


问题: 如何解决block中的循环引用问题?


  • 在 MRC环境下, 可以使用 __block 解决循环引用问题, 因为在MRC环境下, __block生成的变量结构体所持有的对象并不会有retain操作,引用计数不会变化.

  • 使用 __weak 修饰对象, __weak修饰的变量会在 block生成的结构体中形成一个 __weak修饰的指针成员变量,指向对象.循环链得以打破,可以解决循环引用.

  • 使用 __unsafe_unretained 修饰对象,也会产生 block生成的结构体中形成一个 __unsafe_unretained修饰的指针成员变量,指向对象.与 __weak不同的是, __weak的修饰的对象在内存释放时, __weak 修饰的指针会被置为 nil, __unsafe_unretained则不会置空,所以会有一定的风险.


IT界无底坑洞栋主 欢迎加Q骚扰:676758285