Block

Posted by LML on July 27, 2020

前言

本系列是 iOS Memory 相关内容作为主题的第二篇。本篇主要介绍 Block 内存原理、循环引用的原理。在看本文之前,可以先思考一下一个问题,然后在文中找到答案。

  • 如何定义一个 Block?
  • Blcok 到底是什么?
  • Block 有几种类型,有什么区别
  • __block 修饰符的原理以及作用
  • Block 捕获不同类型的变量的原理
  • Block 为什么会造成循环引用

block的写法分类

  • 作为属性
@property (nonatomic, copy, nullable) dispatch_block_t uploadSuccessedBlock;
@property (nonatomic, copy, nullable) void(^uploadFailedBlock)(NSError *error);
  • 直接定义
uploadFailedBlock = ^(NSError * _Nonnull error) {
                [handler uploadCommandResultFailedWithParams:customDict error:error];
            };

__nullable id(^block)(void) = ^() {
    return weakObject;
};

block 的本质

block 类型的变量,实质上是一个结构体类型的变量,比如

int main() {
    void (^blk)(void) = ^{
      printf("BLock")
    }
}

本质上是:

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *Funcptr;
};
struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long blocksize;
} __main_block_desc_0_data = {0,sizeof(struct __main__block__impl_0)};

struct __main__block__impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    __main__block__impl_0(void *fp,__main_block_desc_0 *desc,int flags = 0) {
        impl.isa = NSStackBlock;
        impl.Flags = flags;
        impl.Funcptr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(__main__block__impl_0 *cself) {
    printf("BLock")
}
int main() {
    void (^blk)(void) = __main__block__impl_0(__main_block_func_0,&__main_block_desc_0_data);
    blk.impl->Funcptr(blk);
    return 0;
}
  • isa 指针标示了 block 的类型
  • Funcptr 指针指向了函数指针

上面这张是最简单的 block 模型,也是最基本的 block 模型,揭示了 block 的本质是一个结构体。

但是不同类型的block 还会与上面展示的block有些不同,下面会进行详细介绍

Block 有几种类型,有什么区别

global block

什么是 global block 即 NSGlobalBlock?

  • 不捕获变量的
  • 捕获global var, Global static var, local static var

这种类型的 block,结构体内部不会增加捕获的变量,与上面示例的代码相同,因为即使捕获变量,全局变量、全局静态变量、局部静态变量存储在全局区域,整个生命周期只存在一份,因此只需要这种类型保存在全局区,不需要在自己的结构体内额外的保存捕获的变量

stack block

  • MRC:
    • 没有手动调用 block_copy 的 Block
    • 捕获变量,且捕获的变量类型有除去(global var, Global static var, local static var)之外的变量
  • ARC
    • 没有 strong 指针指向的
    __weak void (^myBlock1)(void) = ^{
        NSLog(@"%d",a);
    };

NSMallocBlock

  • MRC
    • 手动调用了 block_copy 的Block.也需要手动调用 Block_release 释放
    • Block作为属性,如果用copy修饰
      • 需要在 dealloc 方法中调用 Block__release 对 Block 进行释放
  • ARC
    • Strong pointer 修饰的 Block
    • 捕获了非在全局区存储的变量
    • 方法返回的 Block
      • 会自动现copy 到堆上,然后加入自动释放池

NSMallocBlock 被 copy,引用计数会加 1;NSStackBlock 被 copy 会由栈上 copy 到堆上,引用计数为1;NSGlobalBlock 被copy,不会发生变化

blcok 作为 property,用什么修饰?

block使用copy是从MRC遗留下来的“传统”。
在MRC中,方法内部的 block 是在栈区的,由于手动管理引用计数,需要 copy到堆区来防止野指针错误。
在ARC中,写不写都行,对于 block 使用copy还是strong效果是一样的,但写上copy也无伤大雅,还能时刻提醒我们:编译器自动对block进行了copy操作。如果不写 copy,该类的调用者有可能会忘记或者根本不知道“编译器会自动对block进行了copy操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。

作为参数和返回值的思考

三种Block作为方法的参数和返回值的时候需要注意下:

  • NSGlobalBlock:
    • 由于存储在全局区,生命周期贯穿整个程序,无论是作为参数还是返回值,无论是ARC还是MRC下都没有变化
  • NSMallocBlock——与正常的类实例对象类似
    • ARC
      • 参数:被赋值给函数参数中的strong poniter,引用计数+1.但是出了函数作用域,计数-1
      • 返回值:被加入 autoreleasePool,类似于 strong pointer 修饰的类对象
    • MRC
      • 参数:引用计数没有变化,没有被隐藏copy,如果是先定义了一个 mallocBlock, 然后异步执行一个方法,这个方法参数可能需要我们手动 copy 下 Block,结束的时候在手动 release,具体情况具体分析(大部分异步执行都是 bloc k实现,block会捕获对象)
      • 返回值:如果 init new等开头的方法,返回时候不需要操作;如果不是,则返回的时候我们需要对 BLock 调用auotrelease,承接者如想持有,需要调用 Block_copy
  • NSStackBlock
    • ARC
      • 参数:被自动copy了(因为stack Block被赋值给参数,参数是strong)
      • 返回值:先被自动copy,然后自动加入pool
    • MRC
      • 参数:需要手动进行copy
      • 返回值:需要手动进行copy,并加入自动释放池
  • Block 作为方法返回值的时候,与普通的 class 对象类似,分为 retain return value 和 no retain return value;前者在 MRC 下不需要做什么直接 return,承接着就会持有该对象 (stackBlock 需要手动 copy 下,因为还没有到堆上);在 ARC 下编译系统不会进行任何操作,承接者也不会;后者 MRC需要手动调用 autorelese(stackBlock需要先copy),ARC下系统会帮忙加入 autorelesepool(stackBlock会先帮忙copy)
  • 作为参数时候,也与普通的class对象类似:ARC自动帮忙retain+release;MRC需要自己分析释放要retain,一旦retain需要自己进行release

__ block 修饰符的原理以及作用

⚠️ __ block 一般只用于修饰非在全局区存储的变量(非 gloabl、local static、glpbal static),下面默认指的是非全局区域的变量。

数据结构

//__blocks 修饰基础变量对应的数据结构
struct __Block_byref_val_0 {
    void *__isa;
    __Block_byref_val_0 *forwarding;
    int __flags;
    int __size;
    int val;
};

//__blocks 修饰对象对应的数据结构???
struct __Block_byref_val_0 {
    void *__isa;
    __Block_byref_val_0 *forwarding;
    int __flags;
    int __size;
    id __ strong val;
};

__ block 修饰的变量无论是基础变量还是对象,都变成了一个结构体,结构体内部的 isa 指针标示该结构体是在stack、malloc 还是global 的。

比如一个对象的指针,若被 __ block 修饰,则其本质上会变成一个结构体,结构体内部会有个strong类型的指针指向对象的内存。 至于这个结构体存储在啊哪里,与被谁引用有关系,下面具体介绍。

结构体中比较重要的还有 forwarding 指针,为什么需要 forwarding指针呢?下面也会详细介绍

存储域

block 是存储域类说明符
__block 属性修饰的局部变量,从创建到到被 BLock 使用时,
block 变量存储在哪个区域呢?

  • 以下两种情况,__block(结构体)存储在栈上:
    • 刚初始化时;
    • 被栈BLock使用时
  • 下面情况存储在堆上:
    • 使用 __block 的 Block 从栈 copy 到堆上, __block 变量也会受影响,会被 copy 到堆上,并被持有
    • __block 被多个堆BLock使用,引用计数会增加
      • 怎么持有的呢?Block的结构内部持有指向__block变量的strong pointer
      • 如果配置在堆上的Block被废弃,那么他持有的__ block变量也会被释放

有一点值得注意,理解的时候需要区分基础类型变量和对象类型的变量

  • 基础类型变量
    • 未被__ block 修饰的时候就是一个栈上的基础变量
    • 被修饰的时候是一个结构体,可能在栈上也可能在堆上,取决于上面说的具体情况
  • 对象类型变量
    • 未被 __ block 修饰的时候,对象的确在堆上,但是定义的指向对象的这个指针在栈上
    • 被 __ block 修饰后,对象仍然在堆上,但是原来的指向对象的指针变成了结构体,结构体内部有个 staong 类型的指针 指向对象内存。结构体存储在哪里,取决于上面说的具体情况。

为什么有的变量需要 block 修饰?

什么变量需要 block 修饰呢?

  • 当 Block 想要更改捕获的变量时,这个变量需要用 block 修饰
  • 当期待 Block 捕获的变量(Block 捕获变量即会持有变量)与 Block 外的变量变化统一的时候

为什么需要 block 修饰呢?

简单来说,如果不用 block 修饰,Block 内部捕获的变量与没有被捕获的变量是两份,导致修改的时候内部的更改不能同步到 Block外部,例如:

  • 基础变量
    • 当没有 __block 修饰时,内外基础变量地址不同
    • 被__block 修饰时,内外地址相同
  • 对象类型
    • 当没有 __block 修饰时,内外指向对象的指针的地址不同

    • 有 __block 修饰时,内外指向对象的指针的地址相同


明白了为什么需要 __block 修饰,那么结构体中的 forwarding 指针的作用时什么呢?

为了保证

  • __block变量不管是配置在堆上还是配置在栈上,都能够正确的访问该变量
    • 更通俗的将:保证 __block 变量被 copy 到堆上之后,不通过 Block 访问 __block 变量也能访问到正确的变量
  • forward保证了:
    • 堆上的 __blockde forwarding指针指向自己
    • 栈上的 forwarding 指针指向自己被 copy 到堆上的那个内存;若没有被 copy 到堆上,则指向自己
    • 栈上的 __block 变量结构体实例会在 __block 变量从栈复制到堆上的时候,将自己的成员变量 forwarding 的值替换成复制目标即堆上的 __block 的结构体实例的地址

Block 捕获变量

上面介绍了什么情况下需要 __block 修饰,为啥需要修饰。这些都涉及到了 Block 对变量的捕获。本小节对 Block 捕获变量的原理进行详细介绍

捕获基础变量

没有 __block 修饰

  • 全局变量,全局 static 变量
    • Blocks 结构体与没有捕获变量相比无变化.且可以更改捕获的变量的值,且实际上根本没有捕获变量,因为是全局的保存一份就够了
  • 局部static变量
    • Blocks 结构体内部会增加一个指向 local static 内存的指针,且可以更改捕获变量的值
  • 局部变量
    • Blocks 内部会有变量的 copy,与原有的基础变量不是一个,所以编译器做了优化:Blocks内部不能更改捕获的局部变量的值
  • 小结
    • 能不能更改捕获变量的值,Blocks内部持有变量,都是出于保证:在执行Blocks的时候,捕获的变量仍然存在,能访问,不会因为其释放了而Crash

有block修饰。

上面说捕获局部变量不能修改其值,解决办法是用 __block 修饰,为什么这样就解决了呢?

  • __block修饰的局部自动变量变为一个结构体,结构体内部有基础类型变量
  • Blocks 结构体内部有指向__block结构体的指针
  • __block 结构体的存储位置会跟随 Blocks 的存储位置改变而改变
  • forward 指针的作用也在此显现:保证 Blocks 内部和外部的访问的结构体是同一个结构体

捕获对象

普通捕获对象

struct __main_block_impl_0 {
        struct __block impl impl;
        struct __main_block_desc_0 *Desc;
        id __strong obj;
        __main_block_impl_0(void *fp, __main_block_desc_0 *Desc, id __strong obj1, int flags = 0): obj(obj1) {
                impl.isa. = $_NSConCretStackBlock;
                impl.Flags=flags;
                impl.FuncPtr = fp;
                Desc = desc;
        }
}

Block 结构体内部会有个成员变量:strong 类型的指针,强持有普通对象。当Block 销毁时,也会释放对对象的强引用。Block 结构体对捕获的对象的强引用实质是调用了两个方法

  • _block_object_assign 方法持有Block截获的对象
  • __block_object_dispose 当堆上的Block被废弃的时候,调用此方法解除强引用

但是值得注意的是:Block截获对象需要两个条件

  • 被截获的对象 strong 修饰符修饰
    • 捕获 weak 修饰的对象的时候,Block内部不是 strong 修饰符,是 weak 修饰符,进而不会捕获,不会循环引用。即不会调用_block_object_assign
  • Block被copy到堆上
    • BLock 在栈上的时候,也不会调用 _block_object_assign

还有一点需要注意,Blocks 捕获对象,Blocks 结构体内部会新增一个id 类型的指针,指向对象,虽然与Blocks外的指针指向的内存是同一个,但是不是同一个指针,因此不允许改变指针的指向(优化)

捕获 __block 修饰的对象

__block修饰对象,被转换为结构体,该结构体也会捕获对象,和 Block 捕获对象很类似.需要满足两个条件,_结构体会持有对象

  • strong修饰
  • __block 结构体被栈上拷贝到堆上

至于__block 变量什么时候会被拷贝到堆上,与哪个 Block 捕获该 __block 变量有关。

总结下:
Block若捕获__block + strong修饰的对象:

  • __block位于堆上的时候强持有对象
  • Block强持有结构体
  • 二者均通过函数实现持有和释放

如果是 __block 修饰 weak 对象则,

  • __block不会强持有对象,weak 持有
  • Block强持有__block结构体,依然按照上述规律释放和持有

循环引用

Block 为什么会造成循环引用

Block 会持有捕获的对象,如果捕获的对象还持有Block,就形成了一个引用环,造成循环引用,进而造成内存泄漏。

举个最简单的例子

  • 被捕获的对象本身有strong修饰,引用计数1
  • Block 被捕获对象持有引用计数1
  • Block 持有捕获对象,捕获对象引用计数为2
  • Block 被捕获对象持有,除非不会对象释放,否则不会释放
  • 被捕获对象出作用域,引用计数减1,但仍为1
  • 这时候Block 和被捕获对象引用计数均为1,等待对方释放,内存泄漏

那么如何解决呢?

  • ARC
    • weak 修饰即将被 Block 捕获的变量,然后 Block 内部使用 weak修饰的变量,这样 Block 结构体内生成的是 weak 修饰的指针,没有强持有捕获对象
    • 不过值得注意的是,为了防止 Block 执行的过程中,捕获的变量释放,可以在Block内部最开始的位置用strong修饰捕获的变量,这个strong 修饰的变量作用域只在 Block 的{}中,是个局部变量,出了作用域即释放,不会造成循环引用
  • MRC

如何检测循环引用、内存泄漏

  • 工具
    • instrument 的 Leask 和Allocation
    • MLeaksFinder + FBRetainCycleDetector 检测引用环

后面会出博客介绍 MLeaksFinder + FBRetainCycleDetector 的原理

参考