检测项目中的循环引用引起的内存问题
说到检测项目中的循环引用 可以有很多手段,其中牛叉的 instruments 当然是把利器。
当然开发过程中往往会大意引起的 循环引用
比如:忘写了 @weakify(self) && @strongify(self); 在大量使用RAC 和 block.....
当然引起这类原因还有很多...
如果分工明确的话可能会再项目结束后,专门测试这块...然而好像并不是每次迭代都会做这块的工作,除非被明确发现引起崩溃的情况。
so 要是能把这个工作引入debug 期间,如果引起循环引用 可以抛出异常给开发人员,及时修复
这方面还是有些大牛写的工具的:如 FLEX
还有 HeapInspector-for-iOS 可以去看看
但有个缺陷时都需要开发人员自己进行分析(),否则还是无法暴露这类问题。
so 还是自己动手吧!
切入点在 UINavController 和 UIViewController ,
app 最多的还是界面展示上,可能会对ViewController 频繁操作,如 push 一个VC、 present 一个VC
最理想的情况下(大部分情况下)你把vc 推出 nav栈 也就意味这系统要回收vc,当然要用到Nav 。
VC 中又会包含很多其它成员,最终直接获取间接的被 VC 所引用。如果VC 或 VC中的成员 遇到强引用循环,那么VC被推出NAV栈后
就不会被释放,如能抛出异常,进行分析的话就很好解决啦。
当然也会有例外的情况:
假如 VC1 有个@p(n,strong)VC2 的成员属性。然后再 VC1中 使用nav 推出 VC2. VC2被推出nav栈并不会释放。
但是 如果VC1也可以被推出Nav栈的话 必定会被释放,如果不是rootVC的话(除非它又被 另一个VC强引用着,这也太奇葩啦。。)
正常情况很好解决,在vc 被推出nav 栈后开一个计时器 2s内如果vc 没有被释放,则抛出一个异常。
这里会有一个情况:
vc 确实被释放啦,当在2s 后才释放,也就时延时啦,(2s 内正常情况下vc 都会释放的)。引起这种原因很多,
vc 被一些"复杂的引用关系" 直接或间接的引用啦,造成vc 释放时等着什么东西释放。
比如使用
vc 中使用[self performSelector:@selector(xxx) withObject:nil afterDelay:10]; 我在进入vc 4s后就推出,
那么vc 会等着这个10s的任务完成后才会被释放。试想我vc 都被推出去拉(看不到啦),在去执行一个任务有多大意思
除非是一些必要的操作,如果是关于ui的操作就更不应该啦。
这种情况当然应该避免,程序中保持简单高效的引用是很有必要的。尤其内存方面。
代码很简单,对于那个例外(vc 被强引用啦),开发人员本意就是要让vc 被强引用,使用了上面逻辑的代码每次都会死在某些地方。
我还忽略不了,必须重启。又死在同一个地方...。估计会被骂死。。
so 。如果可以忽略掉那个异常,是不是就方便多拉。(断言,抛出异常、其它机制都会使程序挂掉)如果能用代码打个断点,岂不妙哉。
最后来看看那个例外。我怎么知道vc 在被推入nav 栈时被强引用啦??
最初相当 retainCount . 大家都知道 这方法不靠谱,完全没有什么意义。
期间我去重写 NSObject 的 release retain 一些内存管理的方法,试图自己记录retainCount.
然而结果并不是想象中那样,一个完全没有被强引用的VC 初始化出来,然后被加入到nav 栈之前,我记录的retainCount 已经是5还是6来着。。
确实困扰了一两天,有时候长时间想不出的问题,可能已经在牛角尖啦0.0
如何变通,如果能确定vc 是某一个VC的成员是不是就可以解决啦。
so 变成 给定实例 A 和 B 如何确定实例B 是 实例A的成员。
可以根据A 去Class 里找ivar 列表。 然后对比每一个ivar 和 B 对比,如果一样那么就能确定啦,而且还可以获取实例B 的变量名
像这样:
#import <objc/runtime.h> static id GetIvarName(id holdInstance,id ivarInstance){ if (!holdInstance||!ivarInstance) { return nil; } NSString *ivarName = nil; uint32_t ivarCount; Ivar *ivars = class_copyIvarList([holdInstance class], &ivarCount); if(ivars) { for(uint32_t i=0; i<ivarCount; i++) { Ivar ivar = ivars[i]; const char*ivarType = ivar_getTypeEncoding(ivar); NSString *ivarTypeStr = [NSString stringWithCString:ivarType encoding:NSUTF8StringEncoding]; //成员变量不是object 调用object_getIvar 会crash if (![ivarTypeStr hasPrefix:@"@"]) { continue; } id pointer = object_getIvar(holdInstance, ivar); if(pointer == ivarInstance) { ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)]; break; } } free(ivars); } return ivarName; }
上面例外情况基本解决:
在hook 的 push 是 可以确定
if ([self.viewControllers count]>0) { UIViewController *lastVC = [self.viewControllers lastObject]; NSString *ivarName = GetIvarName(lastVC, viewController); viewController.isStrongRef = ivarName?YES:NO; if (viewController.isStrongRef) { DDLogWarn(@"****[%@]**被[%@]的成员变量[%@]强引用**,在%@ pop后将不会被释放,如有必要,请忽略该条信息.",[viewController class],[lastVC class],ivarName,[viewController class]); } }
和 hook 的present中
NSString *ivarName = GetIvarName(self, viewControllerToPresent); viewControllerToPresent.isStrongRef = ivarName?YES:NO; if (viewControllerToPresent.isStrongRef) { DDLogWarn(@"****[%@]**被[%@]的成员变量[%@]强引用**,在%@ dismiss后将不会被释放,如有必要,请忽略该条信息.",[viewControllerToPresent class],[self class],ivarName,[viewControllerToPresent class]); }
以上仅可用在debug 。。
不过遗憾的是,只能检测nav uiviewController 相关的循环引用。应该可以覆盖大部分情况啦。
代码在这里: git clone git@github.com:githhhh/ProbeRefCycles.git
好啦,小试牛刀后下篇 来试试 Instrument 这把利器 检测循环引用....