美文网首页
(抄)如何更好地避免 block 循环引用

(抄)如何更好地避免 block 循环引用

作者: MSG猿 | 来源:发表于2016-06-22 14:56 被阅读400次

    http://pandara.xyz/2016/04/02/weakify_strongify/

    什么是循环引用

    先说循环引用,这个东西大概是大多数程序猿都应该知道的常识,不仅是 Objective-C ,很多其他语言也有这个概念。假设有两个类 A 与 B,A 中存在一个 ivar objB,B 中存在一个 ivar objA。现在各有一个 A 与 B 类型的对象: a 与 b,如果:

    a.objB = b;

    b.objA = a;

    这样就会产生循环引用,如下图:

    image

    iOS 中 ARC 内存管理机制用一句话概括起来就是:

    如果一个对象对象没有被其他对象强引用,那么它就将被销毁

    而由于 a 与 b 彼此强引用了对方,他们两者都无法被释放。要解决这个问题也简单,打破某一条强引用,将它变成弱引用就是了,比如可以:

    @interface A

    //@property (nonatomic, strong) B b; //原来的代码

    @property (nonatomic, weak) B b; //将 strong 引用改成 weak 引用

    @end

    弱引用:weak类型的指针也可以指向对象,但是并不会持有该对象。并且在对象被释放的时候,自动被设置为 nil

    那么 block 的循环引用也跟这个类似,只不过是其中一方变成一个 block:

    block 循环引用什么时候会发生?举一个简单的栗子,有一个 BBBViewController 类型的对象 bCon,通过一个 navigation controller 来加载显示:

    @interface BBBViewController : UIViewController

    @property (nonatomic, strong) UILabel *label;

    @property (strong, nonatomic) VoidBlock block;

    @end

    @implementation BBBViewController

    - (void)viewDidLoad {

    [super viewDidLoad];

    self.block = ^() {

    self.label.text = @"circular reference";

    };

    }

    @end

    在这种情况下,当 bCon 从 navigationCon 中被 pop 掉时,bCon 并不会被释放(可通过在 dealloc 中的断点来观察)。简单一点的情况编译器还能检查出来,然后给你一个警告:

    要是引用层级深一点,编译器可就不知道了:

    要知道 block 为什么会引用了 self,就要稍微了解一下:

    Objective-C block的实现

    关于 block 的实现,还有对外部变量的引用方式,在文末的参考文章可以找到非常详尽的说明,大概来说就是:

    block 实例会保存引用自外部的变量。可能是变量的值,也有可能是变量的指针(比如使用了 __block 修饰的变量)

    那么,对于self呢。参考文献中的例子都没有涉及到这一点。而我们实际上经常使用 block 作为一些对象的回调,其中往往涉及到当前类的属性设置。下面看一个简单的例子:

    //.h

    @interface CCCObject : NSObject

    @property (nonatomic) int value;

    @end

    //.m

    typedef void (^VoidBlock)();

    @implementation CCCObject

    - (id)init

    {

    if (self = [super init]) {

    VoidBlock block = ^() {

    int i = 1024;

    };

    block();

    }

    return self;

    }

    @end

    在 CCCObject 的 init 函数中定义了一个 block,这个 block 声明并初始化了一个局部变量,用 clang 将它翻译成 C++ 代码:

    clang -rewrite-objc CCCObject.m

    我们可以得到一个拥有 10W+ 行代码的文件,摘取主要部分如下:

    CCCObjectinit_block_impl_0

    这是 init 函数中 block 的结构定义。其中 impl 是每个 block 结构都会拥有的变量,它包含一些基本的信息:

    isa 指针,所有对象都会拥有该指针,用来实现对象相关的功能;

    flags,用来按 bit 位表示一些 block 的附加信息,在 block 进行 copy 的时候会使用到;

    funcPtr,函数指针,指向 block 中实际实现的函数调用地址,例如本例中的 __CCCObject__init_block_func_0

    desc,block 的附加描述信息,例如 size,还有 copy 跟 dispose 函数的指针

    CCCObjectinit_block_func_0

    从这个函数里面的那句代码就可以知道,这就是 block 在实际执行的时候调用的业务函数。

    接下来对 block 做一些修改,让它修改 属性 value 的值:

    - (id)init

    {

    if (self = [super init]) {

    VoidBlock block = ^() {

    self.value = 1024;

    };

    block();

    }

    return self;

    }

    接着还是 clang 一下,摘取主要代码部分如下:

    留意 block 结构体的实现,里面多了一个成员变量 CCCObject *self,用来保存所引用的 CCCObject 对象,在实现函数中通过 objc_msgSend 函数来对 value 属性进行设置。换句话说,block 强引用了 self.

    有兴趣的朋友可以再看看 _ICCCObject_init 这个函数,看起来好像一团糟的样子,因为它糅杂了 C 语言里面的两个最强利器:强制类型转换 以及 指针,而且还用得不少,看上去很糟糕。但它实际上做的主要事情是:

    通过构造函数 __CCCObject__init_block_impl_0 构造一个 block 对象,构造时候需要将实际业务函数 ***_func_0 作为其中一个参数传入;

    然后通过这个对象中的函数指针,来调用业务函数,需要将这个 block 结构自身作为参数传入;

    那么如果没有修改 self 的属性,而是单纯的将 self 作为值传递到一个局部变量中呢:

    - (id)init

    {

    if (self = [super init]) {

    VoidBlock block = ^() {

    CCCObject *cccObject = self;

    };

    block();

    }

    return self;

    }

    同样会强引用 self。

    解除 block 引用循环

    所以,如果在 block 中使用到 self 关键字(有例外,下详),而 self 通过某种方式引用了这个 block,那么引用循环就会产生。那么如何打破 block 的引用循环,经典的做法是:

    - (id)init

    {

    if (self = [super init]) {

    __weak __typeof(&*self) weakSelf = self;

    VoidBlock block = ^() {

    weakSelf.value = 1024;

    };

    block();

    }

    return self;

    }

    而 extobjc 这个库中的 EXTScope.h 定义了两个更方便的宏,可以更加优雅地解决 block 循环引用问题: weakify 和 strongify。最新版本的实现方法有点复杂,先看看传统的写法(以下内容主要参考自这里):

    #define weakify( x ) \

    _Pragma("clang diagnostic push") \

    _Pragma("clang diagnostic ignored \"-Wshadow\"") \

    autoreleasepool{} __weak __typeof__(x) __weak_##x##__ = x; \

    _Pragma("clang diagnostic pop")

    #define strongify( x ) \

    _Pragma("clang diagnostic push") \

    _Pragma("clang diagnostic ignored \"-Wshadow\"") \

    try{} @finally{} __typeof__(x) x = __weak_##x##__; \

    _Pragma("clang diagnostic pop")

    使用方法:

    - (id)init

    {

    if (self = [super init]) {

    @weakify(self) // 定义了一个__weak的self_weak_变量

    self.block = ^() {

    @strongify(self) // 局域定义了一个__strong 的同名 self 指针指向self_weak

    // 至于为什么没有报警告,看定义就知道了

    self.value = 1024;

    };

    self.block();

    }

    return self;

    }

    原理就正如注释所写的,转换成原生代码的话:

    - (id)init

    {

    if (self = [super init]) {

    __weak typeof(self) weakSelf = self;

    self.block = ^() {

    __strong typeof(weakSelf) strongSelf = weakSelf;

    strongSelf.value = 1024;

    };

    self.block();

    }

    return self;

    }

    解释一下:

    在 block 外部定义一个 weakSelf,供下面的 block 进行引用;

    在 block 中定义一个局部同名 self 指针,通过 weakSelf strong 引用自身,保证 block 执行的过程中,同名覆盖的变量(也就是 self)不会被释放掉;

    由于 block 对 self 是弱引用,在同名 strong 局部变量被释放之后,block 也就结束了所有对 self 的强引用;

    相关文章

      网友评论

          本文标题:(抄)如何更好地避免 block 循环引用

          本文链接:https://www.haomeiwen.com/subject/pgqndttx.html