美文网首页
记一次查找Android内存泄漏的过程

记一次查找Android内存泄漏的过程

作者: 男的孩 | 来源:发表于2019-10-21 11:57 被阅读0次

    今天无意间发现公司的App存在内存泄漏,商品详情页面无法正常回收。起初以为是WebView导致的,可是在上道具查看之后,发现其实并不是。

    用Profiler将内存dump并export出来,扔到MAT中,结果如下图所示:

    红线标记即未被回收的Activity

    从图中可以看到,一共4个详情页,被别处用链表串了起来,整整齐齐,一个不落。不过这样有个好处,只用分析这一处就可以将此次泄漏问题一锅端。

    从图中可以发现,引用详情页的地方是在ActivityThread中的mNewActivities字段中。该字段是ActivityClientRecord类型的:

    ActivityClientRecord

    我们只需要关心两个字段:activitynextIdle,正是这两个字段将详情页串起来的。

    通过在ActivityThread中搜索mNewActivities,发现一共有两处操作了该字段。第一处是在方法handleResumeActivity中进行了赋值操作,另一处是在内部类Idler中对它赋值为null。

    简单说下ActivityClientRecordActivityRecord是ActivityManagerServer中用来标识Activity的类,包含了一个Activity的所有信息。而ActivityClientRecord代表了应用进程中的一个Activity。

    来看handleResumeActivity方法的代码:

    final void handleResumeActivity(IBinder token,
                boolean clearHide, boolean isForward, boolean reallyResume) {
            // If we are getting ready to gc after going to the background, well
            // we are back active so skip it.
            ...
            // 我们主要看操作mNewActivities字段的代码
            if (!r.onlyLocalRequest) {
                    r.nextIdle = mNewActivities;
                    mNewActivities = r;
                    if (localLOGV) Slog.v(
                        TAG, "Scheduling idle handler for " + r);
                    Looper.myQueue().addIdleHandler(new Idler());
                }
                r.onlyLocalRequest = false;
            ...
      }
    

    从代码中可以看出来,新创建的ActivityClientRecord都会插入到链表的头部,而mNewActivities正好是该链表的头。而接下来的Looper.myQueue().addIdleHandler(new Idler());正是前面说的第二处操作mNewActivities的地方,这行代码的意思是将一个Idler对象放入到消息队列中,也就是将Idler对象添加到MessageQueue中的mIdleHandlers字段中,如下所示:

    public void addIdleHandler(@NonNull IdleHandler handler) {
            if (handler == null) {
                throw new NullPointerException("Can't add a null IdleHandler");
            }
            synchronized (this) {
                mIdleHandlers.add(handler);
            }
        }
    

    那么,它是什么时候执行的呢?从MessageQueuenext方法我们可以知道:当处于空闲状态时,会执行IdleHandler中的方法。

    从这里猜测应该是发送到消息队列中的Idler对象没有被执行,在商品详情页中添加如下代码进行测试。

    Looper.getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
                @Override
                public boolean queueIdle() {
                        Log.e("Idler >>>", "queueIdle()");
                        return false;
                 }
       });
    

    经测试,发现确实没有打印出相关日志,说明消息队列中一直有消息存在。将消息队列中处理的消息打印出来。同样,在详情页添加如下代码:

    Looper.getMainLooper().setMessageLogging(new Printer() {
            @Override
            public void println(String x) {
                Log.e("message >>>", x);
            }
        });
    

    发现大量的如下日志:

    Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {a0266dd} null: 1
    Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {a0266dd} null
    ......
    

    从中可以知道,ViewRootHandler处理了大量ID为1的消息。这是什么消息呢?这其实是MSG_INVALIDATE消息,用来刷新界面的。

    局面已经很明了了:详情页一直在刷新界面,导致IdleHandler无法执行,从而使mNewActivities字段一直持有Activity的引用,最终造成了内存泄漏。

    经过仔细排查,发现是详情页面引用了一个用来显示购物车数量的自定义View,在该自定义View的onDraw方法中,将canvas传递给外部的一个操作Helper类,在Helper类的绘画操作结束之后,调用了postInvalidate()去刷新界面。这就导致了不停的onDraw死循环。

    解决方法就很简单了,删掉相关的postInvalidate调用。

    经检验,引用到该控件的页面均能正常回收。

    好了,结束。

    相关文章

      网友评论

          本文标题:记一次查找Android内存泄漏的过程

          本文链接:https://www.haomeiwen.com/subject/kmpamctx.html