1. 程式人生 > >View#post與Handler#post的區別,以及導致的記憶體洩漏分析

View#post與Handler#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:
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將會造成記憶體洩漏。

相關推薦

View#postHandler#post區別以及導致記憶體洩漏分析

簡述: 寫這篇文章的緣由是最近專案中查記憶體洩漏時,發現最終原因是由於非同步執行緒呼叫View的的post方法導致的。 為何我會使用非同步執行緒呼叫View的post方法,是因為專案中需要用到很多複雜的自定義佈局,需要提前解析進入記憶體,防止在

android-View.postHandler.post區別

View.postDelayed package android.view; public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility

Android7.0 View.postHandler.post

在獲取view寬高時,在Android6.0中使用handler.post()可以正常獲取,而執行在Android7.0上則無法再獲取。而在7.0上改為view.post()方法則又可以正常獲取view寬高。 檢視原始碼和相關資料後知道是因為,雖然這兩個都是post(new runnab

Android7.0 View.postHandler.post

在獲取view寬高時,在Android6.0中使用handler.post()可以正常獲取,而執行在Android7.0上則無法再獲取。而在7.0上改為view.post()方法則又可以正常獲取view寬高。 檢視原始碼和相關資料後知道是因為,雖然這兩個都是p

Go 學習筆記:Println Printf 的區別以及 Printf 的詳細用法

Println 與Printf 都是fmt 包中的公共方法,在需要列印資訊時需要用到這二個函式,那麼這二個函式有什麼區別呢? Println :可以打印出字串,和變數 Printf : 只可以打印出格式化的字串,可以輸出字串型別的變數,不可以輸出整形變數和整

fragment中onCreateViewonActivityCreated的區別以及fragment中生命週期的利用

最近使用了一個自定義的view在activity中執行正常,可在fragment中就奔潰,無提示,之前view是在onCreateView中初始化並呼叫的,崩潰,換到onActivityCreated之後,執行ok了,這是什麼原因呢?? 先看看fragment的生命週期,首

jQueryJS的區別以及jQuery的基礎語法

*在使用jQuery時,要在頁面最上端加上<script src="../jquery-1.11.2.min.js"></script>看一下js與jQuery的區別:JS是這樣使用的:<script type="text/javascript"

centos7 原始碼包RPM包區別以及原始碼包安裝過程

原始碼包與RPM包的區別 1、概念上的區別 軟體包分類 原始碼包 RPM包 包的形式 C原始檔包 編譯之後的二進位制包 優點 開源;可以自由選擇所需功能;可看原始碼;解除安裝方便(直接刪除安裝位置); 使用

newmalloc的區別以及記憶體分配淺析

二、malloc()到底從哪裡得來了記憶體空間: 1、malloc()到底從哪裡得到了記憶體空間?答案是從堆裡面獲得空間。也就是說函式返回的指標是指向堆裡面的一塊記憶體。作業系統中有一個記錄空閒記憶體地址的連結串列。當作業系統收到程式的申請時,就會遍歷該連結串列,然後就尋找第一個空間大於所申請空間的堆結點,

指標陣列的區別以及指標的空間開闢問題

#include <iostream> using namespace std; int main() { char* p = "wanglibao"; char* a = new char[10]; // p[0] = 'e';

JDKJRE的區別以及安裝目錄的關係

簡單的說JDK是面向開發人員使用的SDK,它提供了Java的開發環境和執行環境。SDK是Software Development Kit 一般指軟體開發包,可以包括函式庫、編譯程式等。JDK就是Java Development KitJRE是Java Runtime Env

floatdouble的區別以及float為什麼要加f

單精度浮點數(float)與雙精度浮點數(double)的區別如下:(1)在記憶體中佔有的位元組數不同單精度浮點數(float)在機內佔4個位元組雙精度浮點數(double)在機內佔8個位元組(2)有效數字位數不同單精度浮點數(float)有效數字8位雙精度浮點數(doubl

Handler原始碼詳解及導致記憶體洩漏分析

[TOC] 簡介 android的訊息處理有三個核心類:Looper,Handler和Message, 主要接受子執行緒傳送的資料, 並用此資料配合主執行緒更新UI。 部分圖片來至CodingMyWorld部落格,3Q 使用方法 pu

動態規劃(dynamic programming)(二、最優子問題重疊子問題以及貪心的區別

貪心策略 找到 算法 找問題 貪心 模式 解決 策略 最優 一、動態規劃基礎   雖然我們在(一)中討論過動態規劃的裝配線問題,但是究竟什麽時候使用動態規劃?那麽我們就要清楚動態規劃方法的最優化問題中的兩個要素:最優子結構和重疊子問題。   1、最優子結構     1)如果

word-wrapword-break的區別以及無效情況

OS 自動 class word-wrap 就是 con 整體 tro ace 兩種方法的區別說明: 1,word-break:break-all 例如div寬400px,它的內容就會到400px自動換行,如果該行末端有個英文單詞很長(congratulation等),它會

JavaWEB HTTP請求中POSTGET的區別

get 和post方法.在資料傳輸過程中分別對應了HTTP協議中的GET方法和POST方法. 主要區別: GET從服務其獲取資料;POST上傳資料. GET將表單中的資料按照variable=value的形式,新增到action所指向的URL後面.並且兩者使用了"?"連線,個個變

post put的區別

這兩個方法咋一看都可以更新資源,但是有本質區別的 具體定義可以百度,我這裡就不貼了,光說我自己的理解 首先解釋冪等,冪等是數學的一個用語,對於單個輸入或者無輸入的運算方法,如果每次都是同樣的結果,則稱其是冪等的 對於兩個引數,如果傳入值相等,結果也等於每個傳入值,則稱其為冪等的,如min

我所理解的postget請求區別

Get和Post一般的區別:  1.post更安全(不會作為url的一部分,不會被快取、儲存在伺服器日誌、以及瀏覽器瀏覽記錄中)  2.post傳送的資料更大(get有url長度限制)   get傳參最大長度的理解誤區  1)總結  (1)http協議並未規定get和pos

abstract class 抽象類interface 介面的區別以及應用

抽象類 特點 擁有抽象方法的類必須是抽象類 抽象類可以沒有抽象方法 繼承了抽象類的子類必須實現抽象方法,如果不實現抽象方法那麼子類必須是抽象類 抽象類中可以對方法進行宣告也可以對方法進行實現 抽象方法不能宣告為static 抽象方法不能宣告為private

字串處理中sizeofstrlen區別以及末尾的\0

char *ch = "wonima aisaoziaaa"; int n = sizeof(ch); // 指標長度,對於64平臺來說,值為8 int nn = sizeof(*ch); // 一個字元的長度,值為1 int nnn = strlen(ch); //