主頁 > 移動端開發 > Android藝術開發探索——10.Android的訊息機制

Android藝術開發探索——10.Android的訊息機制

2021-04-24 13:39:40 移動端開發

1.Handler是Android訊息機制的上層介面,通過它可以輕松的將一個任務切換到Handler所在的執行緒中去執行,
2.更新UI僅僅是Handler的一個特殊使用場景:有時候需要在子執行緒中進行耗時的I/O操作,可能是讀取檔案或訪問網路,當耗時操作完成以后需要在UI上進行一些改變,這時通過Handler就可以將更新UI的操作切換到主執行緒中去執行,
3.Android的訊息機制主要是指Handler的運行機制,Handler運行需要底層的MessageQueue和Looper的支撐,
MessageQueue:訊息佇列,采用單鏈表的資料結構存盤訊息串列,以佇列的形式對外提 供插入和洗掉作業.
Lopper:以無限回圈的形式去查找是否有新訊息,有就處理,沒有就一直等待,
Thread:不是執行緒,作用是可以在每個執行緒中存盤資料,
4.Handler創建的時候會采用當前的Looper來構造訊息回圈系統,Handler內部如何獲取到當前執行緒的Looper?使用ThreadLocal,ThreadLocal可以在不同的執行緒中互不干擾地存盤并提供資料,通過ThreadLocal可以獲取每個執行緒的Looper,
5.執行緒是默認沒有Looper的,如果需要使用Handler就必須為執行緒創建Looper,
6.經常提到的主執行緒也叫UI執行緒,就是ActivityThread,ActivityThread被創建時就會初始化Looper,所以,在主執行緒中默認可以使用Handler,

1.Android的訊息機制概述

1.Android的訊息機制:Handler的運行機制和Handler所附帶的MessageQueue和Looper的作業程序,
2.Handler:將一個任務切換到指定的執行緒中去執行,
3.Android不在主執行緒中進行耗時操作:會導致程式無法回應即ANR,我們需要從服務端拉取一些資訊將其顯示再UI上,這時在子執行緒中拉取,拉取完后不能在子執行緒中直接訪問UI,提供Handler,解決子執行緒中無法訪問UI的矛盾,
4.不允許在子執行緒中訪問UI:Android的UI控制元件不是執行緒安全的,如果在多執行緒中并發訪問可能會導致UI控制元件處于不可預期的狀態,
5.不對UI控制元件的訪問加上鎖機制:上鎖機制會讓UI訪問的邏輯變得復雜;鎖機制會降低UI訪問的效率,阻塞某些執行緒的執行,
最簡單且高效的方法就是采用單執行緒模型來處理UI操作,通過Handler切換一下UI訪問的執行執行緒,

Handler的作業原理

1.Handler創建時會采用當前執行緒的Looper來構建內部的訊息回圈系統,若當前執行緒沒有Looper,就會報錯,
解決:1.為當前執行緒創建Looper 2.在一個又Looper的執行緒中創建Handler

2.Handler創建完畢后,內部的Looper和MessageQueue就可以和Handler一起協同作業,
· 通過Handler的post方法將一個Runnable投遞到Handler內部Looper中去處理
· 通過Handler的send方法發送一個訊息,這個訊息同樣在Looper中去處理
· send方法:呼叫MessageQueue的enqueueMessage方法將這個訊息放入訊息佇列中,Looper發現有新訊息時,就會處理,最終訊息中的Runnable或Handler的handleMessage方法就會被呼叫,Looper是運行在創建Handler所在的執行緒中,這樣Handler中的業務邏輯就會被切換到創建Handler所在的執行緒中取執行了,

2.Android的訊息機制分析

2.1 ThreadLocal的作業原理

1.ThreadLocal是一個執行緒內部的資料存盤類,通過它可以在指定的執行緒中存盤資料,資料存盤后,只有在指定執行緒中可以獲取到存盤資料,用來保證我們在不同執行緒中獲取資料時,拿到的是自己執行緒中存盤的資料
2.當某些資料是以執行緒為作用域并且不同執行緒有不同的資料副本時,可以考慮采用ThreadLocal,
使用場景:
1.Handler需要獲取當前執行緒的Looper,Looper的作用域就是執行緒,并且不同執行緒具有不同的Looper,通過ThreadLocal就可以輕松實作Looper在執行緒中的存取,
2.復雜邏輯下的物件傳遞,如監聽器,有時一個執行緒中的任務過于復雜,如函式呼叫堆疊比較深以及代碼入口的多樣性,這時有需要監聽器能貫穿整個執行緒的執行程序,采用ThreadLocal,采用ThreadLocal可以讓監聽器作為執行緒內的全域物件,每個監聽物件都在自己的執行緒內部存盤,在執行緒內部只要通過get方法就可以獲取到監聽器,
ThreadLocalMap
是ThreadLocal的內部類,其實,ThreadLocal并不存盤資料,只是提供對ThreadLocalMap的操作,ThreadLocalMap才是真正存資料的地方,Thread為每個執行緒創建一個ThreadLocalMap,ThreadLocalMap里面有一個Entry型別的陣列,用來存每個Entry,Entry是ThreadLocalMap里面的一個靜態內部類,它通過自己的建構式將ThreadLocal和資料按照鍵值對的形式存下來,Entry在陣列中如何存盤,是根據ThreadLocal的哈希值與陣列長度-1進行與運算,

        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

用 i 在這個陣列中取值,如果有值并且得到的 key 就是你要設定value的key,就直接設定值然后回傳,有值但 key 為 null 了,就更新為新的,那如果有值,key也不為 null,也不與新的 key 相同呢?那就將 i + 1;直到找到一個符合的位置,
總結:每個執行緒會維護屬于自己執行緒的ThreaedLocalMap,存資料使用到的ThreadLocal是要存資料的鍵,根據這個鍵在不同的執行緒中的ThreadLocalMap取到不同的值,形成資料隔離,

ThreadLocal的get方法:

    public T get() {
        Thread t = Thread.currentThread(); //先拿到當前執行緒 t
        ThreadLocalMap map = getMap(t); //再通過 t 取到當前執行緒中的ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this); //在ThreadLocalMap中通過 this 也就是ThreadLocal取到對應的 e
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;  //呼叫 e.value 取到想要的值
                return result;
            }
        }
        return setInitialValue();
    }

set方法:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

2.2 訊息佇列的作業原理

訊息佇列在Android中指的是MessageQueue,MessageQueue主要有兩個操作:插入和讀取,對應的方法分別為enqueueMessage和next,
enqueueMessage:往訊息佇列中插入一條訊息
next:從訊息佇列中取出一條訊息并將其從訊息佇列中移除

2.2.1 enqueueMessage

    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }

        synchronized (this) {
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }

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

1.從頭到尾遍歷這個單鏈表,將 msg.next 設定為 null,再將 msg 放到鏈表的最末尾
2.特殊設定了 when 的話,會找到合適的位置將其插入
3.單鏈表是有順序的,它是按照處理時間順序從近到遠排序
enqueueMessage的主要操作就是單鏈表的插入

2.2.2 next

取的動作是發生在 Looper 的 loop 中,它呼叫的是 MessageQueue 中的 next() 方法

 Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        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.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                //當訊息的Handler為空時,則查詢異步訊息
                if (msg != null && msg.target == null) {
                 //當查詢到異步訊息,則立刻退出回圈
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        //當異步訊息觸發時間大于當前時間,則設定下一次輪詢的超時時長
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        // 獲取一條訊息,并回傳
                        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; //成功地獲取MessageQueue中的下一條即將要執行的訊息
                    }
                } else {
                    // No more messages.
                    //沒有訊息
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                //訊息正在退出,回傳null
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    //沒有idle handlers 需要運行,則回圈并等待
                    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.
            //只有第一次回圈時,會運行idle handlers,執行完成后,重置pendingIdleHandlerCount為0.
            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.
            //重置idle handler個數為0,以保證不會再次重復運行
            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.
            //當呼叫一個空閑handler時,一個新message能夠被分發,因此無需等待可以直接查詢pending message.
            nextPollTimeoutMillis = 0;
        }
    }

next方法是一個無限回圈的方法,若訊息佇列中沒有訊息,那么next方法會一直阻塞在這里,有新訊息時,next方法會回傳這條訊息并將其從單鏈表中移除,

2.3 Looper的作業原理

Looper在Android的訊息機制中扮演著訊息回圈的角色,會不停的從MessageQueue中查看是否有新訊息,有新訊息就處理,否則就會一直阻塞在那里,Looper 負責不斷的呼叫 MessageQueue 的 next() 方法取出訊息并交給 Handler 處理,

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed); //創建一個MessageQueue即訊息佇列
        mThread = Thread.currentThread(); //將當前執行緒的物件保存起來
    }

Looper.prepare()在每個執行緒只允許執行一次,通過Looper.prepare()為當前執行緒創建一個Looper,通過Looper.loop()來開啟訊息回圈,

new Thread("Thread#2"){
	@Override
	public void run(){
		Looper.prepare();
		Handler handler = new Handler();
		Looper.loop();
	};
}.start();
    public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        //創建Looper物件,并保存到當前執行緒的TLS區域
        sThreadLocal.set(new Looper(quitAllowed));
    }

prepareMainLooper()方法,該方法主要在ActivityThread類中使用,

    @Deprecated
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

getMainLooper()方法,通過它可在任何一個地方獲取到主執行緒的Looper,
quit():直接退出Looper
quitSafely():設定一個退出標記,把訊息佇列中的已有訊息處理完畢后安全退出

public void quit() {
        mQueue.quit(false);
    }

public void quitSafely() {
        mQueue.quit(true);
    }

Loop()
只有呼叫了loop后,訊息回圈才會真正起作用

public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        if (me.mInLoop) {
            Slog.w(TAG, "Loop again would have the queued messages be executed"
                    + " before this one completed.");
        }

        me.mInLoop = true;
        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 (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            //唯一跳出回圈的方式:MessageQueue的next方法回傳了null

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            // Make sure the observer won't change while processing a transaction.
            final Observer observer = sObserver;

            final long traceTag = me.mTraceTag;
            long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
            long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
            if (thresholdOverride > 0) {
                slowDispatchThresholdMs = thresholdOverride;
                slowDeliveryThresholdMs = thresholdOverride;
            }
            final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
            final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

            final boolean needStartTime = logSlowDelivery || logSlowDispatch;
            final boolean needEndTime = logSlowDispatch;

            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

            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.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);
                }
            }
            if (logSlowDelivery) {
                if (slowDeliveryDetected) {
                    if ((dispatchStart - msg.when) <= 10) {
                        Slog.w(TAG, "Drained");
                        slowDeliveryDetected = false;
                    }
                } else {
                    if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                            msg)) {
                        // Once we write a slow delivery log, suppress until the queue drains.
                        slowDeliveryDetected = true;
                    }
                }
            }
            if (logSlowDispatch) {
                showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }
  1. loop方法是一個死回圈,唯一跳出回圈的方式:MessageQueue的next方法回傳了null
  2. Looper的quit方法被呼叫時,Looper就會呼叫MessageQueue的quit或quitSafely方法通知訊息佇列退出,當訊息佇列被標記為退出狀態時,它的next方法就會回傳null,(Looper必須退出,否則loop方法會無限回圈下去)
  3. loop方法會呼叫MessageQueue的next方法獲取新訊息,next是一個阻塞操作,當沒有訊息時,next方法會一直阻塞在那里,也導致loop方法一直阻塞在那里
  4. MessageQueue的next方法回傳了新訊息,Looper會處理這條訊息,msg.target.dispatchMessage(msg),msg.target是發送這條訊息的Handler物件,這樣Handler發送的訊息最終又交給它的dispatchMessage方法來處理,Handler的dispatchMessage方法是在創建Handler時所使用的Looper中執行的,這樣將代碼邏輯切換到指定的執行緒中去執行了,

2.4 Handler的作業原理

Handler的作用是投遞訊息和處理訊息的,它會系結一個Looper,一個執行緒可以有多個 Handler,但只會有一個Looper

 public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0);
    }


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

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

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

Handler發訊息,向訊息佇列中插入了一條訊息,MessageQueue的next方法會回傳這條訊息給Looper,Looper收到訊息后開始處理,最終訊息由Looper交由Handler處理,即Handler的dispatchMessage方法會被呼叫,這時Handler進入處理訊息的階段,

    public void dispatchMessage(@NonNull Message msg) {
    	//檢查Message的callback是否為null
        if (msg.callback != null) { 
        	//不為null就通過handleCallback來處理訊息
    	    //Message的callback是一個Runnable物件
    	    //實際上就是Handler的post方法所傳遞的Runnable引數
            handleCallback(msg);
        } else {
            //檢查mCallback是否為null
            if (mCallback != null) {
            //不為null就呼叫mCallback的handleMessage方法處理訊息
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            //最后呼叫Handler的handleMessage方法處理具體的訊息
            handleMessage(msg);
        }
    }

handleCallback

    private static void handleCallback(Message message) {
        message.callback.run();
    }

Callback是個介面:

    /**
     * Callback interface you can use when instantiating a Handler to avoid
     * having to implement your own subclass of Handler.
     */
    public interface Callback {
        /**
         * @param msg A {@link android.os.Message Message} object
         * @return True if no further handling is desired
         */
        boolean handleMessage(@NonNull Message msg);
    }
  1. 通過Callback可以創建Handler物件:Handler handler = new Handler(callback)
  2. Callback:可以用來創建一個Handler的實體但并不需要派生Handler的子類
    一般創建Handler最常見的方式:派生一個Handler的子類并重寫其handleMessage方法來處理具體的訊息,
  3. Handler的默認構造方法 public Handler(),它會通過多載最終呼叫到這個構造器:
    public Handler(@Nullable 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();  //Handler中的Looper通過Looper.myLooper()系結
        if (mLooper == null) {
        //如果當前執行緒沒有Looper,會拋出這個例外,
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

在這里插入圖片描述

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

標籤:其他

上一篇:uniapp全域彈窗類似自定義uni.showModal思路總結

下一篇:Recovery系統的框架結構說明及常用的客制化修改流程

標籤雲
其他(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