Handler的使用陷阱

前言

沈陽剛剛入職,最近在閱讀之前同事的代碼法瑟,因為他的架構設計中使用了Handler模型冀膝,所以再次總結一下Handler的使用問題,這也面試的常見問題之一霎挟。

本文中可能涉及到一些源碼相關的問題窝剖,建議先了解一下Handler源碼。

正文

問題一:構建Handler異常

Handler與Looper酥夭,MessageQueue協(xié)作赐纱,是Android線程切換的主要手段之一脊奋,官方推薦開發(fā)者自己指定Handler的執(zhí)行線程,如果你使用的Handler構造函數(shù)是無參構造方法:


在這里插入圖片描述

標記有刪除線的方法疙描,表示該方法已經廢棄诚隙,如果在開發(fā)中遇到了這樣的方法一定要了解具體api廢棄的原因,這樣可以幫助我們避免很多意想不到的問題淫痰,例如:

Default constructor associates this handler with the Looper for the current thread. If this thread does not have a looper, this handler won't be able to receive messages so an exception is thrown.
Deprecated
Implicitly choosing a Looper during Handler construction can lead to bugs where operations are silently lost (if the Handler is not expecting new tasks and quits), crashes (if a handler is sometimes created on a thread without a Looper active), or race conditions, where the thread a handler is associated with is not what the author anticipated. Instead, use an java.util.concurrent.Executor or specify the Looper explicitly, using Looper.getMainLooper, {link android.view.View#getHandler}, or similar. If the implicit thread local behavior is required for compatibility, use new Handler(Looper.myLooper()) to make it clear to readers.

大概的意思就是Handler默認的構造方法會使用Looper.myLooper()作為Handler的執(zhí)行線程最楷,如果當前線程沒有調用過Looper.prepare(),就會拋出異常:

public Handler(@Nullable Callback callback, boolean async) {
       ...
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
       ...
    }

所以為了避免這個異常待错,我們最好顯式的指定Looper籽孙。除了提前調用Looper.prepare(),我們還可以使用HandlerThread幫助我們創(chuàng)建Looper火俄,然后再綁定到Handler上:

val handlerThread = HandlerThread("test")
handlerThread.start()
val handler = Handler(handlerThread.looper)

到現(xiàn)在我還有疑問犯建,HandlerThread僅僅是為當前線程創(chuàng)建了Looper,為什么不叫LooperThread瓜客?既然和Handler的關聯(lián)如此密切适瓦,為什么getThreadHandler()沒有開放?希望有心得朋友留言幫我解開這個問題谱仪。

@NonNull
public Handler getThreadHandler() {
    if (mHandler == null) {
        mHandler = new Handler(getLooper());
    }
    return mHandler;
}

問題二:內存泄漏

這個問題網上已經有了很多的討論和分享玻熙,這個簡單的總結一下問題的原因:

如果Handler作為Context(Activity/Service等等)的內部類,默認內部類持有外部類的引用疯攒;
通過Handler.post或Handler.postDelay執(zhí)行一個任務之前嗦随,Context已經退出(Activity.finish), MessageQueue仍然持有這個任務,在任務都完成之前敬尺,Activity因為強引用關系枚尼,無法立即回收,導致內存泄漏或是Context使用不當導致程序崩潰砂吞;

解決辦法也非常的簡單署恍,首先通過靜態(tài)內部類的方式解決強引用的問題,如果需要引用關系蜻直,通過WeakReference修改Handler與Activity的引用關系為弱引用盯质,然后每次使用Handler的時候也去判斷一下是否Activity已經被銷毀了,防止意外操作:

    /**
     * Kotlin default is static inner class
     * */
    open class SafeHandler(looper: Looper, activity: Activity) : Handler(looper) {

        private val weakActivity = WeakReference<Activity>(activity)

        override fun dispatchMessage(msg: Message) {
            // activity has destroyed, do not need dispatchMessage
            if (isActivityDestroyed()) {
                return
            }

            super.dispatchMessage(msg)
        }

        override fun sendMessageAtTime(msg: Message, uptimeMillis: Long): Boolean {
            // activity has destroyed, do not need sendMessageAtTime
            if (isActivityDestroyed()) {
                return false
            }
            return super.sendMessageAtTime(msg, uptimeMillis)
        }

        private fun isActivityDestroyed(): Boolean {
            // activity has recycled, do not need dispatchMessage
            if (weakActivity.get() == null) {
                return true
            }

            // activity has destroyed, do not need dispatchMessage
            if (weakActivity.get()!!.isFinishing || weakActivity.get()!!.isDestroyed) {
                return true
            }

            return false
        }

    }

    private val mSafeHandler = object : SafeHandler(Looper.getMainLooper(), this) {

        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            // do some thing
        }

    }

上面的代碼用的是Kotlin袭蝗,默認內部類就是靜態(tài)的唤殴,Java靜態(tài)內部類請使用:

static class SafeHandler extends Handler 

除此之外,我們還應該在Activity銷毀或Handler使用結束時到腥,手動調用removeCallbacksAndMessages方法朵逝,及時清除掉殘留的任務:

handler.removeCallbacksAndMessages(null)

問題三:任務執(zhí)行時間的不穩(wěn)定性

首先要明確一點:Handler的任務調度是單線程模型。對于某些延遲任務乡范,例如我們需要500毫秒后配名,執(zhí)行某個任務啤咽,可以使用Handler:

Handler.postDelay({ /**do some thing*/}, 500)

但是這個500毫秒準確嗎?答案是非常不穩(wěn)定渠脉,因為Handler是單線程模型宇整,如果是中間等待的500ms中,Handler開啟了一個耗時任務芋膘,只有它結束了鳞青,Looper才能繼續(xù)繼續(xù)取出下一個Message,例如下面的代碼:

fun startPost() {
    // 10s task
    mHandler.post {
        Log.e("lzp", "Sleep Task start")
        Thread.sleep(10_000)
    }

    // 1s delay task
    mHandler.postDelayed({
        Log.e("lzp", "Delay Task start")
    }, 1000)
}

按照我的期望为朋,應該是1s后就立刻執(zhí)行延時任務臂拓,但是我不小心之前跑了一個10s的任務,看一下log:


在這里插入圖片描述

中間正好間隔了10s习寸,等10s任務結束胶惰,Looper發(fā)現(xiàn)延時的任務的時間戳早就過了,于是立刻執(zhí)行了延時任務霞溪。這不符合我的期望孵滞,為了解決這個問題,必須將10s任務放入其他線程中鸯匹,停止占用當前線程的資源坊饶。

除了單線程模型的問題,Handler的調度還會收到系統(tǒng)的影響殴蓬,如果app在后臺幼东,Handler可能會執(zhí)行慢或不執(zhí)行,所以Handler也并不適合后臺任務科雳。

所以總結一句話:Handler適合執(zhí)行短小精悍,反應迅速脓杉,執(zhí)行時間要求不精確的任務糟秘,它是一個調度角色,而非執(zhí)行角色球散。

總結

以上三個問題就是今天跟大家分享的內容尿赚,Handler作為Android線程調度老大哥,個人覺得它的解耦方式有點類似于觀察者模式蕉堰,閱讀代碼時反復橫跳凌净,對于閱讀體驗還是影響挺大的,所以Handler雖然好用屋讶,但是不要濫用冰寻。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市皿渗,隨后出現(xiàn)的幾起案子斩芭,更是在濱河造成了極大的恐慌轻腺,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件划乖,死亡現(xiàn)場離奇詭異贬养,居然都是意外死亡,警方通過查閱死者的電腦和手機琴庵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門误算,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人迷殿,你說我怎么就攤上這事儿礼。” “怎么了贪庙?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵蜘犁,是天一觀的道長。 經常有香客問我止邮,道長这橙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任导披,我火速辦了婚禮屈扎,結果婚禮上,老公的妹妹穿的比我還像新娘撩匕。我一直安慰自己鹰晨,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布止毕。 她就那樣靜靜地躺著模蜡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪扁凛。 梳的紋絲不亂的頭發(fā)上忍疾,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音谨朝,去河邊找鬼卤妒。 笑死,一個胖子當著我的面吹牛字币,可吹牛的內容都是我干的则披。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼洗出,長吁一口氣:“原來是場噩夢啊……” “哼士复!你這毒婦竟也來了?” 一聲冷哼從身側響起翩活,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤判没,失蹤者是張志新(化名)和其女友劉穎蜓萄,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體澄峰,經...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡嫉沽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了俏竞。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绸硕。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖魂毁,靈堂內的尸體忽然破棺而出玻佩,到底是詐尸還是另有隱情,我是刑警寧澤席楚,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布咬崔,位于F島的核電站,受9級特大地震影響烦秩,放射性物質發(fā)生泄漏垮斯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一只祠、第九天 我趴在偏房一處隱蔽的房頂上張望兜蠕。 院中可真熱鬧,春花似錦抛寝、人聲如沸熊杨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晶府。三九已至,卻和暖如春钻趋,著一層夾襖步出監(jiān)牢的瞬間郊霎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工爷绘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人进倍。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓土至,卻偏偏與公主長得像,于是被迫代替她去往敵國和親猾昆。 傳聞我的和親對象是個殘疾皇子陶因,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容