View的post方法导致的内存泄漏分析

简述:

写这篇文章的缘由是最近项目中查内存泄漏时,发现最终原因是由于异步线程调用View的的post方法导致的。

为何我会使用异步线程调用View的post方法,是因为项目中需要用到很多复杂的自定义布局,需要提前解析进入内存,防止在主线程解析导致卡顿,具体的实现方法是在Application启动的时候,使用异步线程解析这些布局,等需要使用的时候直接从内存中拿来用。

造成内存泄漏的原因,需要先分析View的post方法执行流程,也就是文章前半部分的内容

文章内容:

  1. View#post方法作用以及实现源码
  2. View#post与Handler#post的区别
  3. 分析View#post方法导致的内存泄漏

post方法分析

看看View的post方法注释:

Causes the Runnable to be added to the message queue. The runnable will be run on the user interface thread

意思是将runnable加入到消息队列中,该runnable将会在用户界面线程中执行,也就是UI线程。这解释,和Handler的作用差不多,然而事实并非如此。

再看看post方法的源码:

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        // 如果当前View加入到了window中,直接调用UI线程的Handler发送消息
        return attachInfo.mHandler.post(action);
    }
    // Assume that post will succeed later
    // View未加入到window,放入ViewRootImpl的RunQueue中
    ViewRootImpl.getRunQueue().post(action);
    return true;
}

分两种情况,当View已经attach到window,直接调用UI线程的Handler发送runnable。如果View还未attach到window,将runnable放入ViewRootImpl的RunQueue中。

那么post到RunQueue里的runnable什么时候执行呢,又是为何当View还没attach到window的时候,需要post到RunQueue中。

View#post与Handler#post的区别

其实,当View已经attach到了window,两者是没有区别的,都是调用UI线程的Handler发送runnable到MessageQueue,最后都是由handler进行消息的分发处理

但是如果View尚未attach到window的话,runnable被放到了ViewRootImpl#RunQueue中,最终也会被处理,但不是通过MessageQueue。

ViewRootImpl#RunQueue源码注释如下:

/**
 * The run queue is used to enqueue pending work from Views when no Handler is
 * attached.  The work is executed during the next call to performTraversals on
 * the thread.
 * @hide
 */

大概意思是当视图树尚未attach到window的时候,整个视图树是没有Handler的(其实自己可以new,这里指的handler是AttachInfo里的),这时候用RunQueue来实现延迟执行runnable任务,并且runnable最终不会被加入到MessageQueue里,也不会被Looper执行,而是等到ViewRootImpl的下一个performTraversals时候,把RunQueue里的所有runnable都拿出来并执行,接着清空RunQueue。

由此可见RunQueue的作用类似于MessageQueue,只不过,这里面的所有

runnable最后的执行时机,是在下一个performTraversals到来的时候,MessageQueue里的消息处理的则是下一次loop到来的时候。RunQueue源码:

static final class RunQueue {
    // 存放所有runnable,HandlerAction是对runnable的包装对象
    private final ArrayList<HandlerAction> mActions = new ArrayList<HandlerAction>();

    // view没有attach到window的时候,View#post最终调用到这
    void post(Runnable action) {
        postDelayed(action, 0);
    }

    // view没有attach到window的时候,View#postDelay最终调用到这
    void postDelayed(Runnable action, long delayMillis) {
        HandlerAction handlerAction = new HandlerAction();
        handlerAction.action = action;
        handlerAction.delay = delayMillis;

        synchronized (mActions) {
            mActions.add(handlerAction);
        }
    }

    // 移除一个runnable任务,
    // view没有attach到window的时候,View#removeCallbacks最终调用到这
    void removeCallbacks(Runnable action) {
        final HandlerAction handlerAction = new HandlerAction();
        handlerAction.action = action;

        synchronized (mActions) {
            final ArrayList<HandlerAction> actions = mActions;

            while (actions.remove(handlerAction)) {
                // Keep going
            }
        }
    }

    // 取出所有的runnable并执行,接着清空RunQueue集合
    void executeActions(Handler handler) {
        synchronized (mActions) {
            final ArrayList<HandlerAction> actions = mActions;
            final int count = actions.size();

            for (int i = 0; i < count; i++) {
                final HandlerAction handlerAction = actions.get(i);
                handler.postDelayed(handlerAction.action, handlerAction.delay);
            }

            actions.clear();
        }
    }

    // 对runnable的封装类,记录runnable以及delay时间
    private static class HandlerAction {
        Runnable action;
        long delay;

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            HandlerAction that = (HandlerAction) o;
            return !(action != null ? !action.equals(that.action) : that.action != null);

        }

        @Override
        public int hashCode() {
            int result = action != null ? action.hashCode() : 0;
            result = 31 * result + (int) (delay ^ (delay >>> 32));
            return result;
        }
    }
}

再看看RunQueue里的消息处理位置,ViewRootImpl#performTraversals:

private void performTraversals() {

    // ....

    // Execute enqueued actions on every traversal in case a detached view enqueued an action
    getRunQueue().executeActions(mAttachInfo.mHandler);

    // ....
}

也就是说,当View没有被attach到window的时候,最后runnable的处理不是通过MessageQueue,而是ViewRootImpl自己在下一个performTraversals到来的时候执行

为了验证RunQueue里的runnable是在下一个performTraversals到来的时候执行的,做一个测试(在Activity的onCreate方法中):

// Activity的跟布局
ViewGroup viewGroup = (ViewGroup) getWindow().getDecorView();
// 自己new的一个View,等待attach到window中
final View view = new View(getApplicationContext()) {
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        // view执行了layout
        Log.e(TAG, "view layout");
    }
};

// 在View未attach到window上之前,
// 使用Handler#post发送一个runnable(最终到了MessageQueue中)
mHandler.post(new Runnable() {
    @Override
    public void run() {
        // 获取View的宽高,查看View是否已经layout
        Log.e(TAG, "MessageQueue runnable, view width = " + view.getWidth() + "  height = " + view.getHeight());
    }
});

// 在View未attach到window上之前,
// 使用View#post发送一个runnable(最终到了ViewRootImpl#RunQueue中)
view.post(new Runnable() {
    @Override
    public void run() {
        // 获取View的宽高,查看View是否已经layout
        Log.e(TAG, "RunQueue runnable, view width = " + view.getWidth() + "  height = " + view.getHeight());
    }
});

// 将view添加到window中
viewGroup.addView(view);

Log:

打印出来的日志说明:

1. 使用handler#post的runnable最先执行,此时View还未layout,无法获取view的宽高。

2. 接着view的onLayout方法执行,表示view完成了位置的布置,此时可以获取宽高。

3. view#post的runnable最后执行,也就是说view已经layout完成才执行,此时能够获取View的宽高。

这里提一下,下一次performTraversals到来的时候,View可能attach到了window上,也可能未attach到window上,也就是代码最后不执行addView动作,使用view#post的runnable仍然无法获取View的宽高,修改如下:

// viewGroup.addView(view);

Log:

我们经常碰到一个问题,就是new一个View之后,通过addView添加到视图树或者是在Activity的onCreate方法里调用setContentView方法。紧接着,我们想获取View的宽高,但是因为view的measure和layout都还未执行,所以是获取不到宽高的。

view#post的一个作用是,在下一个performTraversals到来的时候,也就是view完成layout之后的第一时间获取宽高

View#post方法导致的内存泄漏

分析泄漏之前需要查看ViewRootImpl里的RunQueue成员变量定义以及创建过程:

// 用ThreadLocal对象来保存ViewRootImpl的RunQueue实例
static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();

static RunQueue getRunQueue() {
    RunQueue rq = sRunQueues.get();
    if (rq != null) {
        return rq;
    }
    // 如果当前线程没有创建RunQueue实例,创建并保存在sRunQueues中
    rq = new RunQueue();
    sRunQueues.set(rq);
    return rq;
}

首先这里的ThreadLocal内部持有的实例是线程单利的,也就是不同的线程调用sRunQueues.get()得到的不是同一个对象。

ViewRootImpl使用ThreadLocal来保存RunQueue实例,一般来说,ViewRootImpl#getRunQueue都是在UI线程使用,所以RunQueue实例只有一个。UI线程的对象引用关系:

UIThread是应用程序启动的时候,新建的一个线程,生命周期与应用程序一致,也就是说UI线程对应的RunQueue实例是无法被回收的,但是无所谓,因为每次ViewRootImpl#performTraversals方法被调用时都会把RunQueue里的所有Runnable对象执行并清除。

接着,如果是异步线程调用了View#post方法:

new Thread(new Runnable() {
    @Override
    public void run() {
        new View(getApplicationContext()).post(new Runnable() {
            @Override
            public void run() {
            }
        });
    }
}).start();

这里的的对象引用关系:

这里定义的Thread只是一个临时对象,并没有被GC-Root持有,是可以被垃圾回收期回收的,那么我们post出去的Runnable只是不会被执行而已,最后还是会被回收,并不会造成内存泄漏。

但是如果,这个Thread是一个静态变量的话,那么我们使用异步线程post出去的Runnable也就泄漏了,如果这些runnable又引用了View对象或者是Activity对象,就会造成更大范围的泄漏。

但是,Thread被定义成静态变量的情况很少出现,但是如果线程放在线程池内,并且线程属于核心线程,也就是一直存在,即使线程闲置,我们应用程序中,经常会定义一些线程池用来实现线程的复用:

public class GlobalThreadPool {

    private static final int SIZE = 3;
    private static ScheduledExecutorService mPool;

    public static ScheduledExecutorService getGlobalThreadPoolInstance() {
        if (mPool == null) {
            synchronized (GlobalThreadPool.class) {
                if (mPool == null) {
                    mPool = Executors.newScheduledThreadPool(SIZE);
                }
            }
        }
        return mPool;
    }

    /**
     * run a thead ,== new thread
     */
    public static void startRunInThread(Runnable doSthRunnable) {
        getGlobalThreadPoolInstance().execute(doSthRunnable);
    }
}

接着再把异步处理调用View#post的代码改改:

GlobalThreadPool.startRunInThread(new Runnable() {
    @Override
    public void run() {
        new View(MainActivity.this).post(new Runnable() {
            @Override
            public void run() {
            }
        });
    }
});

这样的话,对象引用关系就变成了:

导出的heap文件hprof查看对象引用关系:

最后,回到文章开头简述中说的,项目中使用异步线程解析布局文件,当解析的布局文件的时候,如果布局文件中包含TextView,这时候,android系统4.4-5.2的机器,就会出现内存泄漏。

因为项目中用到的线程池是静态变量,内部的核心线程在创建TextView的时候,发现TextView的构造方法调用用了setText方法,setText方法又会调用notifyViewAccessibilityStateChangedIfNeeded方法,notifyViewAccessibilityStateChangedIfNeeded方法又创建了一个SendViewStateChangedAccessibilityEvent对象,最后调用了SendViewStateChangedAccessibilityEvent对象的runOrPost方法,runOrPost方法最终又调用了View的post方法。

上面这一大串流程,导致的结果就是异步线程调用了View的post方法,并且Thread是线程池的核心线程,而线程池是静态的。导致使用异步线程创建多个TextView的话,就导致往异步线程的RunQueue中加入多个Runnable,而Runable又引用了View,导致View的泄漏。

泄漏的对象引用关系和上面主动调用View的post方法类似。

至于为什么4.4-5.2的机器才会泄漏,是因为4.4-5.2的系统,View中notifyViewAccessibilityStateChangedIfNeeded方法并没有判断View是否attach到了window,直到google发布的android_6.0系统才修复该问题,该问题可以说是google的问题,因为google官方在Support_v4包中就提供了异步线程加载布局文件的框架,具体参阅:android.support.v4.view.AsyncLayoutInflater

传送门:https://developer.android.com/reference/android/support/v4/view/AsyncLayoutInflater.html

时间: 2024-03-19 02:51:26

View的post方法导致的内存泄漏分析的相关文章

使用HandyJSON导致的内存泄漏问题相关解决方法

在移动开发中,与服务器打交道是不可避免的,从服务器拿到的接口数据最终都会被我们解析成模型,现在比较常见的数据传输格式是json格式,对json格式的解析可以使用原生的解析方式,也可以使用第三方的,我们的项目中使用的是阿里开源的一个swift编写的解析框架--HandyJSON. 在使用过程中,使用instruments的Leak Checks工具对内存泄漏进行检测时发现了这个框架导致了不少的内存泄漏,如图1-1: 这张图是在APP进入首页并将数据加载完毕时截取的,可以看到,HandyJSON一共

static关键字所导致的内存泄漏问题

大家都知道内存泄漏和内存溢出是不一样的,内存泄漏所导致的越来越多的内存得不到回收的失手,最终就有可能导致内存溢出,下面说一下使用staitc属性所导致的内存泄漏的问题. 在dalvik虚拟机中,static变量所指向的内存引用,如果不把它设置为null,GC是永远不会回收这个对象的,所以就有了以下情况: [java] view plain copy public class SecondActivity extends Activity{ private Handler mHandler = n

Java中由substring方法引发的内存泄漏

在Java中我们无须关心内存的释放,JVM提供了内存管理机制,有垃圾回收器帮助回收不需要的对象.但实际中一些不当的使用仍然会导致一系列的内存问题,常见的就是内存泄漏和内存溢出 内存溢出(out of memory ):通俗的说就是内存不够用了,比如在一个无限循环中不断创建一个大的对象,很快就会引发内存溢出. 内存泄漏(leak of memory):是指为一个对象分配内存之后,在对象已经不在使用时未及时的释放,导致一直占据内存单元,使实际可用内存减少,就好像内存泄漏了一样. 由substring

Android内存泄漏分析实战

内存泄漏简单介绍 java能够保证当没有引用指向对象的时候,对象会被垃圾回收器回收.与c语言自己申请的内存自己释放相比,java程序猿轻松了非常多.可是并不代表java程序猿不用操心内存泄漏.当java程序发生内存泄漏的时候往往具有隐蔽性.因此要借助一些专业的平台资源去保证安全性,比如能够通过加密实现. 定义 引用百度百科的定义:"用动态存储分配函数动态开辟的空间,在使用完成后未释放,结果导致一直占领该内存单元. 直到程序结束".从程序员的角度来看"内存泄漏",事实

Android内存泄漏分析及调试

尊重原创作者,转载请注明出处: http://blog.csdn.net/gemmem/article/details/13017999 此文承接我的另一篇文章:Android进程的内存管理分析 首先了解一下dalvik的Garbage Collection: 如上图所示,GC会选择一些它了解还存活的对象作为内存遍历的根节点(GC Roots),比方说thread stack中的变量,JNI中的全局变量,zygote中的对象(class loader加载)等,然后开始对heap进行遍历.到最后,

android 内存泄漏分析技巧

java虚拟机运行一般都有一个内存界限,超过这个界限,就会报outofmemory.这个时候一般都是存在内存泄漏.解决内存泄漏问题,窃以为分为两个步骤:分析应用程序是否真的有内存泄漏,找到内存泄漏的地方.这两个步骤都不是一般意义上的调试,直接打log,断点调试都不是太给力.动脑筋想一想,内存问题应该在很多地方上都会出现,这么常见的问题应该是有工具的.android现在更可以说是一个生态系统,当然也有很多开发辅助工具.在前面的两个步骤中都有很强大的武器,熟练的掌握这些利器,分析问题就会事半功倍.

使用Eclipse Memory Analyzer进行内存泄漏分析三部曲

源地址:http://seanhe.iteye.com/blog/898277 一.准备工作 分析较大的dump文件(根据我自己的经验2G以上的dump文件就需要使用以下介绍的方法,不然mat会出现oom)需要调整虚拟机参数 找个64位的系统在MemoryAnalyzer.ini设置-Xmx2g 如果是32位的xp可以使用下面的方法进行尝试: 安装jrockit 6.0的JDK mat使用jrockit的jdk来启动 Java代码   -vm D:/Program Files/Java/jroc

Java内存泄漏分析与解决方案

Java内存泄漏是每个Java程序员都会遇到的问题,程序在本地运行一切正常,可是布署到远端就会出现内存无限制的增长,最后系统瘫痪,那么如何最快最好的检测程序的稳定性,防止系统崩盘,作者用自已的亲身经历与各位网友分享解决这些问题的办法. 作为Internet最流行的编程语言之一,Java现正非常流行.我们的网络应用程序就主要采用Java语言开发,大体上分为客户端.服务器和数据库三个层次.在进入测试过程中,我们发现有一个程序模块系统内存和CPU资源消耗急剧增加,持续增长到出现java.lang.Ou

Node.js内存泄漏分析

在极客教育出版了一个视频是关于<Node.js 内存泄漏分析>,本文章主要是从内容上介绍如何来处理Node.js内存异常问题.如果希望学习可前往极客学院: 本文章的关键词 - 内存泄漏 - 内存泄漏检测 - GC分析 - memwatch 文章概要 由于内存泄漏在Node.js中非常的常见,可能在浏览器中应用javascript时,对于其内存泄漏不是特别敏感,但作为服务器语言运行时,你就不得不去考虑这些问题.由于很小的逻辑可能导致服务器运行一天或者一个星期甚至一个月才会让你发现内存不断上涨,而