主頁 > 移動端開發 > Handler 的知識點你有了解多少?

Handler 的知識點你有了解多少?

2021-09-03 19:16:13 移動端開發

大家想想這個知識點一般是怎么考察的?請解釋一下Handler的原理?

不不不,這個問題已經爛大街了,我要是面試官,我會這么問,

我們知道在Handler中,存在一個方法叫 sendMessageDelay , 作用是延時發送訊息,請解釋一下Handler是如何實作延時發送訊息的?

Looper.loop是一個死回圈,拿不到需要處理的Message就會阻塞,那在UI執行緒中為什么不會導致ANR?

也請各位讀者先自己思考一下這兩個問題,換做是你該怎么回答,

Handler

我們先從Handler的定義來認識它,先上谷歌原文:

/**
 * A Handler allows you to send and process {@link Message} and Runnable
 * objects associated with a thread's {@link MessageQueue}.  Each Handler
 * instance is associated with a single thread and that thread's message
 * queue.  When you create a new Handler, it is bound to the thread /
 * message queue of the thread that is creating it -- from that point on,
 * it will deliver messages and runnables to that message queue and execute
 * them as they come out of the message queue.
 * 
 * <p>There are two main uses for a Handler: (1) to schedule messages and
 * runnables to be executed at some point in the future; and (2) to enqueue
 * an action to be performed on a different thread than your own.
 * 
 * <p>Scheduling messages is accomplished with the
 * {@link #post}, {@link #postAtTime(Runnable, long)},
 * {@link #postDelayed}, {@link #sendEmptyMessage},
 * {@link #sendMessage}, {@link #sendMessageAtTime}, and
 * {@link #sendMessageDelayed} methods.  The <em>post</em> versions allow
 * you to enqueue Runnable objects to be called by the message queue when
 * they are received; the <em>sendMessage</em> versions allow you to enqueue
 * a {@link Message} object containing a bundle of data that will be
 * processed by the Handler's {@link #handleMessage} method (requiring that
 * you implement a subclass of Handler).
 * 
 * <p>When posting or sending to a Handler, you can either
 * allow the item to be processed as soon as the message queue is ready
 * to do so, or specify a delay before it gets processed or absolute time for
 * it to be processed.  The latter two allow you to implement timeouts,
 * ticks, and other timing-based behavior.
 * 
 * <p>When a
 * process is created for your application, its main thread is dedicated to
 * running a message queue that takes care of managing the top-level
 * application objects (activities, broadcast receivers, etc) and any windows
 * they create.  You can create your own threads, and communicate back with
 * the main application thread through a Handler.  This is done by calling
 * the same <em>post</em> or <em>sendMessage</em> methods as before, but from
 * your new thread.  The given Runnable or Message will then be scheduled
 * in the Handler's message queue and processed when appropriate.
 */

下面由我這枚英語渣上線,強行翻譯一波,

  1. Handler 是用來結合執行緒的訊息佇列來發送、處理 Message物件Runnable物件 的工具,每一個Handler實體化之后會關聯一個執行緒和該執行緒的訊息佇列,當你創建一個Handler的時候,它就會自動系結到到所在的執行緒或執行緒的訊息佇列,并陸續把Message/Runnable分發到訊息佇列,然后在它們出隊的時候去執行,

  2. Handler 主要有兩個用途:

(1) 調度在將來某個時候執行的MessageRunnable
(2)把需要在另一個執行緒執行的操作加入到訊息佇列中去,

  1. post runnablesend messagehandler時,您可以在訊息佇列準備就緒后立即處理該事務,也可以延遲一段時間執行,或者指定某個特定時間去執行,

我們先從Handler的構造方法來認識一下它:

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) 

Handler的構造方法有很多個,但最終呼叫的就是上述構造方法,
老規矩,先上官方解釋,再上學渣翻譯,

* Use the provided {@link Looper} instead of the default one and take a callback
* interface in which to handle messages.  Also set whether the handler
* should be asynchronous.
*
* Handlers are synchronous by default unless this constructor is used to make
* one that is strictly asynchronous.
*
* Asynchronous messages represent interrupts or events that do not require global ordering
* with respect to synchronous messages.  Asynchronous messages are not subject to
* the synchronization barriers introduced by conditions such as display vsync.

1.使用提供的Looper而不是默認的Looper,并使用回呼介面來處理訊息,還設定處理程式是否應該是異步的,
2.默認情況下,Handler是同步的,除非此建構式用于生成嚴格異步的Handler,
3.異步訊息指的是不需要進行全域排序的中斷或事件,異步訊息不受同步障礙(比如display vsync)的影響,

Handler中的方法主要分為以下兩類:

  1. 獲取及查詢訊息,比如 obtainMessage(int what),hasMessages(int what)
  2. 將message或runnable添加/移出訊息佇列,比如 postAtTime(@NonNull Runnable r, long uptimeMillis),sendEmptyMessageDelayed(int what, long delayMillis)

在這些方法中,我們重點需要關注一下enqueueMessage這個方法,

為什么呢?

無論是 postAtTimesendMessageDelayed 還是其他的 post、send方法,它們最終都會調到enqueueMessage 這個方法里去,

比如:

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

可以看到,sendMessageDelayed方法里將延遲時間轉換為訊息觸發的絕對時間,最終呼叫的是sendMessageAtTime方法,

public boolean sendMessageAtTime(@NonNull 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);
}

而sendMessageAtTime方法呼叫了enqueueMessage方法,

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

enqueueMessage方法直接將message交給了MessageQueue去執行,

Message

在分析MessageQueue之前,我們應該先來認識一下Message這個訊息載體類,
老規矩,先從定義看起:

* Defines a message containing a description and arbitrary data object that can be
* sent to a {@link Handler}.  This object contains two extra int fields and an
* extra object field that allow you to not do allocations in many cases.
*
* <p class="note">While the constructor of Message is public, the best way to get
* one of these is to call {@link #obtain Message.obtain()} or one of the
* {@link Handler#obtainMessage Handler.obtainMessage()} methods, which will pull
* them from a pool of recycled objects.</p>

下面是渣翻譯:

  1. 定義一條包含描述和任意資料物件的訊息,該物件可以發送到Handler,此物件包含兩個額外的int欄位和一個額外的object欄位,

  2. 盡管Message的構造方法是public,但獲取一個Message的最好的方法是呼叫Message.obtain或者Handler.obtainMessage方法,這些方法會從可回收的執行緒池中獲取Message物件,

我們來認識一下Message里的欄位:

public final class Message implements Parcelable {
    //用戶定義的標識碼,以便接收者能夠識別這條訊息是關于什么的,
    //每個Handler都有自己的命名空間,因此不需要擔心標識碼與其他Handler的沖突,
    public int what;

    //如果只需要存盤幾個整數值,則arg1和arg2是使用setData(Bundle) setData()的低成本替代方案,
    public int arg1;
    
    public int arg2;

    //要發送給接收者的任意物件
    public Object obj;

    //通常在跨行程通訊中使用,讓服務端能夠得到客戶端的信使物件,給客戶端發訊息
    public Messenger replyTo;

    //可選欄位,指示發送訊息的uid,僅對Messenger發布的訊息有效,否則默認為-1
    public int sendingUid = UID_NONE;

    //可選欄位,指示導致此訊息排隊的uid,
    public int workSourceUid = UID_NONE;

     //此標識在訊息入隊時設定,在創建或者獲取新訊息時清除
     //嘗試入隊或回收已在使用的訊息會發送錯誤
    /*package*/ static final int FLAG_IN_USE = 1 << 0;

    //設定是否是異步訊息
    /*package*/ static final int FLAG_ASYNCHRONOUS = 1 << 1;

    //copyFrom方法中要清除的標志
    /*package*/ static final int FLAGS_TO_CLEAR_ON_COPY_FROM = FLAG_IN_USE;

    @UnsupportedAppUsage
    /*package*/ int flags;

    //Message發送的時間,基于SystemClock#uptimeMillis
    @UnsupportedAppUsage
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public long when;

    /*package*/ Bundle data;
    
    //目標handler
    @UnsupportedAppUsage
    /*package*/ Handler target;

    @UnsupportedAppUsage
    /*package*/ Runnable callback;

    //使用單向鏈表儲存下一個訊息
    @UnsupportedAppUsage
    /*package*/ Message next;
}

在Message中,我們需要關注一下Message的回識訓制,

先來看下recyclerUnchecked方法:

void recycleUnchecked() {
    // Mark the message as in use while it remains in the recycled object pool.
    // Clear out all other details.
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = UID_NONE;
    workSourceUid = UID_NONE;
    when = 0;
    target = null;
    callback = null;
    data = null;

    synchronized (sPoolSync) {
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

在這個方法中,有三個關鍵變數,

  1. sPoolSync :主要是給Message加一個物件鎖,不允許多個執行緒同時訪問Message類和recycleUnchecked方法,
  2. sPool:存盤我們回圈利用Message的單鏈表,這里sPool只是鏈表的頭節點,
  3. sPoolSize:單鏈表的鏈表的長度,即存盤的Message物件的個數,

當我們呼叫recycleUnchecked方法時,首先會將當前Message物件的屬性清空,然后判斷Message是否已到達快取的上限(50個),如果沒有,將當前的Message物件置于鏈表的頭部,

那么取快取的操作呢?

我們來看下obtain方法:

public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

可以看出,Message會嘗試取出sPool鏈表的第一個元素,并將sPool的頭元素往后移動一位,如果sPool鏈表為空,將會回傳一個新的Message物件,

Message里提供obtain方法獲取Message物件,使得Message到了重復的利用,減少了每次獲取Message時去申請空間的時間,同時,這樣也不會永無止境的去創建新物件,減小了Jvm垃圾回收的壓力,提高了效率,

MessageQueue

MessageQueue用于保存由Looper發送的訊息的串列,訊息不會直接添加到訊息佇列,而是通過Handler物件中關聯的Looper里的MessageQueue完成添加的動作,

您可以使用Looper.myQueue()檢索當前執行緒的MessageQueue,

我們先來看看MessageQueue如何實作添加一個Message的操作,

boolean enqueueMessage(Message msg, long when) {
    //判斷msg是否有target屬性以及是否正在使用中
    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標識為正在使用
        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // 如果佇列為空,或者when==0,表示需要立即執行,或者執行時間早于鏈表第一個元素時間
            //將新的msg加入mMessage鏈表的第一位
            msg.next = p;
            mMessages = msg;
            //如果處于阻塞狀態,需要喚醒佇列
            needWake = mBlocked;
        } else {
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            //此訊息是一條延時訊息,根據訊息的when,通過for回圈找到訊息的插入點
            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;
}

mMessages是一個按照訊息實際觸發時間msg.when排序的鏈表,越往后的越晚觸發,enqueueMessage方法根據新插入訊息的when,將msg插入到鏈表中合適的位置,如果是及時訊息,還需要喚醒MessageQueue,

我們接著來看看nativeWake方法,nativeWake方法的原始碼位于\frameworks\base\core\jni\android_os_MessageQueue.cpp,

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

繼續看NativeMessageQueue里的wake函式,

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

它又轉交給了Looper(原始碼位置/system/core/libutils/Looper.cpp)去處理,

void Looper::wake() {
#if DEBUG_POLL_AND_WAKE
    ALOGD("%p ~ wake", this);
#endif

    uint64_t inc = 1;
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
    if (nWrite != sizeof(uint64_t)) {
        if (errno != EAGAIN) {
            LOG_ALWAYS_FATAL("Could not write wake signal to fd %d (returned %zd): %s",
                             mWakeEventFd.get(), nWrite, strerror(errno));
        }
    }
}

Looper里的wake函式很簡單,它只是向mWakeEventFd里寫入了一個 1 值,

上述的mWakeEventFd又是什么呢?

Looper::Looper(bool allowNonCallbacks)
    : mAllowNonCallbacks(allowNonCallbacks),
      mSendingMessage(false),
      mPolling(false),
      mEpollRebuildRequired(false),
      mNextRequestSeq(0),
      mResponseIndex(0),
      mNextMessageUptime(LLONG_MAX) {
      
    mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));
 
     ...
}

從Looper的建構式里可以找到答案,mWakeEventFd本質上是一個eventfd,至于什么是eventfd,這里只能說是eventfd是Linux 2.6提供的一種系統呼叫,它可以用來實作事件通知,更具體的內容需要各位讀者自行查閱學習檔案了,

既然有發送端,那么必然有接收端,接收端在哪呢?

void Looper::awoken() {
#if DEBUG_POLL_AND_WAKE
    ALOGD("%p ~ awoken", this);
#endif

    uint64_t counter;
    TEMP_FAILURE_RETRY(read(mWakeEventFd.get(), &counter, sizeof(uint64_t)));
}

可以看到,awoken函式里的內容很簡單,只是做了一個讀取的動作,它并不關系讀到的具體值是啥,為什么要這樣設計呢,我們得結合awoken函式在哪里呼叫去分析,

awoken函式在Looper的pollInner函式里呼叫,pollInner函式里有一條陳述句

int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

它在這里起到阻塞的作用,如果沒有呼叫nativeWake函式,epoll_wait將一直等待寫入事件,直到超時為止,

如此,便回到我們文章一開始提出的問題了,

Looper.loop是一個死回圈,拿不到需要處理的Message就會阻塞,那在UI執行緒中為什么不會導致ANR?

首先,我們需要明確一點,Handler中到底有沒有阻塞?

答案是有!!!那它為什么不會導致ANR呢?

這得從ANR產生的原理說起,

ANR的本質也是一個Message,這一點很關鍵,我們拿前臺服務的創建來舉例,前臺服務創建時,會發送一個
what值為ActivityManagerService.SERVICE_TIMEOUT_MSG的延時20s的Message,如果Service的創建 作業在上述訊息的延時時間內完成,則會移除該訊息,否則,在Handler正常收到這個訊息后,就會進行服務超時處理,即彈出ANR對話框,

為什么不會ANR,現在各位讀者清楚了嗎?ANR訊息本身就是通過Handler去派發的,Handler阻塞與否與ANR并沒有必然關系,


我們看了MessageQueue是如何加入一條訊息的,接下來,我們來看看它是如何取出一條訊息的,

Message next() {
    //如果訊息回圈已退出并已被釋放,則return
    //如果應用程式在退出后嘗試重新啟動looper,則可能發生這種情況
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
        //將當前執行緒中掛起的所有Binder命令重繪到內核驅動程式,
        //在執行可能會阻塞很長時間的操作之前呼叫此函式非常有用,以確保已釋放任何掛起的物件參考,
        //從而防止行程保留物件的時間超過需要的時間,
            Binder.flushPendingCommands();
        }

        //用于等待下一條可用訊息,使用了linux epoll機制阻塞,不會占用cpu時間
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // 嘗試尋找下一條message
            final long now = SystemClock.uptimeMillis();
            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 {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

            //處理完所有掛起的訊息后,立即處理退出訊息
            if (mQuitting) {
                dispose();
                return null;
            }

            // 下面都是IdleHandler邏輯
            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // No idle handlers to run.  Loop and wait some more.
                mBlocked = true;
                continue;
            }

            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }

        // Run the idle handlers.
        // We only ever reach this code block during the first iteration.
        for (int i = 0; i < pendingIdleHandlerCount; i++) {
            final IdleHandler idler = mPendingIdleHandlers[i];
            mPendingIdleHandlers[i] = null; // release the reference to the handler

            boolean keep = false;
            try {
                keep = idler.queueIdle();
            } catch (Throwable t) {
                Log.wtf(TAG, "IdleHandler threw exception", t);
            }

            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }

        // Reset the idle handler count to 0 so we do not run them again.
        pendingIdleHandlerCount = 0;

        // While calling an idle handler, a new message could have been delivered
        // so go back and look again for a pending message without waiting.
        nextPollTimeoutMillis = 0;
    }
}

next方法里主要做了三件事:
(1)使用nativePollOnce阻塞指定時間,等待下一條訊息的執行,
(2)獲取下一條訊息,并回傳此訊息,
(3)如果訊息佇列為空,則執行IdleHandler,
這里有個新名詞IdleHandler,IdleHandler是可以在 Looper 事件回圈的程序中,當出現空閑的時候,允許我們執行任務的一種機制,MessageQueue中提供了addIdleHandler和removeIdleHandler去添加洗掉IdleHandler,


next方法的第一行有個ptr變數,這個ptr變數是什么含義呢?

MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();
}

mPtr是一個long型變數,它是在MessageQueue的構造方法中,通過nativeInit方法初始化的,

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    if (!nativeMessageQueue) {
        jniThrowRuntimeException(env, "Unable to allocate native queue");
        return 0;
    }

    nativeMessageQueue->incStrong(env);
    return reinterpret_cast<jlong>(nativeMessageQueue);
}

可以看到,ptr的本質是對 jni層的NativeMessageQueue物件的指標的參考,


我們重點來看下nativePollOnce方法,探尋一下Handler中的阻塞機制,nativePollOnce方法最終呼叫的是Looper.cpp中的pollOnce函式,

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) { //一個死回圈
        while (mResponseIndex < mResponses.size()) {
            const Response& response = mResponses.itemAt(mResponseIndex++);
            int ident = response.request.ident;
            if (ident >= 0) {
                int fd = response.request.fd;
                int events = response.events;
                void* data = response.request.data;
#if DEBUG_POLL_AND_WAKE
                ALOGD("%p ~ pollOnce - returning signalled identifier %d: "
                        "fd=%d, events=0x%x, data=%p",
                        this, ident, fd, events, data);
#endif
                if (outFd != nullptr) *outFd = fd;
                if (outEvents != nullptr) *outEvents = events;
                if (outData != nullptr) *outData = data;
                return ident;
            }
        }

        if (result != 0) {
#if DEBUG_POLL_AND_WAKE
            ALOGD("%p ~ pollOnce - returning result %d", this, result);
#endif
            if (outFd != nullptr) *outFd = 0;
            if (outEvents != nullptr) *outEvents = 0;
            if (outData != nullptr) *outData = nullptr;
            return result;
        }

        result = pollInner(timeoutMillis);
    }
}

函式里有個關于mResponses的while回圈,我們從java層呼叫的暫時不用管它,它是ndk的handler處理邏輯,我們重點來看pollInner函式,

int Looper::pollInner(int timeoutMillis) {
    // 根據下一條訊息的到期時間調整超時,
    if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {
        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
        int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);
        if (messageTimeoutMillis >= 0
                && (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {
            timeoutMillis = messageTimeoutMillis;
        }
    }

    // 默認觸發喚醒事件,POLL_WAKE == -1
    int result = POLL_WAKE;
    mResponses.clear();
    mResponseIndex = 0;

    // We are about to idle.
    mPolling = true;

    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
    //等待寫入事件,寫入事件由awoken函式觸發,timeoutMillis為超時時間,0立即回傳,-1一直等待
    int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

    // No longer idling.
    mPolling = false;

    // Acquire lock.
    mLock.lock();

   ...

    // Check for poll error.
    if (eventCount < 0) {
        if (errno == EINTR) {
            goto Done;
        }
        ALOGW("Poll failed with an unexpected error: %s", strerror(errno));
       //POLL_ERROR == -4
        result = POLL_ERROR;
        goto Done;
    }

    // Check for poll timeout.
    if (eventCount == 0) {
        //POLL_TIMEOUT == -3,epoll超時會走此分支
        result = POLL_TIMEOUT;
        goto Done;
    }

    // Handle all events.
    for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        if (fd == mWakeEventFd.get()) {
            if (epollEvents & EPOLLIN) {
                //將eventfd里的數值取出,無實際含義,只是為了清空epoll事件和eventfd里的資料
                awoken();
            } else {
                ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
            }
        } else {
            //不會走到此分支,忽略它
            ssize_t requestIndex = mRequests.indexOfKey(fd);
            if (requestIndex >= 0) {
                int events = 0;
                if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
                if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
                if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
                if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
                pushResponse(events, mRequests.valueAt(requestIndex));
            } else {
                ALOGW("Ignoring unexpected epoll events 0x%x on fd %d that is "
                        "no longer registered.", epollEvents, fd);
            }
        }
    }
Done: ;

    // 中間省略的代碼不做探究,和ndk的handler實作有關
     ...
    return result;
}

可以看到,pollInner函式主要的邏輯是使用epoll_wait去讀取喚醒事件,它有一個最大的等待時長,其最大等待時長和下一條訊息的觸發時間有關,

需要注意一下pollInner的回傳值result,它有三種狀態,進入方法默認為POLL_WAKE,表示觸發喚醒事件,
接下來通過對epoll_wait回傳值的判斷,它可能會變更為另兩種狀態,epoll_wait回傳值為0,表示epoll_wait因超時而結束等待,result值設為POLL_TIMEOUT;epoll_wait回傳值為-1,表示epoll_wait因系統中斷等原因而結束等待,result值設為POLL_ERROR,但不管result值設為哪一個,都會導致pollOnce退出死回圈,然代碼流程回到java層的next方法中,去取得下一個Message物件,

因此,nativePollOnce簡單意義上的理解,它就是一個阻斷器,可以將當前執行緒阻塞,直到超時或者因需立即執行的新訊息入隊才結束阻塞,

各位讀者,看到這里,大家再回過頭去想想文章的第一個問題該怎么回答吧,

Looper

Handler 機制中,我們還剩最后一個一個模塊沒有分析———— Looper,我們先從官方定義來看起:

* Class used to run a message loop for a thread.  Threads by default do
* not have a message loop associated with them; to create one, call
* {@link #prepare} in the thread that is to run the loop, and then
* {@link #loop} to have it process messages until the loop is stopped.

概括一下:

Looper是一個用于在執行緒中回圈遍歷訊息的類,默認情況下,執行緒沒有與之關聯的訊息回圈;如果要創建一個,請在運行Looper的執行緒中呼叫Looper.prepare(),然后使用Looper.loop()讓它處理訊息直到回圈停止,

上面的定義提到了兩個比較關鍵的方法,我們一個一個來看,

Looper.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物件,并把它放到sThreadLocal里,其中sThreadLocal是一個ThreadLocal類,

ThreadLocal類又是什么呢?

多執行緒訪問同一個共享變數的時候容易出現并發問題,特別是多個執行緒對一個變數進行寫入的時候,為了保證執行緒安全,一般使用者在訪問共享變數的時候需要進行額外的同步措施才能保證執行緒安全性,ThreadLocal是除了加鎖這種同步方式之外的一種保證一種規避多執行緒訪問出現執行緒不安全的方法,當我們在創建一個變數后,如果每個執行緒對其進行訪問的時候訪問的都是執行緒自己的變數,這樣就不會存在執行緒不安全問題,

因此,使用ThreadLocal能夠保證不同執行緒的Looper物件都有一個獨立的副本,它們彼此獨立,互不干擾,


Looper.looper()

public static void loop() {
    //獲取當前執行緒的Looper物件
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    //獲取與Looper關聯的messagequeue
    final MessageQueue queue = me.mQueue;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    // Allow overriding a threshold with a system prop. e.g.
    // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
    final int thresholdOverride =
            SystemProperties.getInt("log.looper."
                    + Process.myUid() + "."
                    + Thread.currentThread().getName()
                    + ".slow", 0);

    boolean slowDeliveryDetected = false;

    for (;;) {
        //進入死回圈,不斷去從MessageQueue中去拉取Message
        Message msg = queue.next(); // next方法我們已經在MessageQueue中做了分析
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        // Make sure the observer won't change while processing a transaction.
        final Observer observer = sObserver;

        ...

        final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
        final long dispatchEnd;
        Object token = null;
        if (observer != null) {
            token = observer.messageDispatchStarting();
        }
        long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
        try {
            //注意這里,msg.target是一個handler物件,這個方法最終呼叫了handler的dispatchMessage
            //去做訊息分發
            msg.target.dispatchMessage(msg);
            if (observer != null) {
                observer.messageDispatched(token, msg);
            }
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } catch (Exception exception) {
            if (observer != null) {
                observer.dispatchingThrewException(token, msg, exception);
            }
            throw exception;
        } finally {
            ThreadLocalWorkSource.restore(origWorkSource);
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

        //回收Message,上文中有做過分析
        msg.recycleUnchecked();
    }
}

loop方法主要的作業是:建立一個死回圈,不斷的通過呼叫MessageQueue中的next方法獲取下一個訊息,并最終通過取得的訊息關聯的handler去完成訊息的分發,

總結

最后,我們再來理一理 HandlerMessageMessageQueueLooper四者的關系和職責,

  • Handler : 訊息分發的管理者,負責獲取訊息、封裝訊息、派發訊息以及處理訊息,
  • Message :訊息的載體類,
  • MessageQueue :訊息的容器,負責按訊息的觸發時間對訊息入隊出隊,以及在合適的時間喚醒或休眠訊息佇列,
  • Looper : 訊息分發的執行者,負責從訊息佇列中拉去訊息并交給handler去執行,

為了更好的理解它們的關系,拿現實生活中的場景來舉個例子:

Handler是快遞員,負責收快遞,取快遞,查快遞以及退回快遞,

Message是快遞包裹,message的target屬性就是收件地址,而延時訊息就是收件人預約了派送時間,
希望在指定的時間上門派送,

MessageQueue是菜鳥驛站,要對快遞進行整理并擺放在合適的位置,

Looper是一個24小時不休息的資本家,他總是不停的在看菜鳥驛站有沒有需要派送的快遞,一有快遞就立馬取
出然后壓榨快遞員去派送,

最后,我們用一張四者之間的流程圖來結束整篇文章:

為方便學習了解到更多的Handler知識點,特此我將一些 Android 開發相關的學習檔案、面試題、Android 核心筆記等檔案進行了整理,并上傳之我GitHub專案中,如有需要參考的可以直接去我 CodeChina 地址:https://codechina.csdn.net/u012165769/Android-T3 ,希望能幫助到大家學習提升,



轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/297170.html

標籤:其他

上一篇:Netty——實作Android客戶端長連接

下一篇:在位元組跳動客戶端作業是什么體驗?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more