Weak-Strong

Weak-Strong Dance中Strong解析

注意多线程的情况. 多线程情况下,在执行 block 的过程中可能 self 指向的对象被废弃了。而在 block 内部用一个局部变量持有 self 对象的话,在执行 block 过程中,self 对象的引用计数是加了1的,就不会被废弃了

当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。

自然终止:正常运行run()方法后终止

异常终止:调用stop()方法让一个线程终止运行

在使用Block时常常可以看到Weak-Strong Dance的用法, 很多的文章以及官方文档都举例了这样做的原因. 但是还尚未发现有对strong进行讲解的. 下面就举个栗子具体分析下为什么加strong以及何时起作用

首先放上两个类似 ReactiveCocoa 中 定义weakify和strongify的宏 以便下文用到

#define WeakObj(o) autoreleasepool{} __weak typeof(o) weak##o = o
#define StrongObj(o) autoreleasepool{} __strong typeof(o) o = weak##o


一、weak的作用(代码+注解 简单跳过)

防止被block捕获(会导致引用计数加1), 打破循环引用(retain cycle)

// DeallocMonitor继承NSObject, 仅重写其dealloc方法, 并在其中打印其被释放日志
DeallocMonitor *object1 = [DeallocMonitor new];
DeallocMonitor *object2 = [DeallocMonitor new];
@WeakObj(object2);//__weak typeof(object2) weakobject2 = object2;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        // object1被block捕获, 引用计数加1, 外部作用域结束时仍未被释放, 直至该Block执行完毕才被释放
        NSLog(@"5s已到, %@该被释放勒", object1);
        // weakobject2被weak 修饰, 其指向的object2对象的引用计数不会增加, 当外部作用域结束时就已被释放
        NSLog(@"5s已到, %@早已被释放, 此处为null", weakobject2);
});
// 外部作用域结束

二、为何要加strong, 其何时才起作用?

加strong的原因想必大家都知道是为了防止block执行过程中 __weak typeof(object) weakObject指向的对象突然被释放了, 这就会导致block中的代码运行结果出现意想不到的结果(比如一些代码执行有效, 其余代码执行无效; 弱引用的对象因为为nil而导致的crash等.)

2.1 即使加了strong, 也不能保证weakObject指向的对象不会被释放

只能确保在block执行期间, weakObject指向的对象有效(不会被释放)

下面这段代码就是在block中用strong申明的对象强引用一次weakObject, 但修饰对象在block执行前就已经被释放的栗子

// DeallocMonitor继承NSObject, 仅重写其dealloc方法, 并在其中打印其被释放日志
DeallocMonitor *object = [DeallocMonitor new];
@WeakObj(object);// weakobject
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        // 该strongObj的申明仅在block执行时才见效, 而外部作用域一结束object就已经被释放了, 所以然并卵
       @StrongObj(object);
        /* weakobject用 weak修饰, 故其引用计数不变, 
          上边的宏本意是申明一个新的object局域变量对weakobject指向的原object进行强引用..
          按理 原object引用计数应该会加1, 可是它还没等到被强引用时就已经挂掉了
        */
        NSLog(@"5s已到, %@然后早已被释放, 此处为null", object);
});
// 外部作用域结束
2.2 Block内部申明的强引用指针变量指向weakObject仅在block执行时才有效

定义该Block的时strongObj宏还尚未使原对象引用计数加1! 那么strongObj宏生效时的表现是什么样子的呢? 继续上代码

// 该段代码主要是打了一个时间差, 以模拟strong申明起作用的情形

// DeallocMonitor继承NSObject, 仅重写其dealloc方法, 并在其中打印其被释放日志
DeallocMonitor *object = [DeallocMonitor new];
// 保证外部作用域结束的2.5秒(无限接近..)内object不会被释放
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.5 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        NSLog(@"果断强引用object: %@\n 还能再多坚持2.5s", object);
});
@WeakObj(object);// weakobject
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
       @StrongObj(object);// __strong typeof(object) object = weakobject
        sleep(3);// 卡个3s
        // 此处就不会像上一段代码那样, 强引用一个为nil的object, 故weakobject指向的对象引用计数加1, 直到该block运行完, 才会被释放
        NSLog(@"5s已到, %@打印完这个日志就飞升了", object);
});
// 外部作用域结束
2.3 有多少个嵌套block就应该申明多少对weak-strong

假定我们在最外层block使用的一对weak-strong, 且外层block内还有一个block(没有用weak-strong)引用到了strongObj宏申明的局域变量object, 并假设原对象在外层block开始运行前一直存活, 这就会导致内层block捕获到局域变量object并使其指向对象的引用计数加1, 因为内层block捕获到了外层block中申明的object(强引用), 就跟外层block会捕获到外部强引用变量指向的对象一样一样的

DeallocMonitor *object = [DeallocMonitor new];
@WeakObj(object);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
       @StrongObj(object);// 因为block运行时, weakObject指向对象依旧存在, 故该强引用使其引用计数加1
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
            // 这一层block 发现上边的object是强引用, 导致捕获到其指向对象, 使其引用计数在该内层block尚未执行时就加1了
            NSLog(@"打印完这个日志, %@才被释放", object);
        });
        NSLog(@"%@外层block结束, 引用计数减一", object);
});
sleep(3);
// 外部作用域所在线程小歇一会, 确保object存活3s, 作用域结束

所以嵌套block时 万万要小心, 不要漏写了. 另外weak-strong要成对出现, 不然少一个strong, 都有可能为此付出代价

2.4 遗漏补缺
  1. 在block中对外部weakObject进行强引用(strong修饰)的结果是使weakObject指向的原对象的引用计数加1, 因为weakObject指针指向的是原对象在堆中的存储地址
  2. block 不会对弱引用指针变量指向的对象进行捕获
2.5 block的相关知识, 个人推荐书籍章节
  • Effective-ObjectiveC(Item 37: Understand Blocks)
  • Pro Multithreading and Memory Management for iOS and OS X(Blocks Implementation)
三、题外篇(内存泄露检测工具-妈妈再也不用担心内存泄露)

对于ReactiveCocoa以及各种嵌套Block的常用玩家..想必仅靠Xcode的Instrument去检测memory leak问题是绝对不够的, 个人卖瓜推荐一个检测内存泄露的小工具类:
FXDeallocMonitor
拷贝FXDeallocMonitor.h、FXDeallocMonitor.m文件到项目中, 根据头文件中的方法调用就行, 简单易用

weak-strong dance 简介

使用 Block 时可以通过__weak来避免循环引用已经是众所周知的事情:

// OCClass.m

__weak typeof(self) weakSelf = self;
self.handler = ^{ NSLog(@"Self is %@", weakSelf); };

这时handler持有 Block 对象,而 Block 对象虽然捕获了weakSelf,延长了weakSelf这个局部变量的生命周期,但weakSelf是附有__weak修饰符的变量,它并不会持有对象,一旦它指向的对象被废弃了,它将自动被赋值为nil。在多线程情况下,可能weakSelf指向的对象会在 Block 执行前被废弃,这在上例中无伤大雅,只会输出Self is nil,但在有些情况下(譬如在 Block 中有移除 KVO 的观察者的逻辑,在执行到该逻辑前 self 就释放了)就会导致 crash。这时可以在 Block 内部(第一句)再持有一次weakSelf指向的对象,保证在执行 Block 期间该对象不会被废弃,这就是所谓的 weak-strong dance:

__weak typeof(self) weakSelf = self;
self.handler = ^{
    typeof(weakSelf) strongSelf = weakSelf;
    // ...
    [strongSelf.obserable removeObserver:strongSelf
                              forKeyPath:kObservableProperty];
};

typeof(weakSelf) strongSelf = weakSelf这一句等于__strong typeof(weakSelf) strongSelf = weakSelf,在 ARC 模式下,id 类型和 OC 对象类型默认的所有权修饰符就是__strong,所以是可以省略的。

问题

上面就是对 weak-strong dance 的扫盲级描述。不知道大家怎么想,反正我刚听说这个东西的时候,是有几个疑惑的:

  • self指向的对象已经被废弃的情况下,_handler成员变量也不存在了,在 ARC 下会自动释放它指向的 Block 对象,这个时候 Block 对象应该已经没有被变量所持有了,它的引用计数应该已经为0了,它应该被废弃了啊,为什么它还能继续存在并执行。(这个疑惑其实跟 weak-strong dance 无关,有兴趣的可以看看。)比如以下代码,在 Block 执行前退出这个页面的话,该 Controller 实例会被废弃,但 Block 还是会执行,会打印“Self is (null)”。
typedef void (^Handler)();

@interface TestViewController ()

@property (nonatomic, strong) Handler handler;

@end

@implementation TestViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    __weak typeof(self) weakSelf = self;
    self.handler = ^{
        typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"Self is %@", strongSelf);
    };

    NSTimeInterval interval = 6.0;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(interval * NSEC_PER_SEC)), dispatch_get_main_queue(), weakSelf.handler);
}

- (void)dealloc {
    NSLog(@"Released");
}

@end
  • 本来在 Block 内部使用weakSelf就是为了让 Block 对象不持有self指向的对象,那在 Block 内部又把weakSelf赋给strongSelf不就又持有self对象了么?又循环引用了?

要解决以上疑惑,需要对 ARC、Block、GCD 这些概念有比较深入的了解,主要是要清楚 Block 的实现原理。离职前不久我在公司做过一个关于函数式编程的内部分享,讲完 PPT 后有个同学问我“闭包”是怎么实现的,我当时没有细说,因为不同语言在实现同一个概念时肯定会有一些差异,我也不是什么语言都精通,所以不敢妄议。现在我也不敢说对所有语言的“闭包”实现都了如指掌,但至少对 OC 的闭包实现——Block 还算心中有数的。下面先简单介绍一下 Block 的实现,当然篇幅所限,会略过一些跟今天的主题关系不大的细节。

Block 的实现

Block 是 C 语言的扩展功能,支持 Block 的编译器会把含有 Block 的代码转换成一般的 C 代码执行。之前我一直有用到“Block 对象”这个词,因为一个 Block 实例就是一个含有“isa”指针的结构体,跟一般的 OC 对象的结构是一样的:

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

struct __xx_block_impl_x {
    struct __block_impl impl;
    // ...
};

所以跟一般的 OC 对象一样,这个isa指针也指向该 Block 实例的类型结构体(类对象,也有叫单件类的),Block 有三种类型:

  • _NSConcreteStackBlock
  • _NSConcreteGlobalBlock
  • _NSConcreteMallocBlock

这三种 Block 类的实例设置在不同的内存区域,_NSConcreteStackBlock 的实例设置在 stack 上,_NSConcreteGlobalBlock 的实例设置在 data segment(一般用来放置已初始化的全局变量),_NSConcreteMallocBlock 的实例设置在 heap。如果 Block 在记述全局变量的地方被设置或者 Block 没有捕获外部变量,那就生成一个 _NSConcreteGlobalBlock 实例。其它情况都会生成一个 _NSConcreteStackBlock 实例,也就是说,它是在栈上的,所以一旦它所属的变量超出了变量作用域,该 Block 就被废弃了。而当发生以下任一情况时:

  • 手动调用 Block 的实例方法copy
  • Block 作为函数返回值返回
  • 将 Block 赋值给附有__strong修饰符的成员变量
  • 在方法名中含有usingBlock的 Cocoa 框架方法或 GCD 的 API 中传递 Block

如果此时 Block 在栈上,那就复制一份到堆上,并将复制得到的 Block 实例的isa指针设为 _NSConcreteMallocBlock:

imply.isa = &__NSConcreteMallocBlock;

而如果此时 Block 已经在堆上,那就把该 Block 的引用计数加1

解答疑惑一

说到这里,已经可以回答上文的第一个疑惑了。把 Block 赋值给self.handler的时候,在栈上生成的 Block 被复制了一份,放到堆上,并被_handler持有。而之后如果你把这个 Block 当作 GCD 参数使用(比较常见的需要使用 weak-strong dance 的情况),GCD 函数内部会把该 Block 再 copy 一遍,而此时 Block 已经在堆上,则该 Block 的引用计数加1。所以此时 Block 的引用计数是大于1的,即使self对象被废弃(譬如执行了退出当前页面之类的操作),Block 会被 release 一次,但它的引用计数仍然大于0,故而不会被废弃。

捕获对象变量

Block 捕获外部变量其实可分为三种情况:

  • 捕获变量的瞬时值
  • 捕获__block变量
  • 捕获对象
    前两种情况跟今天的主题关系不大,先按下不表。第三种情况,也就是本文所举例子的情况,如果不用__weak,而是直接捕获self的话,代码大概是这个样子:
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __xx_block_impl_y {
    struct __block_impl impl;
    OCClass *occlass; // 对象型变量不能作为 C 语言结构体成员,可能还需要做一些类型转换,而且真实生成的代码并不一定叫 occlass,领会精神……
    // ...
};

static void __xx_block_func_y(struct __xx_block_impl_y *__cself) {
    OCClass *occlass = __cself -> occlass;
    // ...
}

// ...

也就是说,表示 Block 实例的结构体中会多出一个OCClass类型的成员变量,它会在结构体初始化时被赋值。而结构体中的函数指针void *FuncPtr显然是用来存放真正的 Block 操作的,它会在结构体初始化的时候被赋值为__xx_block_func_y__xx_block_func_y以表示 Block 对象的结构体实例为参数,从而得到occlass这个对象(即被捕获的self)。显然,这里会导致循环引用,而使用了__weak之后,表示 Block 对象的结构体中的成员变量occlass也将附有__weak修饰符:

__weak OCClass *occlass;

顺便说一下,__weak修饰的变量不会持有对象,它用一张 weak 表(类似于引用计数表的散列表)来管理对象和变量。赋值的时候它会以赋值对象的地址作为 key,变量的地址为 value,注册到 weak 表中。一旦该对象被废弃,就通过对象地址在 weak 表中找到变量的地址,赋值为 nil,然后将该条记录从 weak 表中删除。

那当我们使用 weak-strong dance 的时候是怎么个情况呢,会再次持有对象从而造成循环引用么?代码大致如下:

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

struct __xx_block_impl_y {
    struct __block_impl impl;
    __weak OCClass *occlass;
    // ...
};

static void __xx_block_func_y(struct __xx_block_impl_y *__cself) {
    OCClass *occlass = __cself -> occlass;
    // ...
}

解答疑惑二

__weak是个神奇的东西,每次使用__weak变量的时候,都会取出该变量指向的对象并 retain,然后将该对象注册到 autoreleasepool 中。通过上述代码我们可以发现,在__xx_block_func_y中,局部变量occlass会持有捕获的对象,然后对象会被注册到 autoreleasepool。这是延长对象生命周期的关键(保证在执行 Block 期间对象不会被废弃),但这不会造成循环引用,当函数执行结束,变量occlass超出作用域,过一会儿(一般一次 RunLoop 之后),对象就被释放了。所以 weak-strong dance 的行为非常符合预期:延长捕获对象的生命周期,一旦 Block 执行完,对象被释放,而 Block 也会被释放(如果被 GCD 之类的 API copy 过一次增加了引用计数,那最终也会被 GCD 释放)。

额外好处

上文说了每使用一次_weak变量就会把对象注册到 autoreleasepool 中,所以如果短时间内大量使用_weak变量的话,会导致注册到 autoreleasepool 中的对象大量增加,占用一定内存。而 weak-strong dance 恰好无意中解决了这个隐患,在执行 Block 时,把_weak变量(weakSelf)赋值给一个临时变量(strongSelf),之后一直都使用这个临时变量,所以_weak变量只使用了一次,也就只有一个对象注册到 autoreleasepool 中。



文/Sheepy(简书作者)
原文链接:http://www.jianshu.com/p/4e6153ea2734
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值