日常干bug系列之“mNewActivities Memory leak”

2,570 阅读3分钟

最近在跑阿里云兼容性测试的时候,发现在部分机器上,内存普遍偏高,查看日志确定有内存泄漏的情况,开始用LeakCanary并且定位到了有问题的GC Root:

image.png 是framework的泄漏,按照常理判断应为是app层非法调用导致,解决的思路是:

  1. 找出持有者.
  2. 找出持有者的理论释放点.
  3. 找出理论释放点未执行的路径. 根据这个思路进行排查:
  4. 很显然持有者是ActivityThread里mNewActivities属性,是个ActivityClientRecord类型的对象,数据结构是链表.
  5. 这一步才是比例费力的地方,需要翻阅framework层的源代码,消耗了一些时间之后,发现了理论释放点,而且这个执行点应该会在ActivityThread对象在执行handleResumeActivity函数的时候发起调用的.

image.png 3. 再联想到ActivityClientRecord的数据结构,可以推断出正常的执行路径,假设有2个activity,a1和a2,a1在a2的下边,这个时候mNewActivities的链式结构应该是a1->a2,现在a2执行退出操作,ActivityManagerServer会调度a1和a2的生命周期,会先调用handleResumeActivity执行a1的OnResume,同时会把mNewActivities末端的a2移除(就是Idler里执行的逻辑).那么这是正常的逻辑,很显然这个流程没有走通,那么是在哪里出了问题呢, 在回到Idler执行的地方,

image.png 一种猜测是主线程的Looper执行的任务比较多,一直没有时间执行addIdleHandler里的任务,那么我就看看Looper现在忙不忙,在a1的onCreate里添加代码

image.png 果然海量的任务

image.png 接下来就是查找是哪里频繁的发生任务,由于没有好的办法定位是哪里通过Choreographer里的FrameHandler发生任务的,我只能通过注释掉代码来定位,最终是CollapsingToolbarLayout构造函数里的这个部分代码导致的,

image.png 这个代码是注册了一个windowInset事件,windowInset事件在其它文章里会介绍,这里我们只关注这个事件的注册方式,注册是通过ViewCompat注册的,在看一下ViewCompat是怎么注册的,

image.png

image.png 显然ViewCompat在注册的时候,做了一层代理,在这个代理里有个逻辑就是,如果4772行的判断没有命中则会调用4789函数,而4789的代码会一路调用到ViewRootImp的requestFitSystemWindows函数

image.png

而requestFitSystemWindows调用scheduleTraversals函数,

image.png

而scheduleTraversals函数里会向Choreographer发布任务,执行任务就是Choreographer里的FrameHandler,FrameHandler里会让ViewRootImp再次执行performTraversals,performTraversals里会调用dispatchApplyInsets

image.png 执行dispatchApplyInsets这个函数会重新派发windowInset事件,派发之后会再次执行到ViewCompat里的注册函数,如果4772行的if还没有命中,那么就会再次循环一下,如果一直没有命中,就会一直循环下去,looper也就会一直处理消息,经过调试发现就是这个一直在循环调用.

最后总结一下,CollapsingToolbarLayout在构造函数里,注册了OnApplyWindowInsetsListener事件,在注册的代理里一直会经过一些列的调用向Choreographer里的FrameHandler发生异步消息,导致主线的looper繁忙,无法处理ActivityThread里的mNewActivities指空末端节点的逻辑,导致内存泄漏,解决的方案是重写CollapsingToolbarLayout的构造函数,通过把OnApplyWindowInsetsListener置空,阻断循环发送消息(我这里不依赖OnApplyWindowInsetsListener事件处理自身和子控件的布局,所以置空,不会引发其它的问题).