【騰訊Bugly乾貨分享】經典隨機Crash之二:Android消息機制

本文做者:魯可——騰訊SNG專項測試組 測試工程師
安全

背景

承上經典隨機Crash之一:線程安全微信

問題的模型

好幾回灰度top一、top2 Crash發生場景:在很日常、頻繁的使用頁面,打開一個界面,立刻返回,piaji,掛了,估計用戶心中有千萬只草泥馬在奔騰,手機QQ究竟怎麼呢?ide

找到開發童鞋,仍是熟悉的對話:工具

  1. 請教:這個Crash能復現嗎?開發答:場景就在這,就是復現不了啊oop

  2. 這裏有個空指針,那我就加個判空post

我只好去看下開發童鞋的代碼,發現都有一個共性,跟handler postDelayed有關係,這裏抽取出Crash代碼梗概性能

Post一個匿名Runnable,延遲500ms學習

跟開發童鞋反覆再三確認,mGLVideoView置空的地方只有一處,就在onDestroy()測試

開發童鞋通常爲了解決內存泄露問題,會在onDestroy中將變量置空,以讓系統回收,這麼作也理所固然。跟用戶反饋的狀況也吻合,打開界面,立馬返回,會Crash。spa

爲了搞清這個問題的根源,須要對Android消息機制有必定了解,你們能夠搜索下相關文章。

不按套路出牌,碉堡了的用戶是這樣的,如圖所示

弱爆了的我是這樣的,如圖所示

那接下來的事情就好辦了,尋找騰訊手速最快的人,要在500ms以內打開界面,返回,要是他都復現不了,那就真的復現不了,雖然是開個玩笑,但這確實已經不是個機率性問題了,在咱們手速不夠快的狀況下,這類型Crash確實是復現不了,但很顯然這不是解決問題的正確姿式。

解決問題的思路

過後手段:

  1. 加判空

  2. 這裏給你們推薦這篇文章:

    Android handler.removeCallbacksAndMessages(null)的妙用
    http://www.snowdream.tech/2016/02/18/handler-removeCallbacksAndMessages/

好處有:非靜態匿名內部類Runnable持有外部類會致使內存泄露,remove掉以較少內存泄露;消除這類空指針Crash的隱患;減小主線程消息隊列的任務,還能提高點性能

然而這些都不能作到事前發現,今天咱們就一塊兒來探討下一些事前的手段,並解密一個我申請的有利於發現同類問題的專利。

請教了作靜態檢查的同窗,在沒有任何上下文環境的狀況下直接使用一個變量,這種空指針檢查很難搞,咱們主要從動態角度上分析。

一、 在activity onDestroy以後handler.post

監控Activity onDestroyhandler post操做,強制在onDestroy以後再post,就能100%復現這個Crash了

那首先須要尋找Activity與handler之間的聯繫,監控onDestroy,能夠用hook或者相似LeakCanary的方式,註冊ActivityLifecycleCallbacks來監聽,但難點在怎麼把handler postActivity onDestroy創建起聯繫,從開發者的角度來講,這兩個模塊沒有聯繫,Activity徹底不用handler也是能夠的,在Activity的生命週期方法中,沒有哪一個須要帶上handler,Activity中會不會默認隱藏着handler了?

抱着這樣的疑問,我去看了下Activity的源碼(以Android5.0爲準)

果然Activity中會有一個mHandler

看了下這個mHandler在什麼地方會被用到

只有在runOnUiThread中會被用到,但開發者本身綁定MainLooperhandler跟這個mHandler沒有關係。

這種方法須要對Activity Handler兩大核心模塊找到一種關聯,並作一種高精度的手術,限於本人能力有限,一時陷入了困境。

二、 控制消息的時機

既然無法找到Activity Handler的關聯,就只好從消息機制自己着手。

剛開始咱們想到的方法,把這種消息從消息隊列裏取出來,等待時機,而後再從新插入消息隊列

那第一步就須要把這種消息取出來,咱們先來看看源碼是怎麼作的

loop()中會經過next()獲取一個消息,若是能獲取到,則經過dispatchMessage()分發消息,接下來咱們看看next()是怎麼獲取消息的

next獲取了當前系統時間,若到了消息執行時間,則返回消息

這裏必定會有疑問,msg.when是怎麼設置的?消息是如何插入隊列的?

next()從消息隊列獲取一個消息,沒法精準到具體的消息,其實咱們還能夠參考removeMessages的實現,經過反射來取出消息,若是remove的時機過晚,也會致使這個消息已經被消費了,若是remove錯了,致使丟消息,簍子就捅大了。總之,咱們必須搞清楚消息入隊列的過程。

發送消息主要有sendXXX,postXXX兩大類方法,因爲Runnable也會被封裝成Message

其實這裏面也會有個坑:Callback類型Message的what是0,你們有興趣也能夠學習下

看過post (runnable)sendMessage過程後,我畫了一個postXXX、sendXXX調用關係圖

根據上面的圖,能夠看出sendMessageDelayedsendMessageAtTime是很是重要的兩個環節,咱們來看下這兩個方法究竟作了啥

sendMessageDelayed中會用系統開機總時間+dalayMillis,因此傳入sendMessageAtTime的值是相對於系統啓動的絕對值

再來看queue.enqueueMessage的過程

when賦值給了msg.when,這下能解釋next()msg.when是如何得來的問題,到這裏,您應該清楚了,原來插入消息隊列的順序是根據msg.when大小來插入的。

前面說到when傳入的是一個絕對值,那上面爲啥有when==0的判斷,那何時when會爲0呢?當把一個消息強制插入到隊列首的位置,會傳入0

若是咱們要延遲那個消息的處理時機,只需改動這個絕對值就能夠了,咱們決定經過hook sendMessageDelayed,將延遲時間delayMillis改長,若是您看到這裏,是否是以爲方案其實很簡單?確實是的,若是我一上來就告訴您這麼作,那這個問題就很簡單了,其實中間也是踩了一些坑,然而知道爲何要這麼作,彷佛更重要,也更有趣。

到此,您已經清楚Android是如何插入消息的了,您要是願意,徹底能夠把所有消息hook住了,隨意改uptimeMillis,那您已經掌握了玩弄消息順序於股掌之中的技術。

問題的解決方案

最終綜合安全性、穩定性等方面的考慮,咱們採用了將delayMillis時間改長的方案

  1. 考慮到主線程作了不少事情,好比需處理繪製UI等一些系統消息,而開發者通常把延時操做都放在了Runnable裏,這裏咱們只延遲Runnable通過封裝的消息,並根據調用堆棧作了過濾

  2. 考慮到這種Crash容易發生在post短期內,若是開發者原本設置的延遲時間就比較大,若是再加大延遲,會讓消息得不到及時處理,因此咱們對須要加大延遲的時間作了閾值判斷

最終實現的流程圖以下圖所示:

所以,這個專利水到渠成:一種延遲消息分發模擬Crash的方法

最終要達到的效果下圖所示:

衆裏尋他千百度,驀然回首,那人卻在,燈火闌珊處。延遲一個小時,我徹底能夠出去吃個飯、遛個彎,再回來復現這個Crash了。

問:跟當前主線程卡頓監控方案是否有衝突?

答:主線程卡頓監控主要是計算dispatchMessageDispatchingFinished之間的耗時,咱們對dispatchMessage沒作任何手腳,只是延遲了消息的處理時機。

問:會不會形成卡頓?

答:UI上的不流暢主要是掉幀,每一個消息具體耗時多少,仍是取決於消息自己在作什麼,咱們跟開發者本身把delayMillis改長並沒什麼區別。

效果

延遲消息分發SDK已加入NewMonkey隨身版挑戰者模式中,能作到無場景延遲Runnable類型消息的分發,功能上線短短1天內,就發現了Android QQ 4個Crash,都獲得了開發同窗的迅速fix。

因爲本人能力、精力有限,對Android消息機制遠未啃透,如有紕漏,歡迎斧正,對其餘平臺的消息機制更是一竅不通,若對您有所啓發,深感榮幸。

道高一尺魔高一丈,在降Crash率上,依舊任重而道遠。


更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:

騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!

相關文章
相關標籤/搜索