View#post與Handler#post的區別,以及導致的內存泄漏分析

轉載請注明出處:http://blog.csdn.net/a740169405/article/details/69668957

簡述:

寫這篇文章的緣由是最近項目中查內存泄漏時,發現最終原因是由于異步線程調用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:


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:


Log2

我們經常碰到一個問題,就是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

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();

這里的的對象引用關系:


MyThread

這里定義的Thread只是一個臨時對象,并沒有被GC-Root持有,是可以被垃圾回收器回收的,那么我們post出去的Runnable只是不會被執行而已,最后還是會被回收,并不會造成內存泄漏。

但是如果,這個Thread是一個靜態變量的話,那么我們使用異步線程post出去的Runnable也就泄漏了,如果這些runnable又引用了View對象或者是Activity對象,就會造成更大范圍的泄漏。

雖然,Thread被定義成靜態變量的情況很少出現。但是線程池被定義成靜態變量卻常常出現,例如我們應用程序中,經常會定義一些靜態線程池對象用來實現線程的復用,比如下面的這個線程池管理類GlobalThreadPool:

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() {
            }
        });
    }
});

這樣的話,對象引用關系就變成了:


ThreadPool

導出的heap文件hprof查看對象引用關系:


hprof

最后,回到文章開頭簡述中說的,項目中使用異步線程解析布局文件,當解析的布局文件的時候,如果布局文件中包含TextView,這時候,android系統4.4-5.2的機器,就會出現內存泄漏,具體為什么往下看。

  1. TextView的構造方法調用用了setText方法。
  2. setText方法又調用了notifyViewAccessibilityStateChangedIfNeeded方法。
  3. notifyViewAccessibilityStateChangedIfNeeded方法又創建了一個SendViewStateChangedAccessibilityEvent對象,緊接著又調用了SendViewStateChangedAccessibilityEvent對象的runOrPost方法。
  4. runOrPost方法最終又調用了View的post方法。

上面這一大串流程,導致的結果就是異步線程調用了View的post方法,如果這里的線程是核心線程,也就是一直會存在于線程池中的線程,并且線程池又是靜態的,就導致使用異步線程創建多個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

總結:

  1. 當View已經attach到window,不管什么線程, 調用View#post 和 調用Handler#post效果一致
  2. 當View尚未attach到window,主線程調用View#post發送的runnable將在下一次performTraversals到來時執行,而非主線程調用View#post發送的runnable將無法被執行。
  3. 可以通過在主線程調用View#post發送runnable來獲取下一次performTraversals時視圖樹中View的布局信息,如寬高。
  4. 如果調用View#post方法的線程對象被GC-Root引用,則發送的runnable將會造成內存泄漏。

更新(2019年09月17日)

如果需要解決該問題,可以通過反射來置空的方式解決,但是置空代碼需要創建View的子線程執行,這里需要特別注意。

/**
 * 切記此方法需要在創建View的子線程中調用
 */
private void resolveLeak() {
    if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
        // 主線程不需要處理
        return;
    }
    try {
        Class<?> viewRootImpl = Class.forName("android.view.ViewRootImpl");
        Field sRunQueuesField = viewRootImpl.getDeclaredField("sRunQueues");
        if (sRunQueuesField != null) {
            sRunQueuesField.setAccessible(true);
            ThreadLocal threadLocal = (ThreadLocal) sRunQueuesField.get(viewRootImpl);
            if (threadLocal != null) {
                threadLocal.set(null);
            }
        }
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容