主頁 > 移動端開發 > 一篇文章扒掉“橋梁Handler”的底褲

一篇文章扒掉“橋梁Handler”的底褲

2022-03-22 07:38:43 移動端開發

典型的生產者-消費者模式
Android跨行程要掌握的是Binder, 而同一行程中最重要的應該就是Handler 訊息通信機制了,我這么說,大家不知道是否認同,如果認同,還希望能給一個關注哈,

什么是Handler?

Handler主要用于異步訊息的處理:當發出一個訊息之后,首先進入一個訊息佇列,發送訊息的[函式]即刻回傳,而另外一個部分在訊息佇列中逐一將訊息取出,然后對訊息進行處理,也就是發送訊息和接收訊息不是同步的處理, 這種機制通常用來處理相對耗時比較長的操作,

Handler特點

  1. 傳遞Message,用于接受子執行緒發送的資料, 并用此資料配合主執行緒更新UI,

    在Android中,對于UI的操作通常需要放在主執行緒中進行操作,如果在子執行緒中有關于UI的操作,那么就需要把資料訊息作為一個Message物件發送到訊息佇列中,然后,由Handler中的handlerMessage方法處理傳過來的資料資訊,并操作UI,當然,Handler物件是在主執行緒中初始化的,因為它需要系結在主執行緒的訊息佇列中,

    類sendMessage(Message msg)方法實作發送訊息的操作, 在初始化Handler物件時重寫的handleMessage方法來接收Message并進行相關操作,

  2. 傳遞Runnable物件,用于通過Handler系結的訊息佇列,安排不同操作的執行順序,

    Handler物件在進行初始化的時候,會默認的自動系結訊息佇列,利用類post方法,可以將Runnable物件發送到訊息佇列中,按照佇列的機制按順序執行不同的Runnable物件中的run方法,

Handler怎么用?

public class HandlerActivity extends AppCompatActivity {
    private static final String TAG = "HandlerActivity";
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        testSendMessage();
    }

    public void testSendMessage() {
         Handler handler = new MyHandler(this);
         Message message = Message.obtain();
         message.obj = "test handler send message";
         handler.sendMessage(message);
    }
    
    //注1: 為什么要用靜態內部???
    static class MyHandler extends Handler {
        WeakReference<AppCompatActivity> activityWeakReference; // 注2:為何要用弱參考???
        public MyHandler(AppCompatActivity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            Log.d(TAG, (String) msg.obj);
        }
    }
}

Handler原始碼怎么讀?

從使用方式的場景,咱們一步一步的探究里面是怎么實作的,還有上面的標注的兩點,在后面我都會介紹的,各位客官聽我慢慢道來,首先,看下四大金剛關系圖,文字表述再多,不如一張圖來的直接,

“四大金剛”
通過上圖就可以簡單看出Handler、MessageQueue、Message、Looper 這四者是怎么樣互相持有對方的,大概可以了解訊息的傳遞,

下面我們先來一張時序圖,看下訊息是怎么一步步發送出來的,


此刻,應該要開車了,前方高能!!!

  1. 進入的是Handler.sendMessage 方法
public final boolean sendMessage(@NonNull Message msg) {
    return sendMessageDelayed(msg, 0);
}
  1. 接下來繼續呼叫Handler.sendMessageDelayed方法
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
  1. 接著走Handler.sendMessageAtTime 方法,這里面就要用到MessageQueue 物件了,此處說明一下,這個mQueue 是在哪里獲取到的,是在Handler 構造方法里,此處貼圖,從圖中可以看出mLooper=Looper.myLooper() mQueue=mLooper.mQueue Handler 中的MessageQueue 是Looper 中持有的MessageQueue 物件 ,


注1 為啥要用靜態內部類---->如果我們使用Handler 類,沒有用static 關鍵字修飾的話,則會輸出Log: The following Handler class should be static or leaks might occur: 會提示你可能會引起記憶體泄漏,因此在注1 處我用了static 修飾,

好,這里就說這么多,接著開車:

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);
}
  1. 接著時序圖上的流程走,此時要進入到MessageQueue.enqueueMessage 方法中,該方法就是將msg 物件存入到MessageQueue 佇列中,注意此處,將該handler 物件賦值給了msg.target,這個后面會用到的,很關鍵,
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); //3,即將進入MessageQueue.enqueueMessage 方法,
}
  1. 接著來看MessageQueue.enqueueMessage 方法,該方法就是按照時間的順序插入到Message 這個鏈表結構的資料物件中去,
boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) { //4. 后面說明,這個也就是四大金剛圖里的msg.target 所持有的Handler 物件,
        throw new IllegalArgumentException("Message must have a target.");
    }

    synchronized (this) {
        ...
        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 {
            // 鏈表的插入操作,不太熟悉的可以看看資料結構,(此處是根據時間來排序的)
            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); //畫重點,此處喚醒等待的next 方法,
        }
    }
    return true;
}

此時,一條訊息就相當于入隊了, MessageQueue 從名稱來看是佇列,實際上,使用的還是Message.next 指標來進行操作的,也即是鏈表的操作,訊息的入隊完成,后面將會介紹該訊息是怎么發送出去的,

  1. Loop.loop方法,敲重點,省略了部分代碼,只關注核心代碼,這里用到了死回圈,不停的獲取Message 物件,獲取到之后直接呼叫Message.target 變數所持有的Handler 物件,然后呼叫Handler.dispatchMessage 方法,這樣就完成了訊息的分發,
public static void loop() {
    final Looper me = myLooper();
    ...
    final MessageQueue queue = me.mQueue;
    ...
    for (;;) {
        Message msg = queue.next(); // might block   //7.通過MessageQueue.next()方法獲取Message物件,
        ...
        final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        final long end;
        try {
            msg.target.dispatchMessage(msg);
            end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        ...
        msg.recycleUnchecked();
    }
}

7-8. MessageQueue.next() 方法獲取Message 物件,

Message next() {
    ...
    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) { //死回圈
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        nativePollOnce(ptr, nextPollTimeoutMillis);   // 5: 避免了阻塞的關鍵點,釋放資源,處于等待,疑點:處于等待,肯定需要一個東西來喚醒它,上面第5步分析enqueueMessage的時候有行代碼if (needWake) {
            nativeWake(mPtr); //畫重點,此處喚醒等待的next 方法,
        } ,

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) { //******此條件可以先不看,因為通過Handler 發送的訊息target 都會持有Handler,該邏輯不會觸發,訊息同步屏障的時候會優先觸發該邏輯,
                // 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) { //查找當前的msg 物件,
                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;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }
         ...
        nextPollTimeoutMillis = 0;
    }
}
  1. Handler.dispatchMessage 方法,此處有判斷,如果在Activity中使用view.post方法呼叫的時候,就會走到handleCallback 回呼中,通過sendMessagexxx函式發送訊息的就會走到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);
    }
}

該方法會會將msg 物件發送到客戶端定義Handler 的地方,重寫的handleMessage 方法,至此,Handler 發送訊息的流程大致介紹完成,

總結

Handler 發送訊息的時候,在Handler.enqueueMessage 方法中,將該Handler 物件添加到Message中的target 屬性中,這樣就完成了Message 持有Handler 的操作,為最后Message.target.dispatchMessage 做了保證,然后將該Message 物件放入到MessageQueue中的Message.next 中去,完成了訊息鏈表的添加;而這個MessageQueue 是Looper 中所持有的物件,這樣就可以通過Looper類通過對MessageQueue.next()---->Message.next()--->Message.target.dispatchMessage(msg)完成了訊息的分發,

知識點補充

  1. Looper 物件是怎么new 出來的?

    上圖看出是在應用程式行程的ActivityThread 類中的main() 函式中呼叫了Looper.prepareMainLooper() 方法,就new 出來了主執行緒中的Looper.

    上圖也看出,這個Looper.prepareMainLooper()方法是系統呼叫的,開發者不能再次呼叫了,否則會拋出例外,

    prepare這個方法真正的new Looper 了,接著來看看Looper 的建構式


此處創建了MessageQueue, Handler 中的MessageQueue 就是這塊創建的,

  1. 為什么將Looper 保存在ThreadLocal 中?

ThreadLocal:執行緒的變數副本,每個執行緒隔離.我的理解就是,ThreadLocal 內部使用了當前執行緒為Key,需要存盤的物件為Value,通過字典保存起來的,這樣客戶端在獲取的時候,當前執行緒就只會獲取一份保存的Value.回到Looper中,就可以知道一個執行緒里按理說就會只有一個Looper,

  1. Message 為什么推薦使用obtain() 方式獲取Message物件,而不推薦使用new Message()?

這里涉及到池的技術的應用: Message中維護了一個訊息池,訊息使用完就會回收,減少物件創建和銷毀的開銷;java 當中的執行緒池也是用到了該思想,

  1. 同步屏障:

同步屏障機制的作用,是讓這個繪制訊息得以越過其他的訊息,優先被執行,系統中UI繪制會使用到同步屏障,開發中基本用不到,核心代碼: 先設定一個target=null 的訊息,插入到訊息鏈表的頭部,

然后在MessageQueue.next 中 優先查找同步屏障中的訊息asyncHronous 設定為true的異步訊息,

  1. Handler為什么會導致記憶體泄漏以及解決方案?

Handler導致記憶體泄漏一般發生在發送延遲訊息的時候,當Activity關閉之后,延遲訊息還沒發出,那么主執行緒中的MessageQueue就會持有這個訊息的參考,而這個訊息是持有Handler的參考,而handler作為匿名內部類持有了Activity的參考,所以就有了以下的一條參考鏈,
解決:1.使用靜態內部類,如果要呼叫Activity中的方法,就可以在靜態內部類中設定一個
WeakReference activityWeakReference; 參考,

2.在Activity銷毀的時候,即onDestory()方法中呼叫handler.removeCallbacks,移除runnable,

結尾

OK,本次的Android進階技術之Handler到此就全部寫完了,希望喜歡的朋友不要吝嗇你的贊,你的評論,點贊,收藏就是對我最大的支持,記得關注我哦,咱們文章每日都會更新,感謝大家的觀看,

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

標籤:Android

上一篇:如何使用Google Analytics Universal Analytics增強型電子商務

下一篇:如何使用Google Analytics Universal Analytics增強型電子商務

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