Android Handler機制11之Handler機制總結

Android Handler機制系列文章整體內容如下:

本片文章的主要內容如下:

  • 1、Handler機制的思考
  • 2、Handler消息機制
  • 3、享元模式
  • 4、HandlerThread
  • 5、Handler的內存泄露
  • 6、Handler的面試題

一、Handler機制的思考

  • 先提一個問題哈,如果讓你設計一個操作系統,你會怎么設計?

我們一般操作系統都會有一個消息系統,里面有個死循環,不斷的輪訓處理其他各種輸入設備輸入的信息,比如你在在鍵盤的輸入,鼠標的移動等。這些輸入信息最終都會進入你的操作系統,然后由操作系統的內部輪詢機制挨個處理這些信息。

  • 那Android系統那?它內部是怎么實現的? 如果讓你設計,你會怎么設計?

答:如果讓我設計,肯定和上面一樣:

  • 1 設計一個類,里面有一個死循環去做循環操作;
  • 2 用一個類來抽象代表各種輸入信息/消息;這個信息/消息應該還有一個唯一標識符字段;如果這個信息里面有個對象來保存對應的鍵值對;方便其他人往這個信息/消息 存放信息;這個信息/消息應該有個字段標明消息產生的時間;
  • 3 而上面的這些 信息/消息 又組成一個集合。常用集合很多,那是用ArrayList好還是LinkedList或者Map好那?因為前面說了是一個死循環去處理,所以這個集合最好是"線性和排序的"比較好,因為輸入有先后,一般都是按照輸入的時間先后來構成。既然這樣就排除了Map,那么就剩下來了ArrayList和LinkedList。我們知道一個操作系統的事件是很多的,也就是說對應的信息/消息很多,所以這個集合肯定會面臨大量的"插入"操作,而在"插入"效能這塊,LinkedList有著明顯的優勢,所以這個集合應該是一個鏈表,但是鏈表又可以分為很多種,因為是線性排序的,所以只剩下"雙向鏈表"和"單向鏈表”,但是由于考慮下手機的性能問題,大部分人肯定會傾向于選擇"單向鏈表",因為"單項鏈表"在增加和刪除上面的復雜度明顯低于"雙向鏈表"。
  • 4、最后還應該有兩個類,一個負責生產這個輸入信息,一個負責消費這些信息。因為涉及到消費端,所以上面2中說的信息/消息應該有一個字段負責指向消費端。

經過上面的思考,大家是不是發現和其實我們Handler的機制基本上一致。Looper負責輪詢;Message代表消息,為了區別對待,用what來做為標識符,when表示時間,data負責存放鍵值對;MessageQueue則代表Message的集合,Message內部同時也是單項鏈表的。通過上面的分析,希望大家對Handler機制的總體設計有不一樣的感悟。

二、Handler消息機制

如果你想要讓一個Android的應用程序反應靈敏,那么你必須防止它的UI線程被阻塞。同樣地,將這些阻塞的或者計算密集型的任務轉到工作線程去執行也會提高程序的響應靈敏性。然而,這些任務的執行結果通常需要重新更新UI組件的顯示,但該操作只能在UI線程中去執行。有一些方法解決了UI線程的阻塞問題,例如阻塞對象,共享內存以及管道技術。Android為了解決這個問題,提供了一種自有的消息傳遞機制——Handler。Handler是Android Framework架構中的一個基礎組件,它實現了一種非阻塞的消息傳遞機制,在消息轉換的過程中,消息的生產者和消費者都不會阻塞。

Handler由以下部分組成:

  • Handler
  • Message
  • MessageQueue
  • Looper

下面我們來了解下它們及它們之間的交互。

(一)、Handler

Handler 是線程間傳遞消息的即時接口,生產線程和消費線程用以下操作來使用Handler

  • 生產線程:在消息隊列中創建、插入或移除消息
  • 消費線程:處理消息
Handler.png

每個Handler都有一個與之關聯的Looper和消息隊列。有兩種創建Handler方式(這里不是說只有兩個構造函數,而是說把它的構造函數分為兩類)

  • 通過默認的構造方法,使用當前線程中關聯的Looper
  • 顯式地指定使用Looper

如果沒有指定Looper的Handler是無法工作的,因為它無法將消息放到消息隊列中。同樣地,它無法獲取要處理的消息。

    public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

如果是使用上面Handler的構造函數,它會檢查當前線程有沒有可用的Looper對象,如果沒有,它會拋出一個運行時的異常,如果正常的話,Handler會持有Looper中的消息隊列對象的引用。

PS:同一個線程中多的Handler分享一個同樣的消息隊列,因為他們分享的是同一個Looper對象

Callback參數是一個可選的參數,如果提供的話,它將會處理由Looper分發的過來的消息。

(二)、Message

Message 是容納任意數據的容器。生產線程發送消息給Handler,Handler將消息加入到消息隊列中。消息提供了三種額外的信息,以供Handler和消息隊列處理時使用:

  • what:一種標識符,Handler能使用它來區分不同的消息,從而采取不同的處理方法
  • time:告訴消息隊列合適處理消息
  • target:表示那一個Handler應該處理消息

android.os.Message 消息一般是通過Handler中以下方法來創建的

public final Message obtainMessage()
public final Message obtainMessage(int what)
public final Message obtainMessage(int what, Object obj)
public final Message obtainMessage(int what, int arg1, int arg2)
public final Message obtainMessage(int what, int arg1, int arg2, Object obj)

消息從消息池中獲取得到,方法中提供的參數會放到消息體對應的字段中。Handler同樣可以設置消息的目標為其自身,這允許我們進行鏈式調用,比如:

mHandler.obtainMessage(MSG_SHOW_IMAGE, mBitmap).sendToTarget();

消息池是一個消息對象的單項鏈表集合,它的最大長度是50。在Handler處理完這條消息之后,消息隊列把這個對象返回到消息池中,并且重置其所有字段。

當使用Handler調用post方法來執行一個Runnable時,Handler隱式地創建了一個新的消息,并且設置callback參數來存儲這個Runnable。

Message m = Message.obtain();
m.callback = r;
Handler與Message.png

生產線程發送消息給 Handler 的交互

在上圖中,我們能看到生產線程和 Handler 的交互。生產者創建了一個消息,并且發送給Handler,隨后Handler 將這個消息加入消息隊列中,在未來某個時間,Handler 會在消費小城中處理這個消息。

(三)、MessageQueue

MessageQueue是一個消息體對象的無界的單向鏈表集合,它按照時序將消息插入隊列,最小的時間戳將會被首先處理。

MessageQueue.png

消息隊列也通過SystemClock.uptimeMillis獲取當前時間,維護一個阻塞閥值(dispatch barrier)。當一個消息體的時間戳低于這個值的時候,消息就會分發給Handler進行處理

Handler 提供了三種方式來發送消息:

public final boolean sendMessageDelayed(Message msg, long delayMillis)
public final boolean sendMessageAtFrontOfQueue(Message msg)
public boolean sendMessageAtTime(Message msg, long uptimeMillis)

以延遲的方式發送消息,是設置了消息體的time字段為SystemClock.uptimeMillis()+delayMillis。然而,通過sendMessageAtFontOfQueue方法是把消息插入到隊首,會將其時間字段設置為0,消息會在下一次輪訓時被處理。需要謹慎使用這個方法,因為它可能會英系那個消息隊列,造成順序問題,或是其他不可預料的副作用。

(四)、消息隊列、Handler、生產線程的交互

現在我們可以概括消息隊列、Handler、生產線程的交互:

消息隊列、Handler、生產線程的交互.png

上圖中,多個生產線程提交消息到不同的Handler中,然而,不同的Handler都與同一個Looper對象關聯,因此所有的消息都加入到同一個消息隊列中。這一點非常重要,Android中創建的許多不同的Handler都關聯到主線程的Looper。

比如:

  • The Choreographer:處理垂直同步與幀更新
  • The ViewRoot:處理輸入和窗口時間,配置修改等等
  • The InputMethodManager不會大量生成消息,因為這可能會抑制處理系統

(五)、Looper

Looper 從消息隊列中讀取消息,然后分發給對應的Handler處理。一旦消息超過阻塞閥,那么Looper就會在下一輪讀取過程中讀取到它。Looper在沒有消息分發的時候變成阻塞狀態,當有消息可用時會繼續輪詢。

每個線程只能關聯一個Looper,給線程附加的另外的Looper會導致運行時的異常。通過使用Looper的Threadlocal對象可以保證線程只關聯一個Looper對象。

調用Looper.quit()方法會立即終止Looper,并且丟棄消息隊列中的已經通過阻塞閥的所有消息。調用Looper.quitSafely()方法能夠保證所有待分發的消息在隊列中等待的消息被丟棄前得到處理。

Looper.png

Handler 與消息隊列和Looper 直接交互的整體流程
Looper 應在線程的run方法中初始化。調用靜態方法Looper.prepare()會檢查線程是否與一個已存在的Looper關聯。這個過程的實現是通過Looper類中的ThreadLocal對象來檢查Looper對象是否存在。如果Looper不存在,將會創建一個新的Looper對象和一個新的消息隊列。如下代碼展示了這個過程

PS: 公有的prepare方法會默認調用prepare(true)

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException(“Only one Looper may be created per thread”);
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

Handler 現在能接收到消息并加入到消息隊列中,執行靜態方法Looper.loop()方法會開始將消息從消息隊列中出隊。每次輪訓迭代器指向下一條消息,接著分發消息對應目標地的Handler,然后回收消息到消息池中。Looper.looper()方法循環執行這個過程,直到Looper終止。下面代碼片段展示這個過程:

public static void loop() {
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        msg.target.dispatchMessage(msg);
        msg.recycleUnchecked();
    }
}

(六)、整體流程圖

整體流程圖.gif

三、享元模式

享元模式是對象池的一種實現,盡可能減少內存的使用,使用緩存來共享可用的對象,避免創建過多的對象。Android中Message使用的設計模式就是享元模式,將獲取Message通過obtain方法從對象池獲取,Message使用結束通過recyle將Message歸還給對象池,達到循環利用對象,避免重復創建的目的

(一)概念

享元模式(Flywight Pattern) 是一種軟件設計模式。它使用共享物件,用來盡可能減少內存使用量以及分享咨詢給盡可能多的相似物件;它適用于只是因重復而導致無法使用無法令人接受的大量內存的大量物件。通常物件中的部分狀態時可以分享的。常見的做法是把他們放到外部數據結構,當需要使用時將他們傳遞給享元。

(二) 為什么要用享元模式

  • 當一個軟件系統在運行時產生的對象數量太多,將導致運行代價過高,帶來系統性能下降的問題。所以需要采用一個共享來避免大量擁有相同內容對象的開銷。在Java中,String類型就是使用享元模式。String對象是final類型,對象一旦創建就不可改變。在Java字符串常量都是存在常量池中的,Java會確保一個字符串常量在常量池只有一個拷貝。
  • 是對對象池的一種實現,共享對象,避免重復的創建,采用一個共享來避免大量擁有相同內容對象的開銷。使用享元模式可以有效支持大量的細粒度對象。

Flyweight,如果很多很小的對象它們有很多相同的東西,并且在很多地方用到,那就可以把它們抽取成一個對象,把不同的東西變成外部屬性,作為方法的參數傳入。

String類型的對象創建后就不可改變,如果兩個String對象所包含的內容相同時,JVM只創建一個String對象對應這兩個不同的對象引用。字符串常量池。

(三) 核心思想

1、概念

運行共享技術有效地支持大量細粒度對象的復用。系統只使用少量的對象,而這些對象都很相似,狀態變化很小,可以實現對象的多次復用。由于享元模式要求能夠共享對象必須是細顆粒對象,因此它又稱為輕量級模式,它是一種對象結構模式。

享元對象共享的關鍵是區分了內部狀態(Intrinsic State)和外部狀態(Extrinsic State)。

2、內部狀態(Intrinsic State)

存儲在享元對象內部并且不會隨環境改變而改變的狀態,內部狀態可以共享。

3、外部狀態(Extrinsic State)

享元對象的外部狀態通常由客戶端保存,并在享元對象被創建之后,需要使用的時候再傳入到享元對象內部。隨環境改變而改變的、不可以共享的狀態。一個外部狀態與另一個外狀態是相互獨立的。

由于區分了內部狀態和外部狀態,我們可以將具有相同內部狀態的對象存儲在享元池中,享元池中的對象是可以實現共享的,需要的時候將對象從享元池中取出,實現對象的復用。通過向取出的對象注入不同的外部狀態,可以得到一系列相似的對象,而這些對象在內存中實際上只存儲一份。

(四) 享元模式分類

  • 單純享元模式
  • 復合享元模式
1、單純享元模式結構重要核心模塊
  • 抽象享元角色:為具體享元角色規定了必須實現的方法,而外部狀態時以參數的行賄通過此方法傳入。在Java中可以由抽象類、接口擔當

  • 具體享元角色:實現抽象橘色規定的方法。如果存在內部狀態,就負責為內部狀態提供存儲空間。

  • 享元端角色*:負責創建和管理享元角色。想要達到共享目的,這個角色的實現是關鍵。

  • 客戶端角色:維護對所有享元對象的引用,而且還需要存儲對應的外部狀態。

單純享元模式和創新型的簡單工廠模式實際上非常相似,但是它的重點或者用意卻和工廠模式截然不同。工廠模式的使用主要是為了使用系統不依賴于實現的細節;而在享元模式的主要目的是避免大量擁有相同內容對象的開銷。

2、復合享元模式
  • 抽象享元角色:為了具體享元角色規定了必須實現的方法,而外部狀態就是以參數的形式聽過此方法傳入。在Java中可以由抽象類、接口來擔當。

  • 具體享元角色:實現抽象角色規定的方法。如果存在內部狀態,就負責為內部狀態提供存儲空間。

  • 復合享元角色:它所代表的對象是不可以共享的,并且可以分解為多個單純享元對象的組合。

  • 享元工廠角色:負責創建和管理享元角色。想要達到共享的目的,這個角色的實現是關鍵!

  • 客戶端角色:維護對所有享元對象的引用,而且還需要存儲對應的外部狀態。

(五) 享元模式的使用場景

一般在如下場景中使用享元模式

  • 1 一個系統有大量相同或相似的對象,造成內存大量耗費。
  • 2 對象大部分狀態都可以外部化,可以將這些外部狀態傳入對象中。
  • 3 再使用享元模式時需要維護一個存儲享元對象的享元池,而這需要耗費一定的系統資源,因此,應該在需要多次重復使用享元對象時才值得使用享元模式。

四、HandlerThread

HandlerThread官網

(一)、HandlerThread 簡介

我們看到HandlerThread很快就會聯想到Handler。Android中Handler的使用,一般都在UI線程中執行,因此在Handler接受消息后,處理消息時,不能做一些很耗時的操作,否則將出現ANR錯誤。Android中專門提供了HandlerThread類,來解決該類問題。HandlerThread是一個線程專門處理Handler的消息,依次從Handler的隊列中獲取信息,逐個進行處理,保證安全,不會出現混亂引發的異常。HandlerThread繼承于Thread,所以它卑職就是一個Thread。與普通Thread的差別就在于,它有個Looper成員變量。

我們看下官方對它的簡介:

Handy class for starting a new thread that has a looper. The looper can then be used to create handler classes. Note that start() must still be called.

翻譯一下:

HandlerThread可以創建一個帶有looper的線程。Looper對象可以用于創建Handler類來進行調度。

(二)、類源碼解析

代碼在HandlerThread.java

/**
 * Handy class for starting a new thread that has a looper. The looper can then be 
 * used to create handler classes. Note that start() must still be called.
 */
public class HandlerThread extends Thread {
    // 線程的優先級 
    int mPriority;

    // 線程id
    int mTid = -1;

    // Looper對象,消息對象以及循環
    Looper mLooper;

    public HandlerThread(String name) {
        super(name);
        //設置默認的線程優先級
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }
    
    /**
     * Constructs a HandlerThread.
     * @param name
     * @param priority The priority to run the thread at. The value supplied must be from 
     * {@link android.os.Process} and not from java.lang.Thread.
     */
    // 自定義設置線程優先級
    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }
    
    /**
     * Call back method that can be explicitly overridden if needed to execute some
     * setup before Looper loops.
     */
    //如果有需要這個方法可以重寫,例如可以在這里聲明這個Handler關聯此線程
    protected void onLooperPrepared() {
    }
    //Thread線程的run方法
    @Override
    public void run() {
        //獲取當前線程的id
        mTid = Process.myTid();

        // 一旦調用這句代碼,就在此線程中創建了Looper對象,這就是為什么我們要在調用線程start()方法后,才能得到Looper對象,即當調用Looper.myLooper()時不為null
        Looper.prepare();

        // 同步代碼塊,意思就是當獲取mLooper對象后對象后,喚醒所有線程
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
   
        // 設置線程優先級
        Process.setThreadPriority(mPriority);

        //調用上面的方法,需要用戶重寫
        onLooperPrepared();
        
        // 開啟消息循環
        Looper.loop();
        mTid = -1;
    }
    
    /**
     * This method returns the Looper associated with this thread. If this thread not been started
     * or for any reason is isAlive() returns false, this method will return null. If this thread 
     * has been started, this method will block until the looper has been initialized.  
     * @return The looper.
     */
    public Looper getLooper() {
        // 如果線程已經死了,所以返回null
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        // 同步代碼塊,正好和上面(run()方法里面的)形成對應,就是說,只要線程活著并且我的looper為null,那么我就讓你一直等
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }

    /**
     * Quits the handler thread's looper.
     * <p>
     * Causes the handler thread's looper to terminate without processing any
     * more messages in the message queue.
     * </p><p>
     * Any attempt to post messages to the queue after the looper is asked to quit will fail.
     * For example, the {@link Handler#sendMessage(Message)} method will return false.
     * </p><p class="note">
     * Using this method may be unsafe because some messages may not be delivered
     * before the looper terminates.  Consider using {@link #quitSafely} instead to ensure
     * that all pending work is completed in an orderly manner.
     * </p>
     *
     * @return True if the looper looper has been asked to quit or false if the
     * thread had not yet started running.
     *
     * @see #quitSafely
     */
    public boolean quit() {
        Looper looper = getLooper();
        if (looper != null) {
            // 退出消息循環
            looper.quit();
            return true;
        }
        return false;
    }

    /**
     * Quits the handler thread's looper safely.
     * <p>
     * Causes the handler thread's looper to terminate as soon as all remaining messages
     * in the message queue that are already due to be delivered have been handled.
     * Pending delayed messages with due times in the future will not be delivered.
     * </p><p>
     * Any attempt to post messages to the queue after the looper is asked to quit will fail.
     * For example, the {@link Handler#sendMessage(Message)} method will return false.
     * </p><p>
     * If the thread has not been started or has finished (that is if
     * {@link #getLooper} returns null), then false is returned.
     * Otherwise the looper is asked to quit and true is returned.
     * </p>
     *
     * @return True if the looper looper has been asked to quit or false if the
     * thread had not yet started running.
     */
    public boolean quitSafely() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quitSafely();
            return true;
        }
        return false;
    }

    /**
     * Returns the identifier of this thread. See Process.myTid().
     */
    // 返回線程ID
    public int getThreadId() {
        return mTid;
    }
}

整體來說上面代碼還是比較淺顯易懂的。主要作用是建立了一個線程,并且創建了消息隊列,有自己的Looper,可以讓我們在自己線程中分發和處理消息。

quit和quitSafely都是退出HandlerThread的消息循環,其分別調用Looper的quit和quitSafely方法。我們在這里簡單說下區別:

  • 1 quit方法會將消息隊列中的所有消息移除(延遲消息和非延遲消息)。
  • 2 quitSafely 會將消息隊列所有延遲消息移除。非延遲消息則派發出去讓Handler去處理。
  • quitSafely相比于quit方法安全之處在于清空消息之前會派發所有的非延遲消息。
(三)、HandlerThread的使用

通過上面的源碼,我們大概也能推測出HandlerThread的使用步驟,它的使用步驟如下:

  • 第一步:創建一個HandlerThread實例,本質是創建一個包含Looper的線程。比如
  HandlerThread handlerThread=new HandlerThread("name")
  • 第二步:開啟線程
  handlerThread.start()
  • 第三步:獲得線程Looper
 Looper looper=handlerThread.getLooper();
  • 第四步:創建Handler,并用looper初始化
Handler  handler=new Handler(looper);
  • 第五步:利用handle進行一些操作
  • 第六步:調用quit()或者quitSafely()來終止它的循環

所以說整體流程如下:

當我們使用HandlerThrad創建一個線程,它start()之后會在它的線程創建一個Looper對象且初始化一個MessageQueue,通過Looper對象在他的線程構建一個Handler對象,然后我們通過Handler發送消息的形式將任務發送到MessaegQueue中,因為Looper是順序處理消息的,所以當有多個任務存在時會順序的排隊執行。但我們不使用的時候我們應該調用它的quit()或者quitSafely()來終止它的循環。

(四)、HandlerThread和普通Thread的比較

HandlerThread繼承自Thread,當線程開啟時,也就是它run方法運行起來后,線程同時創建了了一個含有消息隊列的Looper,并對外提供自己這個Looper對象的get方法。

(五)、HandlerThread的使用場景
  • 1、開發中如果多次使用類似new Thread(){...}.start()。這種方式開啟一個子線程,會創建多個匿名線程,使得程序運行起來越來越慢,而HandlerThread自帶Looper使他可以通過消息來多次重復使用當前線程,節省開支。
  • 2、android系統提供的Handler類內部的Looper默認綁定的是UI線程的消息隊列,對于非UI線程又想使用消息機制,那么HandlerThread內部的Looper是最合適的,它不會干擾或阻塞UI線程。
  • 3、HandlerThread適合處理本地I/O讀寫操作(比如數據庫),因為本地I/O操作大多數的耗時屬于毫秒級別的,對于單線程+異步隊列的形式不會產生較大的阻塞。而網絡操作相對于比較耗時,容易阻塞后面的請求,因此在這個HandlerThread中不合適加入網絡操作。
(五) 小結:
  • 1、HandlerThread將loop轉到子線程中去處理,說白了就是分擔MainLooper的工作量,降低了主線程壓力,使主界面更流程。
  • 2、開啟一個線程起到多個線程的作用。處理任務是串行執行,按消息發送順序進行處理。HandlerThread本質是一個線程,在線程內部,代碼是串行處理的。但是由于每一個任務都將以隊列的方式逐個被執行到,一旦隊列中某個任務執行時間過長,那么就會導致后續的任務都會被延遲處理。HandlerThread擁有自己的消息隊列,它不會干擾或阻塞UI線程。
  • 3、對于網絡I/O操作,HandlerThread并不合適,因為它只有一個線程,還得排隊一個一個等著。

五、Handler的內存泄露

(一)、概述

android使用Java作為開發環境,Java的跨平臺和垃圾回收機制已經幫助我們解決了底層的一些問題。但是盡管有了垃圾回收機制,在開發android的時候仍然時不時遇到out of memory的問題,這個時候我們不禁要問,垃圾回收器去哪里了?這里我們主要講解handler引起的泄露,并且給出了幾種解決方案,并且最后提供一個第三方庫WeakHandler庫。

可能導致泄漏問題的handler一般會被提示 Lint警告如下:

This Handler class should be static or leaks might occur 

意思是Handler class應該使用靜態聲明,否則可能會出現內存泄露
下面是更詳細的說明(Android Studio,現在應該沒人用Eclipse了吧)

Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler as a static class; In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler; Make all references to members of the outer class using the WeakReference object.

大概意思是:

一旦Handler 被聲明為內部類,那么可能導致它的外部類不能夠被垃圾回收,如果Handler在其他線程(我們通常稱為工作線程(worker thread))使用Looper或MessageQueue(消息隊列),而不是main線程(UI線程),那么久沒有有這個問題。如果Handler使用Looper或MessageQueue在主線程(main thread),你需要對Handler的聲明做如下修改:
聲明Handler為static類;在外部類實例化一個外部類的WeakReferernce(弱引用)并且在Handler初始化時傳入這個對象給你的Handler;將所有引用的外部類成員使用WeakReference對象。

(二)、什么是內存泄露

Java使用有向圖機制,通過GC自動檢查內存中的對象(什么時候檢查由虛擬機決定),如果GC發現一個或一組對象為不可到達狀態,則將該對象從內存中回收。也就是說,一個對象不被任何應用所指向,則該對象會在被GC發現的時候被回收;另外,如果一組對象只包含相互的引用,沒沒有來自他們外部的引用(例如有兩個對象A和B相互持有引用,但沒有任何外部對象持有指向A或B的引用),這讓然屬于不可叨叨,同樣會被GC回收。

(三)、為什么會內存泄露

原因:

  • 當Android應用啟動的時候,會先創建一個應用主線程的Looper對象,Looper實現了一個簡單的消息隊列,一個一個的處理里面的Message對象。主線程Looper對象在整個應用生命周期中存在。
  • 當在主線程中初始化Handler時,該Handler和Looper的消息隊列關聯。發送到消息的隊列的Message會應用發送該消息的Handler對象,這樣系統可以調用Handler.handleMessage(Message)來分發處理該消息。
  • 在Java中,非靜態(匿名)內部類會引用外部類對象。而靜態內部類不會引用外部類對象。
    -垃圾回收機制中約定,當內存中的一個對象的引用計數為0時,將會被回收。
  • 如果外部類是Activity,則會引起Activity泄露。當Acitivity finish后,延時消息會繼續存在主線程消息隊列中1分鐘,然后處理消息。而該消息引用了Activity的Handler對象,然后這個Handler又引用了這個Activity。這些引用對象會保持到該消息被處理完,這樣就導致了該Activity對象無法被回收,從而導致了上面所說的Activity泄露。

所以說如果要修改該問題,只需要按照Lint提示那樣,把Handler類定義為靜態即可,然后通過WeakReference拉埃保持外部的Activity對象。

(四)、什么是WeakReference?

WeakReference弱引用,與強引用(即我們常說的引用)相對,它的特點是,GC在回收時會忽略掉弱引用,即就算有弱引用指向某對象,但只要該對象沒有被強引用所指向(實際上多數時候還要求沒有軟引用,但此處軟件用的概念可以忽略),該對象就會在被GC檢查到時回收掉。對于上面的代碼,用戶在關閉Activity之后,就算后臺線程還沒有結束,但由于僅有一條來自Handler的弱引用指向Activity,所以GC仍然會在檢查的時候把Activity回收掉。這樣內存泄露的問題就不會出現了。

(五)、Handler內存泄露使用弱引用的補充

一般將Handler聲明為static就不必造成內存泄露,聲明成弱引用Activity的話,雖然也不會造成內存泄露,但是需要等到handler中沒有執行任務后才會回收,因此性能不高。

所以說使用弱引用可以解決內存泄露,但是需要等到Handler中任務都執行完,才會釋放activity內存,不如直接static釋放的快。所以說單獨使用弱引用性能不是太高。

(六)、WeakHandler

WeakHandler使用起來和handler一樣,但它是線程安全的,WeakHandler使用如下:

1、WeakHandler的使用
public class ExampleActivity extends Activity {

    private WeakHandler mHandler; // We still need at least one hard reference to WeakHandler
    
    protected void onCreate(Bundle savedInstanceState) {
        mHandler = new WeakHandler();
        ...
    }
    
    private void onClick(View view) {
        mHandler.postDelayed(new Runnable() {
            view.setVisibility(View.INVISIBLE);
        }, 5000);
    }
}

你只需要將在以前的Handler替換成WeakHandler就行了。

2、WeakHandler的原理

WeakHandler的思想是將Handler和Runnable做一次封裝,我們使用的是封裝后的WeakHandler,但其實真正起到Handler作用的是封裝的內部,而封裝的內部對handler和runnable都是用的弱引用。

weakhandler.png
  • 第一幅圖是普通handler的引用關系圖
  • 第二幅圖是使用WeakHandler的引用關系

其實原文有對WeakHandler跟多的解釋,但是表述起來也是挺復雜的。

原文地址:https://techblog.badoo.com/blog/2014/10/09/calabash-android-query/
github項目地址:https://github.com/badoo/android-weak-handler

六、Handler的面試題

1、為什么安卓要使用Handler?

因為android更新UI只能在UI線程。為什么只能在UI線程更新UI?因為Android是單線程模型。為什么Android是單線程模型?那是因為如果任一線程都可以更新UI的話,線程安全處理起來相當麻煩,所以就規定了Android是單線程模型,只允許在UI線程更新UI

2、消息機制的原理:

這個請參考 本篇文章 二、Handler消息機制

3、MessageQueue是什么時候創建的?

MessageQueue是在Looper的構造函數里面創建的,所以一個線程對應一個Looper,一個Looper對應一個MessageQueue。

4、ThreadLocal在Handler機制中的作用

ThreadLocal是一個線程內部的數據存儲類,通過它可以在制定的線程中存儲數據,數據存儲以后,只有在指定線程中可以獲取的存儲的數據,對于其他線程就獲取不到數據。一般來說,當某些數據是以線程為作用域而且不同線程需要有不同的數據副本的時候,可以考慮用ThreadLocal。比如對于Handler,它要獲取當前線程的Looper,很顯然Looper的作用域就是線程,所以不同線程有不同的Looper。

5、Looper,Handler,MessageQueue的引用關系?

一個Handler對象持有一個MessageQueue和它構造時所屬的線程的Looper引用。也就是說一個Handler必須頂有它對應的消息隊列和Looper。一個線程可能有多個Handler,但是至多有只能有一個Looper和一個消息隊列。
在主線程中new了一個Handler對象后,這個Handler對象自動和主線程生成的Looper以及消息隊列關聯上了。子線程中拿到主線程中Handler的引用,發送消息后,消息對象就會發送到target屬性對應的的那個Handler對應的消息隊列中去,由對應Looper來處理(子線程msg->主線程handler->主線程messageQueue->主線程Looper->主線程Handler的handlerMessage)。而消息發送到主線程Handler,那么也就是發送到主線程的消息隊列,用主線程的Looper輪詢。

6、MessageQueue里面的數據結構是什么類型的,為什么不是Map或者其他什么類型的?

這個請參考 本片文章 一、Handler機制的思考

7、Handler引起的內存泄漏以及解決辦法

這個請參考 本片文章 五、Handler的內存泄露

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容