1. 程式人生 > 其它 >Handler面試知識點看這篇就夠了

Handler面試知識點看這篇就夠了

  • Java 中的實現
    • ThreadLocal
    • MessageQueue
      • 非同步訊息和訊息屏障
        • MessageQueue.postSyncBarrier
        • 訊息屏障的消費
        • 非同步訊息的作用
    • Looper
    • Handler
    • MessageQueue.IdleHandler
  • Native的實現
    • MessageQueue
    • Looper#建立epoll事件
    • epoll
    • nativePollOnce
    • nativeWake
    • postDelay的實現
  • HandlerThread
  • IntentService
  • 參考和問題

大綱

強烈建議看一下Gityuan的文章

簡而言之,就是在looper裡面的message暫時處理完了,這個時候會回撥這個介面,返回false,那麼就會移除它,返回true就會在下次message處理完了的時候繼續回撥

/**
  * Callback interface for discovering when a thread is going to block
  * waiting for more messages.
*/
public static interface IdleHandler {
    /**
    * Called when the message queue has run out of messages and will now
    * wait for more.  Return true to keep your idle handler active, false
    * to have it removed.  This may be called if there are still messages
    * pending in the queue, but they are all scheduled to be dispatched
    * after the current time.
    */
    boolean queueIdle();
}

Java 中的實現

涉及到的4個類:

  • Handler
  • Looper
  • ThreadLocal
  • MessageQueue

ThreadLocal

每個執行緒有一些和自己相關的變數,ThreadLocal的作用就是儲存這些變數的。所有的變數是通過內部的靜態類Value儲存的。雖然,執行緒都是通過訪問相同的ThreadLocal,但是每個執行緒儲存的變數是分開的:

public void set(T value) {    
      Thread currentThread = Thread.currentThread();    
      Values values = values(currentThread);    
      if (values == null) {        
            values = initializeValues(currentThread);    }    
      values.put(this, value);
}

上面的set方法中,values方法返回當前執行緒的localValues成員變數:

/** 
  * Gets Values instance for this thread and variable type. 
  */
Values values(Thread current) {    
      return current.localValues;
}

那麼執行緒內部的localValues是什麼?

public class Thread implements Runnable {
    /** 
      * Normal thread local values. 
      */
    ThreadLocal.Values localValues;
    /*省略若干程式碼*/
}

可以看到,Thread中的localValues是定義在ThreadLocal中執行緒本地的變數。如果在 values ()方法取得null值,就執行initializeValues方法。 initializeValues是如何實現的呢?

Values initializeValues(Thread current) {    
      return current.localValues = new Values();
}

然後將value的值放在當前執行緒的的localValues裡。這樣,雖然看起來訪問的是用一個ThreadLocal,但是得到的值卻是根據執行緒而不同的。

注:不同sdk中ThreadLocal內部的實現時不一樣的,比如在6.0的版本實現的方式就不是上面的方式,但是原理還是一樣的

舉個例子

public class JsonTestMetaData {
    public String json_str;
}

public class MainActivity extends Activity {

    ThreadLocal<JsonTestMetaData> local = new ThreadLocal<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
       
        JsonTestMetaData data = new JsonTestMetaData();
        data.json_str = "main_thread";
        local.set(data);

        Log.e(TAG, local.get().json_str);

        new Thread(new Runnable() {
            @Override
            public void run() {
                JsonTestMetaData data = new JsonTestMetaData();
                data.json_str = "other_thread";
                local.set(data);
                Log.e(TAG, local.get().json_str);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                if (local.get() != null) {
                    Log.e(TAG, local.get().json_str);
                } else {
                    Log.e(TAG, "local.get() is null");

                }
            }
        }).start();


        Log.e(TAG, local.get().json_str);

    }
}

得到的結果:

01-09 14:28:36.410 29303-29303/com.xx.app.javabcsxtest E/MainActivity: main_thread
01-09 14:28:36.412 29303-29303/com.xx.app.javabcsxtest E/MainActivity: main_thread
01-09 14:28:36.412 29303-29331/com.xx.app.javabcsxtest E/MainActivity: other_thread
01-09 14:28:36.413 29303-29332/com.xx.app.javabcsxtest E/MainActivity: local.get() is null

TheadLocal可能會引起記憶體洩漏,假如Thread沒有退出的話。

MessageQueue

MessageQueue是一個訊息佇列,包含成員變數Message mMessages;,可以理解成連結串列的頭部。儲存的形式不是佇列,而是單鏈表。 內部包含5個native方法:

private native static long nativeInit();
private native static void nativeDestroy(long ptr);
private native static void nativePollOnce(long ptr, int timeoutMillis);
private native static void nativeWake(long ptr);
private native static boolean nativeIsIdling(long ptr);

底層還是通過native程式碼完成的。後面的部分有展開。

在Java層面,主要是

  • next方法,獲得下一個訊息;
  • enqueueMessage 將訊息插入到佇列中。

非同步訊息和訊息屏障

在ViewRootImpl的scheduleTraversals方法發現這樣一段程式碼

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

發現使用MessageQueue添加了一個訊息屏障

MessageQueue.postSyncBarrier

程式碼比較簡單,只是向佇列中插入一個Msg,when是SystemClock.uptimeMillis(),並且這個Msg沒有target

    public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

    private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token.
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            final int token = mNextBarrierToken++;
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

訊息屏障的消費

MessageQueue的next方法,如果遇到訊息屏障,就去尋找連結串列後第一個非同步訊息,優先處理非同步訊息

Message next() {
    nativePollOnce(ptr, nextPollTimeoutMillis);
    synchronized (this) {
        Message prevMsg = null;
        Message msg = mMessages;

        
        if (msg != null && msg.target == null) {
            do {
                prevMsg = msg;
                msg = msg.next;
            } while (msg != null && !msg.isAsynchronous()); 
        }
        if (msg != null) {
            if (now < msg.when) {//非同步訊息還沒到處理時間,就在等會(超時時間)
                nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
            } else {
                //非同步訊息到了處理時間,就從連結串列移除,返回它。
                mBlocked = false;
                if (prevMsg != null) {
                    prevMsg.next = msg.next;
                } else {
                    mMessages = msg.next;
                }
                msg.next = null;
                if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                msg.markInUse();
                return msg;
            }
        } else {
            nextPollTimeoutMillis = -1;
        }
        //...
    }
}

非同步訊息的作用

可以去看一下Message的setAsynchronous方法的註解

在View的invalidate重繪的過程,會引入同步屏障,其實就是保證非同步Message優先處理,相當於插隊唄。?

Certain operations, such as view invalidation, may introduce synchronization barriers into the {@link Looper}'s message queue to prevent subsequent messages from being delivered until some condition is met. In the case of view invalidation, messages which are posted after a call to {@link android.view.View#invalidate} are suspended by means of a synchronization barrier until the next frame is ready to be drawn. The synchronization barrier ensures that the invalidation request is completely handled before resuming.

Asynchronous messages are exempt from synchronization barriers. They typically represent interrupts, input events, and other signals that must be handled independently even while other work has been suspended.

Looper

Looper是做什麼的?Looper的職能是為一個執行緒建立MessageQueue,繫結到這個執行緒,為此執行緒執行訊息迴圈。

Looper內部包含MessageQueue和Thread的引用

MessageQueue在prepare方法中建立,在loop方法開始迴圈。

Java層的Looper和MessageQueue有在C++對應的類,分別是Looper(Native)和NativeMessageQueue類

執行緒預設是沒有looper的,除非你線上程呼叫prepare方法,然後才能執行loop方法才能進行訊息處理。

prepare做了什麼呢?

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

你可以看到prepare()方法只能呼叫一次。在最後會建立一個Looper放在ThreadLocal裡儲存。 Looper是如何建立的呢?

private Looper(boolean quitAllowed) {    
      mQueue = new MessageQueue(quitAllowed);    
      mThread = Thread.currentThread();
}

可以看到,構造方法是私有的,新建立了一個MessageQueue,mThread就是當前執行緒。 那麼Looper是如何執行訊息迴圈的?

public static void loop() {    
        /*中間省略若干程式碼*/
        final Looper me = myLooper();    
        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();       
              msg.target.dispatchMessage(msg);     
              msg.recycleUnchecked();    
        }
}

可以看到,通過一個無限迴圈,不停的在訊息佇列中拿訊息,將訊息分發到指定的地方。

Message的target其實就是Handler

所以,在你寫的執行緒中,可以這樣使用:

class LooperThread extends Thread {     
      public Handler mHandler;      
      public void run() {          
            Looper.prepare();
            mHandler = new Handler() {
                  public void handleMessage(Message msg) {
                       // process incoming messages here
                  }
            };
            Looper.loop();
     }
}

不過上述的方式太low,在程式碼中也不方便,可以這樣寫:

HandlerThread

HandlerThread thread  = new HandlerThread("new_handler_thread");
Handler handler = new Handler(thread.getLooper(), new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                return false;
            }
});

HandlerThread繼承自Thread,自身會建立一個Looper。

關於 HandlerThread可參考 Android HandlerThread 完全解析

大多數和訊息迴圈的互動都是通過Handler去完成的,就像你在Handler那部分看到的那樣。記得在你不再執行訊息迴圈的時候呼叫Looperquit方法。

Handler

Handler能夠傳送和處理和MessageQueue關聯的 MessageRunnable。每個Handler和一個單獨的執行緒關聯,這個執行緒就是你建立這個Handler的時候所在的執行緒,需要處理的MessageQueue也是這個執行緒的MessageQueue

請看:

public Handler(Callback callback, boolean 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;
    mCallback = callback;
    mAsynchronous = async;
}

public Handler(Looper looper, Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

Handler有很多個建構函式,最終都會呼叫到這個建構函式。你可以看到Handler中的的Looper成員,就是通過Looper的靜態方法myLooper得到的,myLooper是幹啥的?你可以看Looper的內容,在上面程式碼中得到了一個和這個執行緒關聯的Looper。如果 mLooper成員是null,那麼就丟擲異常。你可能會問,我在activity中隨便建立handler啊,沒有呼叫Looper.myLooper()方法。那是因為當你的應用執行的時候,Android已經通過Looper的靜態方法prepareMainLooper建立了,這個方法只能執行一次,否則就會丟擲異常了。這個Looper適合執行緒繫結的,你再看看mQueue,是從mLooper中拿到的。

呼叫的順序如下:

  • Message的callback是否是null?不是呼叫callback的run方法(其實這裡Message的callback是一個Runnable物件,通過Handler的post方法傳遞)
  • Handler的callback是否是null?不是呼叫callback。這裡的callback的型別是Handler.Callback()
  • 最終呼叫Handler的handleMessage的方法。
    /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

MessageQueue.IdleHandler

簡而言之,就是在looper裡面的message暫時處理完了,這個時候會回撥這個介面,返回false,那麼就會移除它,返回true就會在下次message處理完了的時候繼續回撥

Native的實現

這裡可以參考Gityuan - Android訊息機制2-Handler(Native層)

MessageQueue

MessageQueue.java


MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();  //mPtr記錄native訊息佇列的資訊 【2】
}

private native static long nativeInit();
private native static void nativeDestroy(long ptr);
private native void nativePollOnce(long ptr, int timeoutMillis);
private native static void nativeWake(long ptr);
private native static boolean nativeIsPolling(long ptr);
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);

可以看到java層的mPtr實際上是native層地址指標,關於reinterpret_cast

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
    //初始化native訊息佇列 【3】
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    nativeMessageQueue->incStrong(env); //增加引用計數
    return reinterpret_cast<jlong>(nativeMessageQueue);
}

和Java不一樣的是,native的MessageQueue內部有一個looper

NativeMessageQueue::NativeMessageQueue()
            : mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {

    mLooper = Looper::getForThread(); //獲取TLS中的Looper物件
    if (mLooper == NULL) {
        mLooper = new Looper(false); //建立native層的Looper 【4】
        Looper::setForThread(mLooper); //儲存native層的Looper到TLS
    }
}

  • nativeInit()方法:
    • 建立了NativeMessageQueue物件,增加其引用計數,並將NativeMessageQueue指標mPtr儲存在Java層的MessageQueue
    • 建立了Native Looper物件
    • 呼叫epoll的epoll_create()/epoll_ctl()來完成對mWakeEventFd和mRequests的可讀事件監聽
  • nativeDestroy()方法
    • 呼叫RefBase::decStrong()來減少物件的引用計數
    • 當引用計數為0時,則刪除NativeMessageQueue物件
  • nativePollOnce()方法
    • 呼叫Looper::pollOnce()來完成,空閒時停留在epoll_wait()方法,用於等待事件發生或者超時
  • nativeWake()方法
    • 呼叫Looper::wake()來完成,向管道mWakeEventfd寫入字元;

Looper#建立epoll事件

Looper::Looper(bool allowNonCallbacks) :
        mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
        mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
        mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
    mWakeEventFd = eventfd(0, EFD_NONBLOCK); //構造喚醒事件的fd
    AutoMutex _l(mLock);
    rebuildEpollLocked();  //重建Epoll事件【5】
}

eventfd

void Looper::rebuildEpollLocked() {
    if (mEpollFd >= 0) {
        close(mEpollFd); //關閉舊的epoll例項
    }
    mEpollFd = epoll_create(EPOLL_SIZE_HINT); //建立新的epoll例項,並註冊wake管道
    struct epoll_event eventItem;
    memset(& eventItem, 0, sizeof(epoll_event)); //把未使用的資料區域進行置0操作
    eventItem.events = EPOLLIN; //可讀事件
    eventItem.data.fd = mWakeEventFd;
    //將喚醒事件(mWakeEventFd)新增到epoll例項(mEpollFd)
    int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);

    for (size_t i = 0; i < mRequests.size(); i++) {
        const Request& request = mRequests.valueAt(i);
        struct epoll_event eventItem;
        request.initEventItem(&eventItem);
        //將request佇列的事件,分別新增到epoll例項
        int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, request.fd, & eventItem);
    }
}

epoll

Gityuan - select/poll/epoll對比分析

監視的描述符數量不受限制,所支援的FD上限是最大可以開啟檔案的數目,具體數目可以cat /proc/sys/fs/file-max檢視,一般來說這個數目和系統記憶體關係很大,以3G的手機來說這個值為20-30萬。

IO效能不會隨著監視fd的數量增長而下降。epoll不同於select和poll輪詢的方式,而是通過每個fd定義的回撥函式來實現的,只有就緒的fd才會執行回撥函式。

nativePollOnce

Message next() {
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    for (;;) {
        ...
        nativePollOnce(ptr, nextPollTimeoutMillis); //阻塞操作 【2】
        ...
    }

android_os_MessageQueue.cpp

static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj, jlong ptr, jint timeoutMillis) {
    //將Java層傳遞下來的mPtr轉換為nativeMessageQueue
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis); 【3】
}

android_os_MessageQueue.cpp

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
    mPollEnv = env;
    mPollObj = pollObj;
    mLooper->pollOnce(timeoutMillis); 【4】
    mPollObj = NULL;
    mPollEnv = NULL;
    if (mExceptionObj) {
        env->Throw(mExceptionObj);
        env->DeleteLocalRef(mExceptionObj);
        mExceptionObj = NULL;
    }
}

Looper.h

inline int pollOnce(int timeoutMillis) {
    return pollOnce(timeoutMillis, NULL, NULL, NULL); 【5】
}

Looper.cpp

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) {
        // 先處理沒有Callback方法的 Response事件
        while (mResponseIndex < mResponses.size()) {
            const Response& response = mResponses.itemAt(mResponseIndex++);
            int ident = response.request.ident;
            if (ident >= 0) { //ident大於0,則表示沒有callback, 因為POLL_CALLBACK = -2,
                int fd = response.request.fd;
                int events = response.events;
                void* data = response.request.data;
                if (outFd != NULL) *outFd = fd;
                if (outEvents != NULL) *outEvents = events;
                if (outData != NULL) *outData = data;
                return ident;
            }
        }
        if (result != 0) {
            if (outFd != NULL) *outFd = 0;
            if (outEvents != NULL) *outEvents = 0;
            if (outData != NULL) *outData = NULL;
            return result;
        }
        // 再處理內部輪詢
        result = pollInner(timeoutMillis); 【6】
    }
}

nativeWake

呼叫Looper::wake()來完成,向管道mWakeEventfd寫入字元;(epoll的喚醒事件)

boolean enqueueMessage(Message msg, long when) {
    ... //將Message按時間順序插入MessageQueue
    if (needWake) {
            nativeWake(mPtr); 【2】
        }
}

static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->wake(); 【3】
}

void NativeMessageQueue::wake() {
    mLooper->wake();  【4】
}

void Looper::wake() {
    uint64_t inc = 1;
    // 向管道mWakeEventFd寫入字元1
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
    if (nWrite != sizeof(uint64_t)) {
        if (errno != EAGAIN) {
            ALOGW("Could not write wake signal, errno=%d", errno);
        }
    }
}

postDelay的實現

  • 1.呼叫sendMessageAtTime,計算當前時間和delay時間,呼叫MessageQueue的enqueMessage入隊
  • 2.MessageQueue的enqueueMessage會遍歷佇列,根據Message的when計算插入到佇列的位置,如果next的死迴圈處於阻塞,就會呼叫native方法喚醒NativeMessageQueue
  • 3.nativePollOnce會將時間傳遞給Looper.cpp的方法,實際上是epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis); 也就是epoll的阻塞時間。
public final boolean postDelayed(Runnable r, long delayMillis)
{
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
 public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}   
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
} 

 boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

HandlerThread

繼承自Thread,run的時候會呼叫Looper.prepare();有兩個比較重要的方法

public Looper getLooper()

public Handler getThreadHandler()

使用完成後需要呼叫quit()

IntentService

onCreate()會建立HandlerThread並start,建立剛才的HandlerThread的Looper的ServiceHandler Overide的onHandleIntent方法是在非同步執行緒中執行

相關視訊推薦:

【Android handle面試】Handler中的Message如何建立?
【Android handle面試】MessageQueue如何保持各執行緒新增訊息的執行緒安全?
Android(安卓)開發零基礎從入門到精通教程:Studio安裝/UI/Fragment/四大元件/流行框架/專案釋出與管理/專案實戰

本文轉自 https://juejin.cn/post/6984960343130767373,如有侵權,請聯絡刪除。