深入理解 weak-strong dance

weak-strong dance 簡介

使用 Block 時可以通過__weak來避免循環(huán)引用已經是眾所周知的事情:

// OCClass.m

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

這時handler持有 Block 對象肖粮,而 Block 對象雖然捕獲了weakSelf喇澡,延長了weakSelf這個局部變量的生命周期度苔,但weakSelf是附有__weak修飾符的變量,它并不會持有對象迹鹅,一旦它指向的對象被廢棄了澳腹,它將自動被賦值為nil嗅战。在多線程情況下,可能weakSelf指向的對象會在 Block 執(zhí)行前被廢棄净赴,這在上例中無傷大雅绳矩,只會輸出Self is nil,但在有些情況下(譬如在 Block 中有移除 KVO 的觀察者的邏輯玖翅,在執(zhí)行到該邏輯前 self 就釋放了)就會導致 crash翼馆。這時可以在 Block 內部(第一句)再持有一次weakSelf指向的對象,保證在執(zhí)行 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了,它應該被廢棄了啊峡蟋,為什么它還能繼續(xù)存在并執(zhí)行坟桅。(這個疑惑其實跟 weak-strong dance 無關,有興趣的可以看看层亿。)比如以下代碼桦卒,在 Block 執(zhí)行前退出這個頁面的話,該 Controller 實例會被廢棄匿又,但 Block 還是會執(zhí)行方灾,會打印“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對象了么裕偿?又循環(huán)引用了?

要解決以上疑惑痛单,需要對 ARC嘿棘、Block、GCD 這些概念有比較深入的了解旭绒,主要是要清楚 Block 的實現原理鸟妙。離職前不久我在公司做過一個關于函數式編程的內部分享,講完 PPT 后有個同學問我“閉包”是怎么實現的挥吵,我當時沒有細說重父,因為不同語言在實現同一個概念時肯定會有一些差異,我也不是什么語言都精通忽匈,所以不敢妄議》课纾現在我也不敢說對所有語言的“閉包”實現都了如指掌,但至少對 OC 的閉包實現——Block 還算心中有數的丹允。下面先簡單介紹一下 Block 的實現郭厌,當然篇幅所限,會略過一些跟今天的主題關系不大的細節(jié)雕蔽。

Block 的實現

Block 是 C 語言的擴展功能折柠,支持 Block 的編譯器會把含有 Block 的代碼轉換成一般的 C 代碼執(zhí)行。之前我一直有用到“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 類的實例設置在不同的內存區(qū)域嘱根,_NSConcreteStackBlock 的實例設置在 stack 上,_NSConcreteGlobalBlock 的實例設置在 data segment(一般用來放置已初始化的全局變量)巷懈,_NSConcreteMallocBlock 的實例設置在 heap该抒。如果 Block 在記述全局變量的地方被設置或者 Block 沒有捕獲外部變量,那就生成一個 _NSConcreteGlobalBlock 實例顶燕。其它情況都會生成一個 _NSConcreteStackBlock 實例凑保,也就是說,它是在棧上的涌攻,所以一旦它所屬的變量超出了變量作用域欧引,該 Block 就被廢棄了。而當發(fā)生以下任一情況時:

  • 手動調用 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對象被廢棄(譬如執(zhí)行了退出當前頁面之類的操作)攀操,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)聚假。顯然,這里會導致循環(huán)引用闰非,而使用了__weak之后膘格,表示 Block 對象的結構體中的成員變量occlass也將附有__weak修飾符:

__weak OCClass *occlass;

順便說一下,__weak修飾的變量不會持有對象财松,它用一張 weak 表(類似于引用計數表的散列表)來管理對象和變量瘪贱。賦值的時候它會以賦值對象的地址作為 key,變量的地址為 value游岳,注冊到 weak 表中政敢。一旦該對象被廢棄,就通過對象地址在 weak 表中找到變量的地址胚迫,賦值為 nil喷户,然后將該條記錄從 weak 表中刪除。

那當我們使用 weak-strong dance 的時候是怎么個情況呢访锻,會再次持有對象從而造成循環(huán)引用么褪尝?代碼大致如下:

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 中。通過上述代碼我們可以發(fā)現龟虎,在__xx_block_func_y中璃谨,局部變量occlass會持有捕獲的對象,然后對象會被注冊到 autoreleasepool鲤妥。這是延長對象生命周期的關鍵(保證在執(zhí)行 Block 期間對象不會被廢棄)佳吞,但這不會造成循環(huán)引用,當函數執(zhí)行結束棉安,變量occlass超出作用域底扳,過一會兒(一般一次 RunLoop 之后),對象就被釋放了贡耽。所以 weak-strong dance 的行為非常符合預期:延長捕獲對象的生命周期衷模,一旦 Block 執(zhí)行完鹊汛,對象被釋放,而 Block 也會被釋放(如果被 GCD 之類的 API copy 過一次增加了引用計數阱冶,那最終也會被 GCD 釋放)刁憋。

額外好處

上文說了每使用一次_weak變量就會把對象注冊到 autoreleasepool 中,所以如果短時間內大量使用_weak變量的話熙揍,會導致注冊到 autoreleasepool 中的對象大量增加职祷,占用一定內存。而 weak-strong dance 恰好無意中解決了這個隱患届囚,在執(zhí)行 Block 時有梆,把_weak變量(weakSelf)賦值給一個臨時變量(strongSelf),之后一直都使用這個臨時變量意系,所以_weak變量只使用了一次泥耀,也就只有一個對象注冊到 autoreleasepool 中。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末蛔添,一起剝皮案震驚了整個濱河市痰催,隨后出現的幾起案子,更是在濱河造成了極大的恐慌迎瞧,老刑警劉巖夸溶,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異凶硅,居然都是意外死亡缝裁,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進店門足绅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捷绑,“玉大人,你說我怎么就攤上這事氢妈〈馕郏” “怎么了?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵首量,是天一觀的道長壮吩。 經常有香客問我,道長加缘,這世上最難降的妖魔是什么粥航? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮生百,結果婚禮上,老公的妹妹穿的比我還像新娘柄延。我一直安慰自己蚀浆,他們只是感情好缀程,可當我...
    茶點故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著市俊,像睡著了一般杨凑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上摆昧,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天撩满,我揣著相機與錄音,去河邊找鬼绅你。 笑死伺帘,一個胖子當著我的面吹牛,可吹牛的內容都是我干的忌锯。 我是一名探鬼主播伪嫁,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼偶垮!你這毒婦竟也來了张咳?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤似舵,失蹤者是張志新(化名)和其女友劉穎脚猾,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體砚哗,經...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡龙助,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了频祝。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泌参。...
    茶點故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖常空,靈堂內的尸體忽然破棺而出沽一,到底是詐尸還是另有隱情,我是刑警寧澤漓糙,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布铣缠,位于F島的核電站,受9級特大地震影響昆禽,放射性物質發(fā)生泄漏蝗蛙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一醉鳖、第九天 我趴在偏房一處隱蔽的房頂上張望捡硅。 院中可真熱鬧,春花似錦盗棵、人聲如沸壮韭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喷屋。三九已至琳拨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間屯曹,已是汗流浹背狱庇。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留恶耽,地道東北人密任。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像驳棱,于是被迫代替她去往敵國和親批什。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,654評論 2 354

推薦閱讀更多精彩內容