关于 Block 中捕获 self 的分析

栏目: Objective-C · 发布时间: 7年前

内容简介:关于 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指针的地方

关于 Block 中捕获 self 的分析

   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的情况下 _qbosstraceInfoself->_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 的分析


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

重新定义公司

重新定义公司

[美]埃里克·施密特 / 靳婷婷、陈序、何晔 / 中信出版社 / 2015-8 / 49.00

谷歌高管手绘风漫画视频: http://v.youku.com/v_show/id_XMTMxMzQ3NjMyMA==.html?from=y1.7-1.2 Google掌门人第一本国内引进作品 首次公开谷歌内部的管理与运营方法 全面解密执掌谷歌10余年的内幕故事 谷歌 创始人拉里•佩奇作序推荐 今日的谷歌是全球最具标志性的企业,在各个领域都有创新突破,并向技术......一起来看看 《重新定义公司》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具