美文网首页面试移动开发iOS技术
深入理解 weak-strong dance

深入理解 weak-strong dance

作者: Sheepy | 来源:发表于2016-08-15 02:32 被阅读2789次

    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 中。

    相关文章

      网友评论

      • 谭谭谭思密达:一时间大量使用weak是什么意思?
      • abs_:block的修饰符是copy,我觉得即使一个对象被销毁之前,他的成员属性的block 已经被拷贝了一份,复制体不会被销毁吧,还是能执行的,我的想法是这样的
      • 806349745123:Pro Multithreading and Memory Management for iOS and OS X with ARC, Grand Central Dispatch, and Blocks这书上也说过:在访问附有__weak修饰符的变量时,实际上必定要访问注册到autoreleasepool 和 楼主说的『局部变量occlass会持有捕获的对象,然后对象会被注册到 autoreleasepool。这是延长对象生命周期的关键』差不多意思。我个人理解是这样的:
        id a = [NSObject new]; // retainCount = 1
        id __weak b = a; // retainCount = 1
        此时当我们访问b的时候:
        id c = b;
        可以看成id c = [b autorelease];
        ARC这时候会将编译时候插入的类似[a release]等取消,由autorelease进行管理,延长了生命周期
      • hanl001:疑惑二__weak修饰的对象就是weak的吧,__autoreleasing修饰才是autorelease
      • honzon_0:typeof(weakSelf) strongSelf = weakSelf这一句等于__strong typeof(weakSelf) strongSelf = weakSelf,在 ARC 模式下,id 类型和 OC 对象类型默认的所有权修饰符就是__strong,所以是可以省略的

        在arc下 我这里的测试代码与你表述的好像有所不同

        @interface Honzon : NSObject

        @property (nonatomic, strong)void(^honzonBlock)(void);

        @EnD

        @Implementation Honzon
        @EnD

        Honzon *honzon = [[Honzon alloc] init];

        __weak Honzon *weakHonzon = honzon;
        printf("0: %ld\n", CFGetRetainCount((__bridge CFTypeRef)(weakHonzon)));
        //->0: 2
        honzon.honzonBlock = ^ {
        printf("2: %ld\n", CFGetRetainCount((__bridge CFTypeRef)(weakHonzon)));
        //->2: 2

        typeof(weakHonzon) strongHonzon1 = weakHonzon;
        printf("3.1: %ld\n", CFGetRetainCount((__bridge CFTypeRef)(weakHonzon)));
        //->3.1: 2
        printf("3.2: %ld\n", CFGetRetainCount((__bridge CFTypeRef)(strongHonzon1)));
        //->3.2: 2

        __strong typeof(weakHonzon) strongHonzon2 = weakHonzon;
        printf("4.1: %ld\n", CFGetRetainCount((__bridge CFTypeRef)(weakHonzon)));
        //->4.1: 3
        printf("4.2: %ld\n", CFGetRetainCount((__bridge CFTypeRef)(strongHonzon2)));
        //->4.2: 2

        };
        printf("1: %ld\n", CFGetRetainCount((__bridge CFTypeRef)(weakHonzon)));
        //->1: 2
        honzon.honzonBlock();

        可以看到 在arc下 weakHonzon在 3.1和4.1的retainCount明显不同,所以”typeof(weakSelf) strongSelf = weakSelf这一句等于__strong typeof(weakSelf) strongSelf = weakSelf”是不是有误,也可能是我的测试代码有问题,欢迎及时指出。
        806349745123:typeof(weakSelf) strongSelf = weakSelf != __strong typeof(weakSelf) strongSelf = weakSelf
        __strong typeof(weakSelf) strongSelf = weakSelf = NSObject *strongSelf = weakSelf

        typeof()在arc可能不是简单的获取类型,你可以试试
      • NSObjectKit: 经测试 strongSelf并不能延迟vc的释放 移除kvo也会导致崩溃, 不知博主测试过没有
      • 陈阿票:楼主,为什么我clang -rewrite-objc ViewController.m 的时候并不能得到.cpp文件?
      • ynot16:疑惑二提到的是通过strongSelf再次持有weakSelf来延长对象声明周期,但是解答疑惑二里面,好像只提到了一般情况下__weak关键字把对象放到autoreleasepool,防止循环引用的原理?没有提到stongSelf的作用,具体干了什么?也说到了是__weak把对象放倒autoreleasepool使得可以延长生命周期,那开头说到的“这时可以在 Block 内部再持有一次weakSelf指向的对象,延长该对象的生命周期,”的这句话,不是说strongSelf使得延长对象的生命周期么?看了几遍没理解到,希望大神指点 :blush:
        ynot16:@Sheepy 明白了好多,谢谢你。也就是说,weak strong dance,是为了防止block执行前,self被释放,而再用一个局部变量持有weakSelf的对象来延长生命周期。而不会导致循环引用,就是因为这个局部变量在block执行完后,超出作用域,变量释放,然后对象释放,最后block释放。是这样理解么。
        Sheepy:@ynotcc 其实也不一定是多线程情况下才会出现执行 block 的过程中 self 被释放的情况,所以一般如果有用 __weak 来解决循环引用的时候,都建议用一下 weak strong dance。这个在 OC 中写起来有点啰嗦,但用 swift 的时候是很顺的,weak 修饰的捕获列表参数会变成 optional,在 block 内部用的时候一般都会 guard let 去解包,这就已经是 weak strong dance 了。我之前不知道这个说法的时候,就一直是这么用的。
        Sheepy:@ynotcc 疑惑二其实是为了说明在 block 内用一个局部变量持有 self 指向的对象不会再次造成循环引用。你的问题是 weak strong dance 为啥能延长生命周期,其实 weak strong dance 要解决的问题是,多线程情况下,在执行 block 的过程中可能 self 指向的对象被废弃了。而在 block 内部用一个局部变量持有 self 对象的话,在执行 block 过程中,self 对象的引用计数是加了1的,就不会被废弃了。
      • ynot16:疑惑-:”而之后如果你把这个 Block 当作 GCD 参数使用(比较常见的需要使用 weak-strong dance 的情况),GCD 函数内部会把该 Block 再 copy 一遍,而此时 Block 已经在堆上,则该 Block 的引用计数加1。所以此时 Block 的引用计数是大于1的,即使self对象被废弃(譬如执行了退出当前页面之类的操作),Block 会被 release 一次,但它的引用计数仍然大于0,故而不会被废弃。“,为什么要说如果的情况,那要是不把这个block传给GCD,就是说引用计数还是1,self废弃,block就废弃么?还请大神解答
        Sheepy:@ynotcc 对啊,如果不传给 GCD 或者其他方法名中含有 usingBlock 的 Cocoa 框架方法,你也没有手动再去 copy 一次这个 Block 的话,self 废弃,它当然也废弃了。
      • soundtravel:疑惑一 的前提就是错的吧。。self指向的对象肯定是释放不了的
        Sheepy:@ynotcc 对啊,如果 block 执行前就退出了这个页面,还是打印 Self is nil。
        ynot16:@Sheepy 在你这个例子上面,如果加上strongSelf的操作,再次持有weakSelf的对象,还是会打印self is nil?
        Sheepy:@soundtravel 疑惑一指的是如下这种情况,Block 是延迟执行的,我的例子中是6秒后,在 Block 执行前,如果退出了这个页面,那这个 TestViewController 的实例的那块内存是会被废弃的,同时 self.handler 指向的那个 Block 也会被释放(相当于 MRC 下调用 release 方法),但由于这个 Block 被 GCD copy 了一次,所以它的引用计数还是大于0,它不会被废弃。6秒到了之后 Block 还会执行,会在控制台打印 Self is nil.
        @Implementation TestViewController

        - (void)viewDidLoad {
        [super viewDidLoad];

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

        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
      • 司马捷:测试的代码,如果放上来,就更好了了~求代码 :stuck_out_tongue_winking_eye:
        Sheepy:@机器人小雪 呃,测试代码指什么?测试 weak-strong dance 的使用场景什么的么,这些以前是写过,这次我就是直接写了篇文章啊……

      本文标题:深入理解 weak-strong dance

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