關於 Block 中捕獲 self 的分析
作者:kodyzhou
問題
最近遇到一個已經使用了weak-strong dance的block依舊強引用了self的情況,好在block沒被VC持有只是延遲釋放,但這裡的關鍵是用了weak_self的blcok理應不會強持有self才對,莫非之前的代碼都有問題?下面是」有問題的」代碼(為方便理解已刪掉部分無關代碼)
- (void)requestQBossYellowDiamondAdvWithId:(int)appid { qz_weakify(self); [[QBossEngine instance] getAdv:_uin appid:appid iReqFlag:0x01 key:@"" advCnt:1 advid:0 iPullAsExposeOper:1 withDone:^(NSDictionary *dict , NSString *traceInfo){ qz_strongify(self); _qbosstraceInfo = traceInfo; _bannerImageLink = dict[@"img"]; }]; }
的確有加weakify和strongify(宏的具體展開可參照下面的demo代碼),但仔細看代碼的話會發現訪問成員變數的時候都沒有加self,其實這裡有默認一個條件,即_qbosstraceInfo等同於self->_qbosstraceInfo,一般來講這樣理解是沒錯的,但是qz_strongify在block內重新定義了一個self的話也適用嘛?兩者如果等同的話block應該只捕獲外部的weak_self才對,但實際運行結果又與假設的不符,看來只能分析具體的實現了
重寫成C++代碼
下面是仿照qz_strongify寫法的demo代碼
- (void)testBlock { __weak KDTest *weak_self = self; id blockVar = ^{ _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored "-Wshadow"") __strong KDTest *self = weak_self; _Pragma("clang diagnostic pop") self->_testString = @"1"; _testString = @"2"; self.testString = @"3"; }; }
接著通過Clang重寫成C++,重點看self->_testString = @"1";和_testString = @"1";這兩句,重寫後的結果如下
(*(NSString *__strong *)((char *)self + OBJC_IVAR_$_KDTest$_testString)) = (NSString *)&__NSConstantStringImpl__var_folders_yz_mzcvr8_x7n18p3pdyf1f5n8m0000gn_T_block_6c1266_mi_0; (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_KDTest$_testString)) = (NSString *)&__NSConstantStringImpl__var_folders_yz_mzcvr8_x7n18p3pdyf1f5n8m0000gn_T_block_6c1266_mi_1;
可以看到裡面使用的同一個self,(char *)self + OBJC_IVAR_$_KDTest$_testString),不過其實這也證明不了什麼,因為就算重定義了self兩個也都是指向一個地址,重點還是看是否有強引用self,下面是block生成的結構體
struct __KDTest__testBlock_block_impl_0 { struct __block_impl impl; struct __KDTest__testBlock_block_desc_0* Desc; KDTest *__weak weak_self; __KDTest__testBlock_block_impl_0(void *fp, struct __KDTest__testBlock_block_desc_0 *desc, KDTest *__weak _weak_self, int flags=0) : weak_self(_weak_self) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
可以看到裡面只捕獲了一個weak_self,一開始我以為這就是最終結論,肯定是工具誤報沒錯了(′▽`) ,不過so上有個類似問題,裡面有一句話
if you write self->_testItVar you access data member of structure self, if you write only _testIVar you access ivar from current structure that is visible
大概意思就是不寫self的時候訪問的是當前可見的structure的變數,放在這裡來說就是即使自己重新定義了一個self,不加self使用的仍然是實例方法傳進來的self,重定義的self只對顯式的訪問有效,所以那就是說C++方法有問題嘍?剛好周會上也有說到重寫C++,其實真正編譯的時候代碼不會轉成C++,實際的實現不一定是這樣,所以這裡的C++代碼對不對是要打問號的,那麼把上面的demo代碼轉成彙編肯定不會有錯了吧
彙編代碼
利用Xcode自帶的彙編器分析下實現,由於轉成的彙編代碼(基於ARMv7)太長這裡只講關鍵部分
首先對於實例方法會帶上兩個隱藏的參數,一個是self,一個是cmd,下面是調用testBlock方法之前的初始化部分
push {r4, r5, r6, r7, lr} add r7, sp, #12 sub sp, #60 add r2, sp, #48 str r0, [sp, #56] str r1, [sp, #52]
ARM彙編有規定第一個參數會放入r0中,所以對應這裡r0就是self,可以看到有將self的值存入棧內,棧上的偏移為56
下面是創建block的部分(簡單一句賦值彙編就有這麼長?_?)
.loc 1 20 32 prologue_end @ /Users/kodyzhou/Downloads/block.m:20:32 ldr r0, [sp, #56] .loc 1 20 20 is_stmt 0 @ /Users/kodyzhou/Downloads/block.m:20:20 str r0, [sp, #12] @ 4-byte Spill mov r0, r2 ldr r1, [sp, #12] @ 4-byte Reload bl _objc_initWeak Ltmp1: add r1, sp, #48 add r2, sp, #16 movw lr, :lower16:(___block_descriptor_tmp-(LPC0_0+4)) movt lr, :upper16:(___block_descriptor_tmp-(LPC0_0+4)) LPC0_0: add lr, pc movw r3, :lower16:("___19-[KDTest testBlock]_block_invoke"-(LPC0_1+4)) movt r3, :upper16:("___19-[KDTest testBlock]_block_invoke"-(LPC0_1+4)) LPC0_1: add r3, pc movw r9, #0 movw r12, #0 movt r12, #49664 movw r4, :lower16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC0_2+4)) movt r4, :upper16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC0_2+4)) LPC0_2: add r4, pc ldr r4, [r4] .loc 1 21 8 is_stmt 1 @ /Users/kodyzhou/Downloads/block.m:21:8 add.w r5, r2, #24 add.w r6, r2, #20 .loc 1 21 19 is_stmt 0 @ /Users/kodyzhou/Downloads/block.m:21:19 str r4, [sp, #16] str.w r12, [sp, #20] str.w r9, [sp, #24] str r3, [sp, #28] str.w lr, [sp, #32] adds r2, #24 str r0, [sp, #8] @ 4-byte Spill mov r0, r2 str r6, [sp, #4] @ 4-byte Spill str r5, [sp] @ 4-byte Spill bl _objc_copyWeak ldr r0, [sp, #56] .loc 1 21 19 discriminator 1 @ /Users/kodyzhou/Downloads/block.m:21:19 bl _objc_retain add r1, sp, #16 .loc 1 21 19 @ /Users/kodyzhou/Downloads/block.m:21:19 str r0, [sp, #36] .loc 1 21 8 discriminator 2 @ /Users/kodyzhou/Downloads/block.m:21:8 mov r0, r1 bl _objc_retainBlock
block在創建的時候一開始是放在棧上的,調用了最後的_objc_retainBlock後才會拷貝到堆上,block本質就是一個結構體,布局如下圖,當需要捕獲外部變數的時候會把捕獲的變數放到結構體內,總之這裡關鍵就是要看是否有將self強引用並捕獲到block內,我們首先要先找到存放block指針的地方
movw r4, :lower16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC0_2+4)) movt r4, :upper16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC0_2+4)) LPC0_2: add r4, pc ldr r4, [r4] .loc 1 21 8 is_stmt 1 @ /Users/kodyzhou/Downloads/block.m:21:8 add.w r5, r2, #24 add.w r6, r2, #20 .loc 1 21 19 is_stmt 0 @ /Users/kodyzhou/Downloads/block.m:21:19 str r4, [sp, #16]
這裡就是用來初始化block第一個成員isa指針的部分,將指針存到r4然後通過str指令寫入棧內,可以看到它在棧上的偏移是16,按照struct的布局繼續往下看
str.w r12, [sp, #20] str.w r9, [sp, #24] str r3, [sp, #28] str.w lr, [sp, #32] adds r2, #24 str r0, [sp, #8] @ 4-byte Spill mov r0, r2 str r6, [sp, #4] @ 4-byte Spill str r5, [sp] @ 4-byte Spill bl _objc_copyWeak ldr r0, [sp, #56] .loc 1 21 19 discriminator 1 @ /Users/kodyzhou/Downloads/block.m:21:19 bl _objc_retain add r1, sp, #16 .loc 1 21 19 @ /Users/kodyzhou/Downloads/block.m:21:19 str r0, [sp, #36]
在連續存儲了棧偏移為20、24等幾個變數後,可以看到有句ldr r0, [sp, #56],前面說到這裡存儲的是self的地址,把self地址存到r0後馬上調用了_objc_retain方法,這個方法會將r0指向的對象引用計數+1,然後隨即將這個對象的地址存放到棧偏移36的地方,這裡應該就是強引用self的部分了,證據找到了!不過為了讓結果更明顯順便貼下當顯式指明self情況時的彙編代碼
.loc 1 21 9 is_stmt 1 @ /Users/kodyzhou/Downloads/block.m:21:9 add.w r5, r2, #20 .loc 1 21 20 is_stmt 0 @ /Users/kodyzhou/Downloads/block.m:21:20 str r4, [sp, #12] str.w r12, [sp, #16] str.w r9, [sp, #20] str r3, [sp, #24] str.w lr, [sp, #28] adds r2, #20 str r0, [sp, #4] @ 4-byte Spill mov r0, r2 str r5, [sp] @ 4-byte Spill bl _objc_copyWeak add r0, sp, #12 .loc 1 21 9 discriminator 1 @ /Users/kodyzhou/Downloads/block.m:21:9 bl _objc_retainBlock
可以看到這時沒有objc_retain只執行了objc_copyWeak,所以不加self會導致額外的retain即強持有self
最後的最後看一下block調用的反編譯結果
int ___19-[KDTest testBlock]_block_invoke(int arg0) { var_18 = objc_loadWeakRetained(arg0 + 0x28); rax = var_18 + *_OBJC_IVAR_$_KDTest._testString; objc_storeStrong(rax, @"1"); objc_storeStrong(*(arg0 + 0x20) + *_OBJC_IVAR_$_KDTest._testString, @"2"); [var_18 setTestString:@"3"]; rax = objc_storeStrong(var_18, 0x0); return rax; }
可以看到不同於重寫的C++方法,這裡加不加self會導致不同的賦值方式,不加self的情況會使用block中持有的self來訪問。
至此可以確定在block中重定義了self的情況下_qbosstraceInfo和self->_qbosstraceInfo不等同,前者會導致blcok強持有外部的self。
總結
對於strongify有兩種不同實現,各有優缺點
__strong KDTest *strong_self = weak_self;
第一種是重新定義一個和self命名不同的變數比如strong_self,然後後面都用這個strong_self來操作,這種寫法優點是含義很明確、不會造成誤解,因為只用了strong_self所以很明確不會捕獲外部的self,但缺點是得時刻注意不要錯寫成self__strong KDTest *self = weak_self;
第二種就是空間裡面使用的,重新定義的變數就叫self(其實這裡編譯器也不讓重新定義self的,只是在宏裡面強行掩蓋掉了),優點是發消息的時候不用擔心寫錯了直接用self就行,但缺點是直接訪問成員變數時必須指明self否則會強引用住外部的self,由於很容易誤以為寫不寫self是一樣的,對於不熟悉的人很容易忽視掉這最重要的一點
總而言之要把握weak-strong dance正確的使用姿勢還是需要多多注意,不明白實現的話很容易寫出有問題的代碼,終わり(′-ω-`)
更多精彩內容歡迎關注騰訊 Bugly的微信公眾賬號:
騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合併功能幫助開發同學把每天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在發布後快速的了解應用的質量情況,適配最新的 iOS, Android 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!
推薦閱讀:
※Run Mario!寫一個越獄插件
※iOS有幾種不同的鍵盤布局?
※wp系統真的被安卓ios甩遠了嗎 ?
※iOS 開發需要哪些硬體條件?
※App 裝得多會讓 iPhone 變卡么?