HandlerThread解析以及相關問題分析

stormWen發表於2019-03-02

HandlerThread 相信大家都比較熟悉了,從名字上看是一個帶有Handler訊息迴圈機制的一個執行緒,比一般的執行緒多了訊息迴圈的機制,可以說是Handler+Thread的結合,從原始碼上看也是如此的設計,一般情況下如果需要子執行緒和主執行緒之間相互互動,可以用HandlerThread來設計,這比單純的Thread要方便,而且更容易管理,因為大家都知道Thread的生命週期在一些情況下是不可控制的,比如直接new Thread().start(),這種方式在專案中是不推薦使用的,實際上Android的原始碼中也有很多地方用到了HandlerThread,下面我將分析一下HandlerThread以及涉及到的一些其他相關的問題。

HandlerThread的原始碼簡單分析

    int mPriority;
    int mTid = -1;
    Looper mLooper;
    private @Nullable Handler mHandler;

    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }
複製程式碼

首先欄位有優先順序,可以看到HandlerThread裡面的優先順序是預設的,當然了,你也可以修改,呼叫有二個引數的建構函式就可以了,在建構函式裡面有個String型別,代表HandlerThread名字的,這個一般情況下可以取執行緒的名稱。

 /**
     * Call back method that can be explicitly overridden if needed to execute some
     * setup before Looper loops.
     */
    protected void onLooperPrepared() {
    }

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }
複製程式碼

這裡面有一個方法onLooperPrepared(),在實際中,我們可以重寫這個方法做一些初始化的操作,這個run()是重點,可以看到Looper進行了初始化並且開始迴圈接收訊息了,並且喚醒了當前所有等待的執行緒,由於run方法是在start之後才會啟動的,因此在用HandlertThread的時候,在初始化了例項之後就必須呼叫start方法開啟訊息迴圈了。

 public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }
複製程式碼

這個方法是獲取當前的looper,可以看到如果沒有獲取的時候就一直等待直到獲取,而前面也提到了獲取到了就喚醒了所有的執行緒,看來這是執行緒的等待-喚醒機制應用。

public Handler getThreadHandler() {
        if (mHandler == null) {
            mHandler = new Handler(getLooper());
        }
        return mHandler;
    }
複製程式碼

這個是獲取HandlerThread繫結的Looper執行緒的Handler

public boolean quit() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quit();
            return true;
        }
        return false;
    }
    
    
 public boolean quitSafely() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quitSafely();
            return true;
        }
        return false;
    }
複製程式碼

可以看到這兩個方法去退出執行緒的Looper迴圈,那麼這兩個方法有什麼區別呢,實際上都是呼叫了MessageQueue的quit()方法,原始碼如下:

void quit(boolean safe) {
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }

        synchronized (this) {
            if (mQuitting) {
                return;
            }
            mQuitting = true;

            if (safe) {
                removeAllFutureMessagesLocked();
            } else {
                removeAllMessagesLocked();
            }

            // We can assume mPtr != 0 because mQuitting was previously false.
            nativeWake(mPtr);
        }
}
複製程式碼

可以看到:
當我們呼叫quit方法的時候,實際上執行了MessageQueue中的removeAllMessagesLocked方法,該方法的作用是把MessageQueue訊息池中所有的訊息全部清空,無論是延遲訊息(延遲訊息是指通過sendMessageDelayed或通過postDelayed等方法傳送的需要延遲執行的訊息,只要不是立即執行的訊息都是延遲訊息)還是非延遲訊息。

而quitSafely方法時,實際上執行了MessageQueue中的removeAllFutureMessagesLocked方法,通過名字就可以看出,該方法只會清空MessageQueue訊息池中所有的延遲訊息,並將訊息池中所有的非延遲訊息派發出去讓Handler去處理,quitSafely相比於quit方法安全之處在於清空訊息之前會派發所有的非延遲訊息,一句話,就是清除未來需要執行的訊息。

這兩個方法有一個共同的特點就是:Looper不再接收新的訊息了,訊息迴圈就此結束,此時通過Handler傳送的訊息也不會在放入訊息杜佇列了,因為訊息佇列已經退出了。
應用這2個方法的時候需要注意的是:quit方法從API 1就開始存在了,比較早,而quitSafely直到API 18才新增進來.

到此原始碼分析結束,下面看一個實際的簡單的例子:

  private static class UserHandlerThread extends HandlerThread implements Handler.Callback {
        private Handler mHandler;
        public UserHandlerThread(String name) {
            super(name);
            start();
            mHandler = new Handler(getLooper(), this);
        }

        @Override
        protected void onLooperPrepared() {
            super.onLooperPrepared();
        }

        @Override
        public boolean handleMessage(Message msg) {
            if (msg.what==0x123){
                Log.d("[app]","收到訊息了");
            }
            return false;
        }

        public Handler getHandler() {
            return mHandler;
        }
    }
    
    
    private UserHandlerThread handlerThread;
    handlerThread = new UserHandlerThread("handlerThread");
    handlerThread.getHandler().sendEmptyMessage(0x123);
    
複製程式碼

OK,大家都知道結果了吧,一般情況下,在OnDestroy方法中需要退出迴圈,比如下面程式碼:

@Override
    protected void onDestroy() {
        super.onDestroy();
        if (handlerThread != null) {
            handlerThread.getHandler().removeCallbacksAndMessages(null);
            handlerThread.quit();
            handlerThread = null;
        }
    }
    //Looper.getMainLooper().quit();
複製程式碼

大家先忽略註釋的那句程式碼,
好了,下面分析3個涉及到相關的問題:

● 既然子執行緒可以退出訊息迴圈佇列,那麼UI執行緒,也就是主執行緒是否也一樣可以在onDestroy方法退出訊息迴圈呢,就是取消剛才程式碼的註釋.

先說答案:不能退出主執行緒的訊息佇列,不然會丟擲Main thread not allowed to quit.錯誤,是不是很熟悉,沒錯,上面的程式碼中已經貼出來了,為什麼呢,MessageQueue有一個欄位:mQuitAllowed

 // True if the message queue can be quit.
    private final boolean mQuitAllowed;
複製程式碼

這個欄位代表:是否可以退出訊息佇列的意思,那麼主執行緒的這個欄位是什麼呢,是false,就是不能退出當前訊息的訊息佇列的意思,來看主執行緒的訊息初始化程式碼:Looper.prepareMainLooper();

public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }
    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));
    }
    
     private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
複製程式碼

可以看到prepare引數中,主執行緒傳入的是false,而在Looper的建構函式裡面被賦值了,也就是主執行緒的是false,所以這就決定了主執行緒是不能退出訊息佇列,那為什麼子執行緒就可以退出訊息佇列呢,因為子執行緒傳入的是true,程式碼如下:

Looper.prepare();
 public static void prepare() {
        prepare(true);
    }
複製程式碼

看到沒,子執行緒傳入的是true,這說明子執行緒是可以退出訊息佇列的,
實際上真正退出主執行緒的訊息佇列是FrameWork層做的事情,下面的第三個問題會提到。

● Handler裡面發出去的訊息,究竟是在哪個執行緒執行的呢?

本文並不打算長篇分析Handler的原始碼,只簡單討論跟Handler執行緒相關的問題,實際上,對於Handler,如果能理解下面的圖,然後再自己去分析原始碼,我想理解會更深

HandlerThread解析以及相關問題分析

我們知道,handler的建構函式中有一個引數,就是設定當前Looper的,程式碼如下:

 /**
     * Use the provided {@link Looper} instead of the default one.
     *
     * @param looper The looper, must not be null.
     */
    public Handler(Looper looper) {
        this(looper, null, false);
    }
     public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
    mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can`t create handler inside thread that has not called Looper.prepare()");
        }
    mQueue = mLooper.mQueue;
複製程式碼

可以看到傳入的Looper最終會被賦值到mLooper欄位,如果沒有呼叫這個建構函式,那麼mLooper由當前執行緒獲取,而訊息佇列MessageQueue是由Looper得到的,換句話說:Handler的訊息在哪個Looper迴圈,那意味著所有的訊息將會被哪個Looper的訊息佇列所儲存和處理,而一個執行緒只能繫結一個Looper,因此一個Handler只能繫結一個Looper,也就意味著,Handler繫結了哪個執行緒的Looper,那麼所有的訊息都會在哪個執行緒執行,這跟上面的圖是一致的,當然了有同學說我在Handler裡面開執行緒就另外說了,但Handler操作的本身一定是在對應的Looper所在的執行緒的,就是如果Handler繫結了主執行緒的Looper,那麼所有的訊息都會在主執行緒處理,如果繫結的是子執行緒,那麼所有的訊息都會在子執行緒處理。

●在Android一個應用程式”退出”的真正含義是什麼呢?

對於這個問題,有同學可能說我殺掉程式,應用程式掛了,也就相當於退出了。實際上這個說法是不正確的,因為這裡的殺掉程式,不僅僅是程式本身,而且連程式的記憶體空間也一併被處理,我們前面說了,子執行緒可以退出訊息佇列,意味著子執行緒就再也無法接收到任何訊息了,這就是子執行緒的退出含義,實際上,主執行緒也是一樣的,只是這個過程對我們開發者不可見而已,在主執行緒中,如果退出了訊息佇列,那麼意味著主執行緒也無法接收到任何訊息,下面是程式碼,在ActivityThread.java裡面:

 public final void scheduleExit() {
            sendMessage(H.EXIT_APPLICATION, null);
        }
 case EXIT_APPLICATION:
                    if (mInitialApplication != null) {
                        mInitialApplication.onTerminate();
                    }
                    Looper.myLooper().quit();
                    break;
複製程式碼

看到這裡,我想熟悉IPC過程的同學應該都知道了,沒錯,scheduleExit()不是本地程式呼叫的,而是由服務端程式ActivityAManagerService服務進行呼叫的,這也是我為什麼說退出主執行緒是由FrameWork呼叫的原因,在AMS裡面有2處地方呼叫了退出的程式碼,分別是繫結本地程式和記憶體回收工作的時候呼叫的,下面是程式碼(在ActivityManagerService.Java裡面):

在APP本地程式繫結到AMS程式的時候呼叫
 private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid) {

        // Find the application record that is being attached...  either via
        // the pid if we are running in multiple processes, or just pull the
        // next app record if we are emulating process with anonymous threads.
        ProcessRecord app;
        if (pid != MY_PID && pid >= 0) {
            synchronized (mPidsSelfLocked) {
                app = mPidsSelfLocked.get(pid);
            }
        } else {
            app = null;
        }

        if (app == null) {
            Slog.w(TAG, "No pending application record for pid " + pid
                    + " (IApplicationThread " + thread + "); dropping process");
            EventLog.writeEvent(EventLogTags.AM_DROP_PROCESS, pid);
            if (pid > 0 && pid != MY_PID) {
                Process.killProcessQuiet(pid);
                //TODO: killProcessGroup(app.info.uid, pid);
            } else {
                try {
                //這裡也呼叫了
                    thread.scheduleExit();
                } catch (Exception e) {
                    // Ignore exceptions.
                }
            }
            return false;
        }
        //省略一些非必要程式碼
    }


//i清理記憶體的時候呼叫
final void trimApplications() {
        synchronized (this) {
            int i;

            // First remove any unused application processes whose package
            // has been removed.
            for (i=mRemovedProcesses.size()-1; i>=0; i--) {
                final ProcessRecord app = mRemovedProcesses.get(i);
                if (app.activities.size() == 0
                        && app.curReceiver == null && app.services.size() == 0) {
                    Slog.i(
                        TAG, "Exiting empty application process "
                        + app.toShortString() + " ("
                        + (app.thread != null ? app.thread.asBinder() : null)
                        + ")
");
                    if (app.pid > 0 && app.pid != MY_PID) {
                        //殺程式
                        app.kill("empty", false);
                    } else {
                        try {
                        //這裡呼叫了退出主執行緒訊息佇列程式碼
                            app.thread.scheduleExit();
                        } catch (Exception e) {
                            // Ignore exceptions.
                        }
                    }
                    cleanUpApplicationRecordLocked(app, false, true, -1, false /*replacingPid*/);
                    mRemovedProcesses.remove(i);

                    if (app.persistent) {
                        addAppLocked(app.info, false, null /* ABI override */);
                    }
                }
            }

            // Now update the oom adj for all processes.
            updateOomAdjLocked();
        }
    }
複製程式碼

看到了吧,在首次App繫結程式的時候,如果發生app==null這個錯誤的時候就呼叫了退出主執行緒訊息佇列,另一個就是在清理記憶體的時候呼叫,這兩個過程對我們開發者不可見,我們瞭解就好,在原始碼面前,並沒有什麼是神奇的地方。

感謝大家閱讀,如果有什麼錯誤的地方,請歡迎指出,謝謝。

相關文章