5分鐘了解Handler錯誤使用場景

2021-01-08 網易

  

  碼個蛋(codeegg)第 821 次推文

  作者:刁兒郎當

  博客:https://www.jianshu.com/p/43e21be8d849

  碼妞看世界

  

  小燕子葉

  寫在前面

  Handler的相關博客太多了,隨便一搜都一大把,但是基本都是上來就貼源碼,講姿勢,短時間不太好弄明白整體的關係,和流程,本文就以生活點餐的例子再結合源碼原理進行解析。希望對你有一點幫助。

  來,咱們進入角色。

  Hander,Looper,MessageQueue,Message的全程協作的關係就好比一個餐廳的整體運作關係:

  Handler —— 點餐員
Looper —— 後廚廚師長。
MessageQueue —— 訂單打單機。
Message —— 一桌一桌的訂單。

  接下來我們回顧下我們餐廳點餐的場景,餐廳點餐分為標準點餐和特殊點餐,我們分解來看。

  標準流程

  

  首先進入一家店,通過點餐員點餐把數據提交到後廚打單機。

  

  然後廚師長一張一張的拿起訂單,按照點餐的先後順序,交代後廚的廚師開始製作。

  

  製作好後上菜,並標記已做好的訂單。

  

  特殊流程

  

  訂單為延遲訂單,比如客人要求30分鐘後人齊了再製作,這時會把該訂單按時間排序放到訂單隊列的合適位置,並通過SystemClock.uptimeMillis()定好鬧鈴。至於為什麼用uptimeMillis是因為該時間是系統啟動開始計算的毫秒時間,不受手動調控時間的影響。

  

  如果打單機中全是延遲訂單,則下令給後廚廚師休息,並在門口貼上免打擾的牌子(needWake),等待鬧鈴提醒,如有新的即時訂單進來並且發現有免打擾的牌子,則通過nativeWake()喚醒廚師再開始製作上菜。

  

  但是為了提升店鋪菜品覆蓋,很多相鄰的店鋪都選擇合作經營,就是你可以混搭旁邊店的餐到本店吃,此時只需點餐員提交旁邊店的訂單即可,這樣旁邊店的廚師長就可以通過打單機取出訂單並進行製作和上菜。

  

  總結

  一家店可以有多個點餐員,但是廚師長只能有一個。打單機也只能有一個。

  映射到以上場景中,一家店就好比一個Thread,而一個Thread中可以有多個Handler(點餐員),但只能有一個Looper(廚師長),一個MessageQueue(打單機),和多個Message(訂單)。

  看看整個流程

  根據以上的例子我們類比看下源碼,充分研究下整個機制的流程,和實現原理。

  Looper的工作流程

  ActivityThread.main();//初始化入口
1. Looper.prepareMainLooper(); //初始化
Looper.prepare(false); //設置不可關閉
Looper.sThreadLocal.set(new Looper(quitAllowed)); //跟線程綁定
1.1.Looper.mQueue = new MessageQueue(quitAllowed); //Looper和MessageQueue綁定
1.2.Looper.mThread = Thread.currentThread();
2. Looper.loop();
2.1.myLooper().mQueue.next(); //循環獲取MessageQueue中的消息
nativePollOnce(); //阻塞隊列
native -> pollInner() //底層阻塞實現
native -> epoll_wait();
2.2.Handler.dispatchMessage(msg);//消息分發

  myLooper().mQueue.next()實現原理

  

  通過myLooper().mQueue.next() 循環獲取MessageQueue中的消息,如遇到同步屏障 則優先處理異步消息.

  

  同步屏障即為用Message.postSyncBarrier()發送的消息,該消息的target沒有綁定Handler。在Hnandler中異步消息優先級高於同步消息。

  

  可通過創建new Handler(true)發送異步消息。ViewRootImpl.scheduleTraversals方法就使用了同步屏障,保證UI繪製優先執行。

  

  Handler.dispatchMessage(msg)實現原理

  

  優先回調msg.callback。

  

  其次回調handler構造函數中的callback。

  

  最後回調handler handleMessage()。

  

  Hander發送消息的流程

  1.Handler handler = new Handler();//初始化Handler
1.Handler.mLooper = Looper.myLooper();//獲取當前線程Looper。
2.Handler.mQueue = mLooper.mQueue;//獲取Looper綁定的MessageQueue對象。

  2.handler.post(Runnable);//發送消息
sendMessageDelayed(Message msg, long delayMillis);
sendMessageAtTime(Message msg, long uptimeMillis);
Handler.enqueueMessage();//Message.target 賦值為this。
Handler.mQueue.enqueueMessage();//添加消息到MessageQueue。

  MessageQueue.enqueueMessage()方法實現原理

  

  如果消息隊列被放棄,則拋出異常。

  

  如果當前插入消息是即時消息,則將這個消息作為新的頭部元素,並將此消息的next指向舊的頭部元素,並通過needWake喚醒Looper線程。

  

  如果消息為異步消息則通過Message.when長短插入到隊列對應位置,不喚醒Looper線程。

  

  經常有人問為什麼主線程的Looper阻塞不會導致ANR?

  

  首先我們得知道ANR是主線程5秒內沒有響應。

  

  什麼叫5秒沒有響應呢?Android系統中所有的操作均通過Handler添加事件到事件隊列,Looper循環去隊列去取事件進行執行。如果主線程事件反饋超過了5秒則提示ANR。

  

  如果沒有事件進來,基於Linux pipe/epoll機制會阻塞loop方法中的queue.next()中的nativePollOnce()不會報ANR。

  

  對於以上的例子來說,ANR可以理解為用戶進行即時點餐後沒按時上菜(當然未按時上菜的原因很多,可能做的慢(耗時操作IO等),也可能廚具被佔用(死鎖),還有可能廚師不夠多(CPU性能差)等等。。。),顧客發起了投訴,或差評。但如果約定時間還沒到,或者當前沒人點餐,是不會有差評或投訴產生的,因此也不會產生ANR。

  

  以上的所有內容均圍繞原理,源碼,接下來我們舉幾個特殊場景的例子

  1. 為什么子線程不能直接new Handler()?

  new Thread(new Runnable() {
@Override
public void run() {
Handler handler = new Handler();
}
}).start();

  
以上代碼會報以下下錯誤

  java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:207)
at android.os.Handler.<init>(Handler.java:119)
at com.example.test.MainActivity$2.run(MainActivity.java:21)
at java.lang.Thread.run(Thread.java:919)

  通過報錯提示「not called Looper.prepare()」 可以看出提示沒有調用Looper.prepare(),至於為什麼我們還得看下源碼。

  public Handler(Callback callback, boolean async) {
...省略若干代碼

  //通過 Looper.myLooper()獲取了mLooper 對象,如果mLooper ==null則拋異常
mLooper = Looper.myLooper();
if (mLooper == null) {
//可以看到異常就是從這報出去的
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}

  public static @Nullable Looper myLooper() {
//而myLooper()是通過sThreadLocal.get()獲取的,那sThreadLocal又是個什麼鬼?
return sThreadLocal.get();
}

  //可以看到sThreadLocal 是一個ThreadLocal<Looper>對象,那ThreadLocal值從哪賦值的?
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

  //sThreadLocal 的值就是在這個方法裡賦值的
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));
}

  

  2. 為什麼主線程可以直接new Handler?

  

  //我們看下ActivityMain的入口main方法,調用了 Looper.prepareMainLooper();
public static void main(String[] args) {
...
Looper.prepareMainLooper();
...
}

  //看到這一下就明白了,原來主線程在啟動的時候默認就調用了prepareMainLooper(),而在這個方法中調用了prepare()。
//提前將sThreadLocal 進行賦值了。
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}

  3. Handler為什麼會內存洩露?

  

  public class HandlerActivity extends AppCompatActivity {
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};

  @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
handler.sendEmptyMessageDelayed(1,5000);
}
}

  

  當以上代碼寫完後編譯器立馬會報黃並提示:

  「this handler should be static or leaks might occur...Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler as a static class; In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler; Make all references to members of the outer class using the WeakReference object.」

  
大致意思就說「由於這個處理程序被聲明為一個內部類,它可以防止外部類被垃圾回收。如果處理程序正在對主線程以外的線程使用Looper或MessageQueue,則不存在問題。如果處理程序正在使用主線程的Looper或MessageQueue,則需要修復處理程序聲明,如下所示:將處理程序聲明為靜態類;並且通過WeakReference引用外部類」。

  

  說了這麼一大堆,簡單意思就是說以上這種寫法,默認會引用HandlerActivity,當HandlerActivity被finish的時候,可能Handler還在執行不能會回收,同時由於Handler隱式引用了HandlerActivity,導致了HandlerActivity也不能被回收,所以內存洩露了。

  

  我們來寫一種正確的寫法

  public class HandlerActivity extends AppCompatActivity {
MyHandler handler = new MyHandler(this);

  @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
handler.sendEmptyMessageDelayed(1,5000);
}
private static class MyHandler extends Handler{
private WeakReference<HandlerActivity> activityWeakReference;

  public MyHandler(HandlerActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}

  @Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}
}

  

  4. 補充個小知識點,啥是隱式引用?

  

  其實我們寫的非靜態內部類和非靜態匿名內部類,在編譯器編譯過程中,隱式幫我們傳入了this這個參數,這也是為什麼,我們平時在方法中能使用this這個關鍵字的原因,了解了隱式引用,那麼為什麼它會是導致內存洩漏?這裡又得說明一下,虛擬機的垃圾回收策略。

  

  垃圾回收機制:Java採用根搜索算法,當GC Roots不可達時,並且對象finalize沒有自救的情況下,才會回收。也就是說GC會收集那些不是GC roots且沒有被GC roots引用的對象,就像下邊這個圖一樣。

  

  

  垃圾回收.png

  

  注意:

  不是所有內部類都建議使用靜態內部類,只有在該內部類中的生命周期不可控的情況下,建議採用靜態內部類。其他情況還是可以使用非靜態內部類的。

  好了Handler的介紹到此結束了,篇幅略長,希望給你帶來了一點幫助。

  

特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺「網易號」用戶上傳並發布,本平臺僅提供信息存儲服務。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相關焦點

  • Handler 使用詳解
    Handler 機制處理的4個關鍵對象四、 Handler常用方法五、子線程更新UI 異常處理六、主線程給子線程發送消息的方法七、主線程發送消息給子線程的例子八、子線程給主線程發送消息的方法九、 主、子 線程 互發消息方法十、子線程方法中調用主線程更新UI的方法十一、移除Handler 發送的消息方法一、Handler 簡介在了解
  • 你真的了解Handler嗎
    ❝提到handler,大家都想到些什麼呢,切換線程?延時操作?那麼你是否了解「IdleHandler,同步屏障,死循環」的設計原理?以及由Handler機制衍生的「IntentService,BlockCanary」?
  • Android開發:HandlerThread是什麼?
    是否了解HandlerThread的使用?是否了解HandlerThread的原理?考察的知識點HandlerThread的使用HandlerThread的原理考生應該如何回答1、首先,我們可以圍繞HandlerThread是什麼,什麼場景下會用HandlerThread,HandlerThread怎麼用這幾個話題來概述一下。
  • Android Handler 由淺入深源碼全解析
    只聞起名還未使用過的同學別擔心,我們先說說他的做用,再分析源碼的實現。        先思考這樣一個場景,我們知道在android中主線程中是不能做複雜的耗時操作。然而可不可以有一種機制是主線程通知子線程來做某件事呢?        注意: 這裡說的是通知子線程來做某件事,不是說在主線程中另起一個子線程來做某件事。兩者是有區別的。
  • 面試必備:異步 Handler 十大必問!
    子線程中怎麼使用 Handler除了上面 Handler 的基本原理,子線程中如何使用 Handler 也是一個常見的問題。子線程中使用 Handler 需要先執行兩個操作:Looper.prepare 和 Looper.loop。為什麼需要這樣做呢?Looper.prepare 和 Looper.loop 都做了什麼事情呢?
  • Flask框架鉤子函數使用方式及應用場景分析
    全局的場景包含:共享session的鑑權函數、請求黑白名單過濾、根據endpoint進行請求j等。藍圖場景包含api的請求必填欄位校驗,是否json請求校驗,請求的token校驗等。當訪問應用出錯時,根據錯誤響應碼,進行一些定製化的操作,如返回一個可愛的404頁面。
  • Android中Handler問題匯總【面試必備】
    任何一個handler在使用sendMessage或者post時候,都是先構造一個Message,並把自己放到message中,然後把Message放到對應的Looper的MessageQueue,Looper通過控制MessageQueue來獲取message執行其中的handler或者runnable。
  • 關於Handler你所需要知道的一切
    Handler消息機制首先有四個對象我們必須要了解一下Handler、Looper、ThreadLocal還有MessageQueue。雖然我們的大多數使用Handler的場景,都是我們在子線程做了一下耗時的操作(IO或者資料庫),當子線程執行完以後我們可能需要更新UI,這個時候我們用Handler來處理(sendMessage()或者post())就把子線程切換到了UI線程了。
  • Handler原理,這一篇就夠了
    前言hanlder的大概原理,可能很多人不知道,至少不清楚,網上很多文章也是到處粘貼,聽別說handler把Message發送到MessageQueue裡面去,Looper通過死循環,不斷從MessageQueue裡面獲取Message處理消息,因為Mesage.target就是當前hanlder,所以最後轉到handleMessage()方法中去處理,整個流程是這樣。
  • Android開發 面試必問的Handler消息機制
    最近項目提測了也閒了下來看到Handler就想起面試必問,Handler機制相信大家每個人面試的時候都被問到吧,就來總結一下看看,話不多說先看流體圖:這個流體圖應該已經把整個Handler消息機制的流程都涵蓋了,應該算是很直觀了吧,首先最外層我寫了Thread.currentThread(),這說明了一個線程裡有且僅有一個Looper,所以大家應該注意如果在子線程中使用
  • Web Worker 的內部構造以及 5 種你應當使用它的場景
    這一次我們將剖析 Web Worker:對它進行簡單概述後,我們將分別討論不同類型的 Worker 以及它們內部組件的運作方法,同時也會以場景為例說明它們各自的優缺點。在文章的最後,我們將講解最適合使用 Web Worker 的 5 個場景。我們在 之前的文章 中已經詳盡地討論了 JavaScript 的單線程運行機制,對此你應當已經瞭然於胸。
  • 重學 Android 之 Handler 機制
    機制的使用幾乎隨處可見,作為面試中的常客,我們真的了解 handler 嗎?想必很多同學會想,當然了,handler 機制不就是 Looper 、handler  MessageQueue 嗎?下面拋磚引玉,我們先來看看這些問題為什么子線程中不可以直接 new Handler() 而主線程中可以?為什麼建議使用 Message.obtain() 來創建 Message 實例?Looper 在主線程中死循環,為啥不會 ANR ?
  • 再談PHP錯誤與異常處理
    本文章分5個部分介紹我的異常處理的理解:一、異常與錯誤的概述二、ERROR的級別三、PHP異常處理中的黑科技四、巧妙的捕獲錯誤和異常五、自定義異常處理和異常嵌套六、PHP7中的異常處理一、異常與錯誤的概述  PHP中什麼是異常:  程序在運行中出現不符合預期的情況,允許發生
  • golang中Context的使用場景
    golang中Context的使用場景context在Go1.7之後就進入標準庫中了。它主要的用處如果用一句話來說,是在於控制goroutine的生命周期。當一個計算任務被goroutine承接了之後,由於某種原因(超時,或者強制退出)我們希望中止這個goroutine的計算任務,那麼就用得到這個Context了。
  • PHP 中的錯誤和異常處理
    E_ERROR 和 E_RECOVERABLE_ERROR 級別的錯誤在 PHP7 之前是不能被捕獲到的,也就是說,你無法使用 try...catch... 這樣的語句捕獲到這種級別的錯誤,但不管是 PHP7 還是 PHP5 對於未捕獲的異常依然是一個致命錯誤在 PHP5 中<?
  • 攔截PHP各種異常和錯誤,發生致命錯誤時進行報警(一)
    種種以上,都是因為大家關閉了錯誤信息,並且未將錯誤、異常記錄到日誌,導致那些隨機發生的錯誤很難追蹤。這樣矛盾就來了,即不要顯示錯誤,又要追蹤錯誤,這如何實現了?以上問題都可以通過PHP的錯誤、異常機制及其內建函數'set_exception_handler','set_error_handler','register_shutdown_function' 來實現'set_exception_handler' 函數 用於攔截各種未捕獲的異常,然後將這些交給用戶自定義的方式進行處理'set_error_handler' 函數可以攔截各種錯誤
  • Mybatis之TypeHandler簡析
    typeHandlerTypeHandler處理資料庫類型和Java類型之間的轉換,先看下這個是怎麼使用的。默認註冊的時候會註冊一個UnknownTypeHandler,這個handler是處理java中的Object的變量的。
  • Android多線程:手把手帶你深入Handler源碼分析(下)
    前言    在Android開發的多線程應用場景中,Handler機制
  • 在前端業務場景下的設計模式
    接下來介紹一種在前端業務中使用代理模式的場景。在前端開發的移動端頁面調試中,由於在移動端沒有對應的開發者面板,除了使用chrome://inspect/#devices和safari開發工具之外,我們還可以使用vConole或erdua來完成瀏覽頁面結構、查看 console 等調試需求。
  • python爬蟲基礎之urllib的使用
    這篇文章主要介紹了python爬蟲基礎之urllib的使用,幫助大家更好的理解和使用python,感興趣的朋友可以了解下一