关于 Block 中捕获 self 的分析
栏目: Objective-C · 发布时间: 6年前
内容简介:关于 Block 中捕获 self 的分析
问题
最近遇到一个已经使用了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 *self = weak_self;
第一种 是重新定义一个和self命名不同的变量比如strong_self,然后后面都用这个strong_self来操作,这种写法优点是含义很明确、不会造成误解,因为只用了strong_self所以很明确不会捕获外部的self,但缺点是得时刻注意不要错写成self -
__strong KDTest *strong_self = weak_self;
第二种 就是空间里面使用的,重新定义的变量就叫self(其实这里编译器也不让重新定义self的,只是在宏里面强行掩盖掉了),优点是发消息的时候不用担心写错了直接用self就行,但缺点是直接访问成员变量时必须指明self否则会强引用住外部的self,由于很容易误以为写不写self是一样的,对于不熟悉的人很容易忽视掉这最重要的一点
总而言之要把握weak-strong dance正确的使用姿势还是需要多多注意,不明白实现的话很容易写出有问题的代码,終わり(´-ω-`)
如果您觉得我们的内容还不错,就请转发到朋友圈,和小伙伴一起分享吧~
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 关于 Block 中捕获 self 的分析
- 捕获异常URL--scrapy 源码分析之retry中间件
- kbd-audio:通过麦克风来捕获和分析键盘输入的工具
- 安天蜜网捕获“利用ElasticSearch Groovy漏洞进行门罗币(Dog)挖矿”事件分析
- js捕获错误信息
- Python捕获所有异常
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
剑指Offer:名企面试官精讲典型编程题(第2版)
何海涛 / 电子工业出版社 / 2017-5 / 65.00
《剑指Offer:名企面试官精讲典型编程题(第2版)》剖析了80个典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这5个面试要点。《剑指Offer:名企面试官精讲典型编程题(第2版)》共分7章,主要包括面试的流程,讨论面试每一环节需要注意的问题;面试需要的基础知识,从编程语言、数据结构及算法三方面总结程序员面试知识点;高质量的代码,讨论影响代码质量的3个要素(规范性、完整......一起来看看 《剑指Offer:名企面试官精讲典型编程题(第2版)》 这本书的介绍吧!