文档章节

关于 Block 中捕获 self 的分析

腾讯Bugly
 腾讯Bugly
发布于 2017/05/08 11:33
字数 2393
阅读 16
收藏 0
点赞 0
评论 0

问题

最近遇到一个已经使用了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的情况下_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正确的使用姿势还是需要多多注意,不明白实现的话很容易写出有问题的代码,終わり(´-ω-`)


更多精彩内容欢迎关注腾讯 Bugly的微信公众账号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!

© 著作权归作者所有

共有 人打赏支持
腾讯Bugly
粉丝 276
博文 134
码字总数 568948
作品 0
深圳
如何优雅的处理循环引用(retain cycle)

什么是循环引用? 顾名思义, 就是几个对象某种方式互相引用, 形成了"环"。由于 Objective-C 内存管理使用引用计数的架构, 而并不是 GC(garbage collector), 而在 ARC(自动引用计数) 下所有 OC...

__block ⋅ 06/13 ⋅ 0

python django事务transaction源码分析

python Django事务 网上关于django1.6的事务资料很多,但是1.8的却搜不到任何资料,自己要用的时候费了不少劲就是不行,现在记下要用的人少走弯路version:Django 1.8事务官方文档事务中文文...

张豪飞 ⋅ 2016/07/26 ⋅ 0

iOS底层原理总结 - 探寻block的本质(一)

面试题 block的原理是怎样的?本质是什么? block的作用是什么?有什么使用注意点? block的属性修饰词为什么是copy?使用block有哪些使用注意? block在修改NSMutableArray,需不需要添加b...

xx_cc ⋅ 05/20 ⋅ 0

【转】PACKET_MMAP的实现

在上一篇文章中,已经提到了在libpcap-1.0.0中已经增加了部分平台的PACKETMMAP支持,就一直想写一篇关于PACKETMMAP实现的文章。 socket的创建和销毁如下,与不使用PACKET_MMAP是一样的: PAC...

weixin_42215796 ⋅ 05/25 ⋅ 0

iOS Block循环引用精讲

前言 本篇文章精讲iOS开发中使用Block时一定要注意内存管理问题,很容易造成循环引用。本篇文章的目标是帮助大家快速掌握使用block的技巧。 我相信大家都觉得使用block给开发带来了多大的便利...

hejunbinlan ⋅ 2016/06/07 ⋅ 0

ARC 下内存泄露的那些点

在网上搜了一下,发现这篇文章是第一篇、也是唯一 一篇总结 ARC 内存泄露的博客,哈哈好兴奋。 在 iOS 4.2 时,苹果推出了 ARC 的内存管理机制。这是一种编译期的内存管理方式,在编译时,编...

hejunbinlan ⋅ 2016/06/07 ⋅ 0

iOS 中的 block 是如何持有对象的

Block 是 Objective-C 中笔者最喜欢的特性,它为 Objective-C 这门语言提供了强大的函数式编程能力,而最近苹果推出的很多新的 API 都已经开始原生的支持 block 语法,可见它在 Objective-C ...

oschina ⋅ 2016/08/15 ⋅ 1

理解 ARC 下的循环引用

ARC 下的循环引用类似于日本的 B 级恐怖片。当你刚成为苹果开发者,你或许不会关心他们的存在。直到某天你的一个 app 因内存泄露而闪退,你才突然意识到他们的存在,并且发现循环引用像幽灵一...

那条鱼 ⋅ 2016/03/30 ⋅ 0

iOS之轻松上手block

导语不会使用block的iOS程序员,不是一个合格的程序员学会了block,你再也不想用繁琐的代理block没有你想象中的那么难,不要害怕,不要畏惧,勇敢尝试笔者入行iOS时已经是ARC的天下,所以这里...

Align ⋅ 2016/01/27 ⋅ 0

iOS 进阶必读

初探 CALayer 属性 一直觉得一个 view 就一个 layer,到今天才发现不是这样子的。 Xcode8调试黑科技:Memory Graph实战解决闭包引用循环问题 Xcode8的调试技能又增加了一个黑科技:Memory Gr...

掘金官方 ⋅ 2017/12/08 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

OSChina 周三乱弹 —— 这样的女人私生活太混乱了

Osc乱弹歌单(2018)请戳(这里) 【今日歌曲】 @ 胖达panda :你经历过体验到人生的大起大落吗?我一朋友在10秒内体验了,哈哈。@小小编辑 请点一首《almost lover》送给他。 《almost love...

小小编辑 ⋅ 31分钟前 ⋅ 5

自己动手写一个单链表

文章有不当之处,欢迎指正,如果喜欢微信阅读,你也可以关注我的微信公众号:好好学java,获取优质学习资源。 一、概述 单向链表(单链表)是链表的一种,其特点是链表的链接方向是单向的,对...

公众号_好好学java ⋅ 37分钟前 ⋅ 0

Centos7重置Mysql 8.0.1 root 密码

问题产生背景: 安装完 最新版的 mysql8.0.1后忘记了密码,向重置root密码;找了网上好多资料都不尽相同,根据自己的问题总结如下: 第一步:修改配置文件免密码登录mysql vim /etc/my.cnf 1...

豆花饭烧土豆 ⋅ 今天 ⋅ 0

熊掌号收录比例对于网站原创数据排名的影响[图]

从去年下半年开始,我在写博客了,因为我觉得业余写写博客也还是很不错的,但是从2017年下半年开始,百度已经推出了原创保护功能和熊掌号平台,为此,我也提交了不少以前的老数据,而这些历史...

原创小博客 ⋅ 今天 ⋅ 0

LVM讲解、磁盘故障小案例

LVM LVM就是动态卷管理,可以将多个硬盘和硬盘分区做成一个逻辑卷,并把这个逻辑卷作为一个整体来统一管理,动态对分区进行扩缩空间大小,安全快捷方便管理。 1.新建分区,更改类型为8e 即L...

蛋黄Yolks ⋅ 今天 ⋅ 0

Hadoop Yarn调度器的选择和使用

一、引言 Yarn在Hadoop的生态系统中担任了资源管理和任务调度的角色。在讨论其构造器之前先简单了解一下Yarn的架构。 上图是Yarn的基本架构,其中ResourceManager是整个架构的核心组件,它负...

p柯西 ⋅ 今天 ⋅ 0

uWSGI + Django @ Ubuntu

创建 Django App Project 创建后, 可以看到路径下有一个wsgi.py的问题 uWSGI运行 直接命令行运行 利用如下命令, 可直接访问 uwsgi --http :8080 --wsgi-file dj/wsgi.py 配置文件 & 运行 [u...

袁祾 ⋅ 今天 ⋅ 0

JVM堆的理解

在JVM中,我们经常提到的就是堆了,堆确实很重要,其实,除了堆之外,还有几个重要的模块,看下图: 大 多数情况下,我们并不需要关心JVM的底层,但是如果了解它的话,对于我们系统调优是非常...

不羁之后 ⋅ 昨天 ⋅ 0

推荐:并发情况下:Java HashMap 形成死循环的原因

在淘宝内网里看到同事发了贴说了一个CPU被100%的线上故障,并且这个事发生了很多次,原因是在Java语言在并发情况下使用HashMap造成Race Condition,从而导致死循环。这个事情我4、5年前也经历...

码代码的小司机 ⋅ 昨天 ⋅ 2

聊聊spring cloud gateway的RetryGatewayFilter

序 本文主要研究一下spring cloud gateway的RetryGatewayFilter GatewayAutoConfiguration spring-cloud-gateway-core-2.0.0.RC2-sources.jar!/org/springframework/cloud/gateway/config/G......

go4it ⋅ 昨天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部