你真的懂 weak strong dance 吗?

在阅读这篇文章之前,首先思考如下问题:

  1. 为什么 weak strong dance 能够避免循环引用?
  2. 为什么不直接使用weak?
  3. 使用 weak strong dance 的正确姿势?

本文从 weak strong dance 的** 由来 用途 原理 扩展** 逐步分析解答上述问题,但不仅仅只是解答问题。


由来

在iOS开发中,无论objective-c还是swift都是采用引用计数来管理内存。而循环引用算是采用引用计数法管理内存中比较棘手的一个问题。在MRC时代,因为对象的引用计数是由程序猿自己来控制,优秀的程序员能够自如的把控对象之间的持有关系;到了ARC和swift中,由于编译器接手了内存管理的工作,为了方便程序员控制“对象之间的持有关系”,苹果于2011 WWDC Session #322中提出 weak strong dance ,官方示例代码如下

- (void)dealloc
{
  [[NSNotificationCenter defaultCenter] removeObserver:_observer];
}

- (void)loadView
{
  [super loadView];

  __weak TestViewController *wself = self;
  _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
                                                                object:nil
                                                                 queue:nil
                                                            usingBlock:^(NSNotification *note) {
      TestViewController *sself = wself;
      [sself dismissModalViewControllerAnimated:YES];
  }];
}

用途

在使用block时,因为block会捕获外部变量,并将其拷贝到block中。ARC 下这里的变量如果是指针变量,会将指针变量所指向的对象的引用计数加一。 因此如果不是对block有一定理解很容易发生循环引用,在这篇文章里有介绍会发生循环引用的情景。
在RAC及swift中,为了避免block带来的循环引用,官方推荐 weak strong dance ,即 在block外部声明一个弱引用对象,在block内部声明一个局部变量强持有这个弱引用,通过使用新生成局部变量来避免循环引用。


原理

说到原理,得从block的内部结构说起:


block内部结构

其中invoke指向block的实现代码,variables保存block捕获到的外部变量。

现在来分析引言中的示例代码:

  1. block 会将wself捕获到variables中,因为是weak修饰的,因此block不会对self进行强引用;同时 block 中的 invoke (函数指针)会指向 block 中的实现代码。
  2. 在执行block时(即调用invoke),TestViewController *sself = wself; 才会执行,如果执行这行代码时self还未释放,那么这里会将TestViewController实例的引用计数加一,防止在调用self期间self已经被释放。当这个方法执行完,ARC会自动将引用计数减一。
  3. 如果在执行block时,self已经释放,即wself被置为nil。那么 TestViewController *sself = wself; 执行时sself得到的也是一个nil,因此 [sself dismissModalViewControllerAnimated:YES]; 将不会被执行。
  4. 如果所有时候都和3中一样只是“不执行”,那该有多好。但是结果往往不如人意。不信? 测试代码如下:
///WeakTestObject.h
typedef void(^test_block)(void);
@interface WeakTestObject : NSObject
/**
 *  <#summary#>
 */
@property (copy,nonatomic) test_block block;
@end

///ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];

    WeakTestObject *obj = [[WeakTestObject alloc]init];
    obj.block = ^{
        NSLog(@"execute block in obj");
    };
    __weak __typeof(obj) wObj = obj;
    test_block VcBlock = ^{
        WeakTestObject *sObj = wObj;
        sObj.block();
    };
    obj = nil;
    VcBlock();    
}

VcBlock 在执行之前,obj已经释放,导致执行 VcBlock 过程中 sObj 以及 sObj.block 均为nil。程序进而crash在 sObj.block(); 这里。 真实情况往往比这里的模拟代码复杂很多,可能会经过几次时间和空间的跨度;那么如何避免这种crash呢?

两种处理:

  • 1 、对 sObj.block 进行判断
test_block block = ^{
        WeakTestObject *sObj = wObj;
        if (sObj.block) {
            sObj.block();
        }
    };
  • 2、 对 sObj 进行判断
test_block block = ^{
        WeakTestObject *sObj = wObj;
        if (sObj) {
            sObj.block();
        }
    };

显然第二种处理更优。首先饿哦们没有必要对sObj的每一个舒心进行判断,其实 在使用sObj 时 ,往往也不是仅仅执行它的一个block属性,而且会涉及到block嵌套或其他各种坑爹情况,其次根据接口封闭原则我们也不应该过多去关心类的实现。

最终 weak strong dance 的正确姿势如下:

__weak __typeof(obj) wObj = obj;
    test_block block = ^{
        WeakTestObject *sObj = wObj;
        if (sObj) {
            /// do ...
        }
    };

扩展

1. RAC中的宏

因为RAC中大量使用block语言,为了方便开发者RAC中定义了一对宏 @weakify() @strongify() ,对于这对宏的具体分析可阅读哀殿的 这篇文章 ,文中提到“Xcode 丢失了错误提示的能力”这一问题
另外YYKit中也定义了类似的宏,同时避免了上述问题,如下

#ifndef weakify
    #if DEBUG
        #if __has_feature(objc_arc)
        #define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object;
        #else
        #define weakify(object) autoreleasepool{} __block __typeof__(object) block##_##object = object;
        #endif
    #else
        #if __has_feature(objc_arc)
        #define weakify(object) try{} @finally{} {} __weak __typeof__(object) weak##_##object = object;
        #else
        #define weakify(object) try{} @finally{} {} __block __typeof__(object) block##_##object = object;
        #endif
    #endif
#endif

#ifndef strongify
    #if DEBUG
        #if __has_feature(objc_arc)
        #define strongify(object) autoreleasepool{} __typeof__(object) object = weak##_##object;
        #else
        #define strongify(object) autoreleasepool{} __typeof__(object) object = block##_##object;
        #endif
    #else
        #if __has_feature(objc_arc)
        #define strongify(object) try{} @finally{} __typeof__(object) object = weak##_##object;
        #else
        #define strongify(object) try{} @finally{} __typeof__(object) object = block##_##object;
        #endif
    #endif
#endif

这里补充另一个坑(请注意如下两种调用的区别)

//1
[self.viewModel.resignSubject subscribeNext:^(id x) {
        @strongify(self)
        [self.keyBoard keyboardDown];
    }];
//2
[self.viewModel.resignSubject subscribeNext:^(id x) {
        @strongify(self)
        [_keyBoard keyboardDown];
    }];

在 1 中 self.keyBoard 中的self其实是被重定义的局部的“self”, 而我们通过 _keyBoard 调用的话,表面上虽然看起来连self都没有调用,更不会有什么问题了。但,第二种写法其实是存在很大隐患的,系统在“寻找” _keyBoard 这个实例对象时,是通过对 self 指针进行地址偏移得到的,在这里编译器可不会对这个self进行宏替换。

在RAC源码中还有对另一个宏 @unsafeify() 的使用

RACCompoundDisposable *selfDisposable = self.disposable;
    [selfDisposable addDisposable:otherDisposable];

    @unsafeify(otherDisposable);

    // If this subscription terminates, purge its disposable to avoid unbounded
    // memory growth.
    [otherDisposable addDisposable:[RACDisposable disposableWithBlock:^{
        @strongify(otherDisposable);
        [selfDisposable removeDisposable:otherDisposable];
    }]];

@unsafeify() 就是 __unsafe_unretained 在RAC中的宏,但是这种用法即使在RAC源码中出现的都极少,不过 __unsafe_unretained 对比 __weak 来说在性能会比较好。

2. 接口设计原则

首先,并不是涉及到block引用外部对象的问题都会带来循环引用;其次,如果是我们自己设计一个类的时候,应该尽量的避免可能产生循环引用的问题(例如执行完block后将其置nil),如果实在无法避免应该在接口里详细说明。
例如:苹果框架中UIView封装的 animateWithDuration 方法、GCD等都
不会带来循环引用(注:NSTimer方法可能会带来循环引用); 还有一些有名的三方框架例如 Masonry 也不会产生循环引用。

3. swift 中的 weak strong dance
testFunc(closure:{ [weak self] in
            if let strongSelf = self {
                // do something
                print(strongSelf)
            }
        })

testFunc(closure:{ [weak self] in
            guard let strongSelf = self else {return}
            ///do something
            print(strongSelf)
        })

上面是根据可选绑定方式得来的较常规的写法,还有如下这种方式,可避免另外显示生成strongSelf

testFunc(closure:{ [weak self] in
            withExtendedLifetime(self, {
               print(self ?? 0)
            })
        })

只是经 withExtendedLifetime 处理后的self 变为了一个可选类型。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容