面試中 - Handler引發的那些靈魂拷問

努力完善中。。。o(╥﹏╥)o 如果還有遇到別的問題的同學評論區打上,我會補上的。也歡迎糾錯!

如果對Handler源碼不夠了解可以看看這個:Handler源碼學習記錄(java層、native層)
模仿Handler原理,使用eventfd+epoll實現Handler基礎功能的小案例 -> gayhub地址(MessageQueueDemo)

1 Handler是什麼?

android提供的線程切換工具類。主要的作用是通過handler實現從子線程切換回主線程進行ui刷新操作。


1.1 爲什麼Handler能實現線程切換?

在創建Handler的時候需要傳入目標線程的Looper。(沒有傳入Looper默認拿當前線程的Looper,如果當前線程也沒有準備好Looper會拋異常)
而當sendMessage的時候,會將當前的Handler對象賦值給Message中的target變量。並將該Message存到傳入目標線程Looper的MessageQueue中
當Looper消費Message的時候便會拿到Message中的taeget執行dispatchMessage(msg)方法,從而實現線程切換。


1.2 爲什麼主線程才能刷新ui,子線程不可以?

‘android的ui刷新並不是線程安全的’ 所以必須要有一個線程專門來做這件事情,那就是主線程。
刷新ui的時候會檢查當前線程是否爲主線程,如果不是會拋異常。

view在更新的時候由於可能會發生大小、位置等變化,會執行requestLayout來告訴父View自己要更新layout。
然後父View也會一層層調用requestLayout,最終去到ViewRootImpl#requestLayout,在其requestLayout中
會進行線程檢查。

//ViewRootImpl#requestLayout
@Override
public void requestLayout() {
   
       
        if (!mHandlingLayoutInLayoutRequest) {
   
       
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

//ViewRootImpl#checkThread
void checkThread() {
   
       
    if (mThread != Thread.currentThread()) {
   
       
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

嚴格來說,子線程也不是不可以刷新ui。詳細的文章:https://blog.csdn.net/xyh269/article/details/52728861


1.3 爲什麼android要設計成只有在主線程才能刷新ui?

設計成單線程刷新,目的是提高穩定性以及提高性能。
①提高穩定性:由於多線程存在資源共享的問題一旦處理不妥當,會造成數據丟失、重複以及錯亂等問題。單線程能有效降低出錯風險,提高穩定性。
②從①中引申出,如果要處理好多線程的資源共享的問題,就需要增加同步鎖、原子類、線程安全的集合等。這樣必然會存在等待,而且由於ui刷新是非常
頻繁的,如果出現大量而又頻繁的等待,會增加cpu的負擔,從而導致性能下降。


以上是個人觀點,可以看看大佬更加全面的文章:https://blog.csdn.net/qq_39154578/article/details/83782287


2 一個線程有幾個Handler?

可以創建無數個,但是其內部的Looper只會有一個。


2.1 一個線程有幾個Looper?如何保證?

一個線程只有一個Looper;
Looper內部是通過ThreadLocal保證的線程唯一,在Looper.prepare方法的時候創建並set進ThreadLocal。


2.2 爲什麼ThreadLocal能保證Looper線程唯一?

因爲線程維護了一個ThreadLocalMap的容器,該容器專門提供給ThreadLocal獲取數據。
key就是ThreadLocal,value就是需要獲取的數據。

ThreadLocal設計思想
① Thread 內部持有一個全局變量ThreadLocalMap<ThreadLocal(弱引用), T (強引用)> threadLocals
② ThreadLocal內部
get() -> 獲取當前thread對象 -> 獲取threadLocals(並且判空,爲空就爲該thread創建threadLocals)-> map.get(this) 獲取到value
set(T value) -> 獲取當前thread對象 -> 獲取threadLocals(並且判空,爲空就爲該thread創建threadLocals)-> map.set(this, value)
remove() -> 獲取當前thread對象 -> 獲取threadLocals -> map.remove(this)





3 爲什麼主線程可以new Handler?

主線程:
ActivityThread main方法已經幫我們準備好Looper了。
Looper.prepareMainLooper()
Looper.loop() 這個是死循環,是主線程一直存活的關鍵。
所以在主線程可以直接new Handler,而且還可以不用穿Looper參數。(沒有傳入Looper默認拿當前線程的Looper,如果當前線程也沒有準備好Looper會拋異常)




3.1 那麼子線程中如何使用Handler?

Looper.prepare() 準備Looper
Looper.loop() 讓Looper運行起來
Looper.myLooper() 提供給Handler獲得該線程的Looper
由於子線程的Looper創建是在prepare()中,無法保證外部的Hander立即能獲得有效的Looper,所以需要做同步鎖操作。


HandlerThread 封裝好了一切同步操作,也可以用它。


3.2 子線程維持的Looper,無消息應該怎麼處理?

由於Looper.loop()的存在會一直阻塞線程,線程是不會退出的,可能會導致內存泄漏。
需要手動退出 Looper.quit() -> MessageQueue quit()
MessageQueue quit()工作:清空所有Message,nativeWake喚醒等待,next()繼續執行,dispose()


3.3 爲什麼主線程的Looper不需要退出?

如果主線程Looper退出了,整個程序就會退出了。因爲整個app程序都是依靠Looper來分發處理消息,處理生命週期回調的。


4 Handler內存泄漏的原因是什麼?

匿名內部類默認會持有外部類的引用。
內存泄漏:程序在向系統申請分配內存空間後(new),在使用完畢後未釋放。內存泄漏容易造成OOM(內存溢出)。
OOM:我想要使用一個4M的連續空間,但是找不到。系統就會拋出OOM。

MessageQueue維護着所有將要處理的Message,在enqueueMessage的時候,將Handler對象存入Message target變量中。
所有Message的生命有可能會比Handler所在的Activity生命要長,Activity銷燬了,但是Message都還沒執行的話,該Activity
就無法銷燬,導致內存泄漏。


5 Handler的消息阻塞是怎麼實現的?

Handler的實現原理是使用了linux的兩個系統調用實現的:eventfd + epoll
eventfd 負責通知
epoll 負責監聽
首先會通過eventfd系統調用創建一個喚醒fd並且註冊到epoll裏邊。
如果當有延時消息入隊的時候,會根據消息的延時時長爲epoll設置阻塞時長,知道超時epoll自動喚醒,然後返回java層處理消息
如果當有立即執行的消息入隊的時候,會通過寫入數據到喚醒fd從而喚醒epoll,然後返回java層處理消息
如果沒有消息,epoll會一直阻塞,直到被喚醒。






5.1 Looper.loop()會阻塞主線程但爲什麼不會出現ANR?

ANR:應用無響應異常
ANR產生原因:當前的事件沒有機會得到處理
例如:當前在處理一個點擊事件,但是這個點擊事件裏邊的處理是耗時的,主線程就會等待這個耗時的處理。
與此同時另外一個點擊事件發送過來了,新的事件就會被阻塞。當事件超過某一個時間限制(觸摸事件一般是5s)
仍未被執行,就會拋ANR。



所以ANR與Looper.loop()的阻塞是不相關的。並且Looper.loop()的阻塞是爲了保證主線程不退出而設計的。

更詳細的文章:https://blog.csdn.net/qq_32583189/article/details/52253147


6 Handler如果讓Message儘快執行執行?

發送異步消息。
方法:使用Message#setAsynchronous設置

這樣還是不能實現儘快執行的,還需要增加同步屏障(一種特殊的消息)
但是添加以及移除同步屏障的方法,對開發者是不公開的。所以需要使用反射去設置。
記住:使用完之後必須要移除同步屏障,不然同步屏障後面的所有同步消息都無法執行的。
同步屏障的作用:攔截同步屏障後面所有同步消息,只允許異步消息用過。


使用場景介紹:ViewRoomtImpl中爲了讓消息儘快執行大量使用了異步消息

//ViewRoomtImpl#scheduleTraversals
void scheduleTraversals() {
   
                   
        if (!mTraversalScheduled) {
   
                   
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); //添加消息屏障
            ...
        }
    }

//ViewRoomtImpl#unscheduleTraversals
    void unscheduleTraversals() {
   
                   
        if (mTraversalScheduled) {
   
                   
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); //移除消息屏障
            ...
        }
    }

7 Handler有沒有一些機制是有助於程序優化的?

IdleHandler:可以在程序空閒的時候,做一些不太耗時的操作。
使用方法
mHandler.getLooper().getQueue().addIdleHandler
mHandler.getLooper().getQueue().removeIdleHandler


IdleHandler#queueIdle():
返回值用於告訴MessageQueue
true:每次進入阻塞都會調用該IdleHandler,直到該IdleHandler被移除。
false:只調用一次IdleHandler,就被自動移除了。


使用場景:
1、Activity啓動優化,onCreate,onStart,onResume中耗時較短但非必要的代碼可以放到IdleHandler中執行,減少啓動時間。
2、想要一個View繪製完成之後添加其他依賴於這個View的View,當然這個View#post()也能實現,區別就是前者會在消息隊列空閒時執行。

詳細的文章:https://www.jianshu.com/p/1dc73c8ab6a1


8 MessageQueue如何保證線程安全?

MessageQueue內部的很多方法爲了保證線程安全,都增加了對象鎖synchronized (this)。


9 Message怎麼創建?

Message.obtain():使用複用池,減少new Message,防止頻繁GC。(頻繁GC會導致內存抖動(STW) -> 導致卡頓)
設計模式:享元模式(類似例子 recycleview bindView createView)




















發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章