iOS内存泄漏自动检测工具PLeakSniffer

ffbn3269 8年前
   <p>新款objective-C内存泄漏自动检测工具<strong>PLeakSniffer</strong>,<a href="/misc/goto?guid=4959675785717387015">GitHub地址</a>。</p>    <h2><strong>背景</strong></h2>    <p>前些天读到WeRead团队分享的一款内存泄漏检测工具<a href="/misc/goto?guid=4958987581074905592">MLeaksFinder</a>,恍惚想起早些时候自己也有过编写这样一个小工具的想法,不知道由于什么原因把这事给忘记了。在仔细读过MLeaksFinder源码,了解实现思路之后,发现和自己最初的想法并不相同,终于在上个周末战胜拖延症将之前的想法付诸于代码,也就诞生了这款功能类似的内存泄漏检测工具PLeakSniffer。建议读者先详细阅读下<a href="/misc/goto?guid=4958987581074905592">MLeaksFinder</a>这篇博客。</p>    <h2><strong>为什么要再造轮子</strong></h2>    <p>我在公司的项目里实际试用了MLeaksFinder,还查处了2处泄漏。根据MLeaksFinder代码文件中日期推测,这个项目至少已开始半年有余,并在微信读书上得到了实践验证,在功能性和稳定性上都应该有不错的表现。</p>    <p>在编写完PLeakSniffer之后,查出了与MLeaksFinder相同的内存泄漏,思路迥异的代码抵达了相同的终点,写代码的乐趣莫过于此。新的思路或许还能抛砖引玉,如果激发更多的创意,也算是对iOS开发社区的一点小贡献。</p>    <p>MLeaksFinder现阶段能查处UIViewController和UIView的泄漏,我早先的想法还能递归的查出UIViewController之下所有Property的泄漏,并在PLeakSniffer及公司项目中得到了初步的验证,这算是对MLeaksFinder功能的一个小补充。</p>    <h2><strong>这类工具的意义</strong></h2>    <p>在我们讨论这类工具的意义之前,我们先得明确一点:</p>    <blockquote>     <p>如果不使用Instrument当中的Leak检测工具,并没有什么轻易的100%精准的内存泄漏检测方式。</p>    </blockquote>    <p>但这类工具还是有其存在价值的,内存泄漏的危害不用赘述,如果有一款工具能在80%的场景下检测出可能的内存泄漏,而且这种检测并不会带来任何副作用(不影响生产环境代码),为什么不使用它呢。</p>    <p>大部分人都低估了他们写代码时导致意外内存泄漏的可能性。Retain Cycle,Block强引用,NSTimer释放不当,这些常见的错误还是很容易出现在我们的代码里,Instrument每使用一次要费些精力,适合做定期的大排查。平常时候就更适合用MLeaksFinder,PLeakSniffer这类工具来做实时监控,提供免费建议。</p>    <h2><strong>PLeakSniffer实现思路</strong></h2>    <p>我们绝大部分时候都是在编写UIViewController,UIViewController就像一个根节点,持有并管理着很多的子节点对象,这些子节点的生命周期都依赖于Controller,Controller释放的时候,他们也随之释放。用一张图简单的描述他们的关系:</p>    <p><img alt="iOS内存泄漏自动检测工具PLeakSniffer" src="https://simg.open-open.com/show/4fd7e552b3af861c03e00e7052b90636.png"></p>    <p>根据各个应用使用的设计模式不同(MVC,MVP,MVVM等),Controller所持有的Property也不相同。这里我们使用MVP作为例子,Controller所包含的对象就包括各种View对象,和Presenter,Model对象。当然每个对象又有可能持有更多的子对象。</p>    <p>PLeakSniffer基于这样一个假设: > 如果Controller被释放了,但其曾经持有过的子对象如果还存在,那么这些子对象就是泄漏的可疑目标。</p>    <p>当然这个假设并不是一个100%适用的真理,不同工程师编写代码的方式风格差别很大,有些会把某些UIViewController做成单例(个人觉得这不是个好主意。。),有些会把某些View缓存起来(即使Controller已被释放),还会有其他考虑不到的场景。但在80%以上的场景,我们在Controller结束生命周期之后会将其持有的资源一并释放。这时候PLeakSniffer可以发挥用处,给你一些免费的泄漏建议。</p>    <p><strong>那么怎么在Controller被释放之后,知道其持有的对象没有被释放呢?</strong></p>    <p>一个小技巧可以达成这个目标:子对象(比如view)建立一个对controller的weak引用,如果Controller被释放,这个weak引用也随之置为nil。那怎么知道子对象没有被释放呢?用一个单例对象每个一小段时间发出一个ping通知去ping这个子对象,如果子对象还活着就会一个pong通知。所以结论就是:如果子对象的controller已不存在,但还能响应这个ping通知,那么这个对象就是可疑的泄漏对象。完整的结构可以用下图表示:</p>    <p><img alt="iOS内存泄漏自动检测工具PLeakSniffer" src="https://simg.open-open.com/show/5f5fcfd86f3435d6517cdd3f29ed87fc.png"></p>    <p>通知移除需要一个时机,这里我们使用Associated Object机制给每一个子对象再生成一个Proxy对象,在Proxy对象的dealloc里面移除通知。</p>    <p>当然什么时候去判断一个对象的生命周期开始,什么时候判断为结束,需要一个精挑细选的机制。View,Controller,Property各不相同。</p>    <p>PLeakSniffer采取保守的策略,通过Objective C的runtime机制,递归的将一个Controller所有强引用的property找出,并安装proxy监听Ping通知。在我的测试下,基本上能将property泄漏的场景找出。</p>    <p>PLeakSniffer的使用方式很简答,通过Pod安装后,通过以下代码激活即可。</p>    <pre>  <code class="language-objectivec">#if MY_DEBUG_ENV  [[PLeakSniffer sharedInstance] installLeakSniffer];  [[PLeakSniffer sharedInstance] addIgnoreList:@[@"MySingletonController"]];  #endif  </code></pre>    <p>addIgnoreList可以添加一些特殊的忽略名单,比如单例这种无法正确预测泄漏的对象。切记用Debug的宏将上述代码包住,不要把这些检测泄漏的代码带进线上环境。</p>    <p>如果检测到可疑泄漏,PLeakSniffer会在控制台打印一条日志:</p>    <p><strong>Controller泄漏:Detect Possible Controller Leak: %@</strong></p>    <p><strong>其他对象泄漏:Detect Possible Leak: %@</strong></p>    <p> </p>    <p>来自:http://mrpeak.cn/blog/leak/</p>