FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案

问题描述

这还得从使用Aspects这个库说起,如下图:
FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案

上图中的id token 的类型如下图:
FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案

AspectIdentifier 的block属性对应的是图一的传参usingBlock,很明显token引用了usingblock,usingblock引用了token,构成循环引用,这个是一个常规的循环引用内存泄漏。可是FBRetainCycleDetector没有扫描出环,一开始我怀疑是我自己改造过的AllocationTracker没有跟踪到AspectIdentifier实例(闲鱼改造了FBAllocationTracker),经排查AllocationTracker是有记录的,可是为什么没有扫描出环呢?

FBRetainCycleDetector的原理简介

1.记录下运行过程中创建和释放的实例
2.分析未释放的实例retain了哪些实例(对应深度优先遍历的临接表),逐层展开,形成一个有向图
3.使用深度优先遍历图的过程中,查看是否有成环

一般的oc对象获取retain的实例通过运行时接口很容易就可以获取到,block就比较特殊,下面介绍一下FBRetainCycleDetector 如何获取block捕获的变量,并确定哪些变量是被block强引用的:
对应的接口 static NSIndexSet _GetBlockStrongLayout(void block)

0x01:转换block对应的ABI 结构,判断是否存在block强引用的变量

FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案

判断block是否强引用其他变量并拷贝至堆上:
FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案
Block引用了 1) C++ 栈上对象 2)OC对象 3) 其他block对象 4) __block修饰的变量,并被拷贝至堆上时则需要copy/dispose辅助函数,辅助函数由编译器生成
除了case 1,其他三种case都会分别调用下面的函数:
void _Block_object_assign(void destAddr, const void object, const int flags);
void _Block_object_dispose(const void *object, const int flags);
_Block_object_assign(或者_Block_object_dispose)会根据flags的值来决定调用相应类型的copy helper(或者dispose helper)

0x02:根据block size确定捕获了多少个变量,创建出一个同样大小的block,并且给捕获的变量指针指向一个伪装的oc对象FBBlockStrongRelationDetector

FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案

0x03:调用block的dispose_helper()释放伪装的block

FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案

0x04:查看dispose_helper()调用了哪几个FBBlockStrongRelationDetector的 release接口

FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案

0x05:根据前面记录的索引下标,记录下真正被block捕获并强引用的实例

FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案

回归问题

现在回归最开始那个问题,从图一可以知道usingblock捕获了3个变量,

0x01看看fb的分析结果:

FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案
只有disposal、guideHandler,没有__block id token
对比一下三者的区别,只有token是__block声明过(这里已经确定token没有被释放,查看AllocationTracker记录是否有token变量即可),说明fb获取block捕获变量的方案对__block无效

0x02对比一下差别:

这是我声明的一个block:
FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案

对其执行clang -rewrite-objc编译转换成C++实现
FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案
FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案
从上图可以确定block捕获的变量就存放在block的ABI struct 的末尾;__block声明的变量要复杂一些,isa、forwarding、flags、size是必有,disposer、copy是obj-c对象才有

来看看block的disposer和copy函数实现:
FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案
分别对捕获的变量做了相应的disposer和copy处理,第二个参数传人的值是不一样的,估计就是这个参数有关联

解决方案

0x01 按原有的方式先把block捕获的非__block声明的strong变量找出来

FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案
看一下红色框:这里直接跳过了block的头部,做了一个小优化,直接从存放变量的索引开始

0x02 除去第一步找到的变量,对剩余的变量在做两次次过滤:

1.是否是malloc指针:不做这步过滤,会导致指针的强转取值crash,因为block可能捕获了一个inter数据,这时如果当成指针来处理,那么基本上会被当作访问非法指针而崩溃
2.去掉__block变量没有disposer和copy 函数的变量,这个使用size大小来判断即可
FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案
FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案

0x03 构造__block变量

FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案
上面每一个构造赋值都是必不可少的

0x04 再一次调用block的disposer函数,对构造的block进行释放操作,获取最后的结果

FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案

  1. diposer() 释放了 struct BlockByref 下的void *refObj对象(即FBBlockStrongRelationDetector对象)
    2.这里注意一下,为什么没有对malloc出来的的__block变量进行free?这里调用free的话,会导致crash,因为block的disposer函数调用到了__block变量自己的disposer接口的时候,已经释放了这段内存,我是通过打开malloc scribble 这个调试开关,发现这段内存有部分被置为了0x55确定的。

查看结果

FBRetainCycleDetector不能扫描__block变量的问题分析和解决方案

从图片可以看出来__block变量已然被解析出来了

git路径

我把修改提交到FBRetainCycleDetector的github上的一个个人分支,等待fb的省核,附上git地址:
https://github.com/chaokongzwp/FBRetainCycleDetector.git