本文做者:魯可——騰訊SNG專項測試組 測試工程師
安全
承上經典隨機Crash之一:線程安全微信
好幾回灰度top一、top2 Crash發生場景:在很日常、頻繁的使用頁面,打開一個界面,立刻返回,piaji,掛了,估計用戶心中有千萬只草泥馬在奔騰,手機QQ究竟怎麼呢?ide
找到開發童鞋,仍是熟悉的對話:工具
請教:這個Crash能復現嗎?開發答:場景就在這,就是復現不了啊oop
這裏有個空指針,那我就加個判空post
我只好去看下開發童鞋的代碼,發現都有一個共性,跟handler postDelayed
有關係,這裏抽取出Crash代碼梗概性能
Post一個匿名Runnable
,延遲500ms學習
跟開發童鞋反覆再三確認,mGLVideoView
置空的地方只有一處,就在onDestroy()
中測試
開發童鞋通常爲了解決內存泄露問題,會在onDestroy
中將變量置空,以讓系統回收,這麼作也理所固然。跟用戶反饋的狀況也吻合,打開界面,立馬返回,會Crash。spa
爲了搞清這個問題的根源,須要對Android消息機制有必定了解,你們能夠搜索下相關文章。
不按套路出牌,碉堡了的用戶是這樣的,如圖所示
弱爆了的我是這樣的,如圖所示
那接下來的事情就好辦了,尋找騰訊手速最快的人,要在500ms以內打開界面,返回,要是他都復現不了,那就真的復現不了,雖然是開個玩笑,但這確實已經不是個機率性問題了,在咱們手速不夠快的狀況下,這類型Crash確實是復現不了,但很顯然這不是解決問題的正確姿式。
過後手段:
加判空
這裏給你們推薦這篇文章:
Android handler.removeCallbacksAndMessages(null)的妙用
http://www.snowdream.tech/2016/02/18/handler-removeCallbacksAndMessages/
好處有:非靜態匿名內部類Runnable
持有外部類會致使內存泄露,remove
掉以較少內存泄露;消除這類空指針Crash的隱患;減小主線程消息隊列的任務,還能提高點性能
然而這些都不能作到事前發現,今天咱們就一塊兒來探討下一些事前的手段,並解密一個我申請的有利於發現同類問題的專利。
請教了作靜態檢查的同窗,在沒有任何上下文環境的狀況下直接使用一個變量,這種空指針檢查很難搞,咱們主要從動態角度上分析。
activity onDestroy
以後handler.post
監控Activity onDestroy
、handler post
操做,強制在onDestroy
以後再post
,就能100%復現這個Crash了
那首先須要尋找Activity與handler之間的聯繫,監控onDestroy
,能夠用hook或者相似LeakCanary的方式,註冊ActivityLifecycleCallbacks
來監聽,但難點在怎麼把handler post
跟Activity onDestroy
創建起聯繫,從開發者的角度來講,這兩個模塊沒有聯繫,Activity徹底不用handler也是能夠的,在Activity的生命週期方法中,沒有哪一個須要帶上handler,Activity中會不會默認隱藏着handler了?
抱着這樣的疑問,我去看了下Activity的源碼(以Android5.0爲準)
果然Activity中會有一個mHandler
看了下這個mHandler
在什麼地方會被用到
只有在runOnUiThread
中會被用到,但開發者本身綁定MainLooper
的handler
跟這個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調用關係圖
根據上面的圖,能夠看出sendMessageDelayed
和sendMessageAtTime
是很是重要的兩個環節,咱們來看下這兩個方法究竟作了啥
在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
時間改長的方案
考慮到主線程作了不少事情,好比需處理繪製UI等一些系統消息,而開發者通常把延時操做都放在了Runnable
裏,這裏咱們只延遲Runnable
通過封裝的消息,並根據調用堆棧作了過濾
考慮到這種Crash容易發生在post短期內,若是開發者原本設置的延遲時間就比較大,若是再加大延遲,會讓消息得不到及時處理,因此咱們對須要加大延遲的時間作了閾值判斷
最終實現的流程圖以下圖所示:
所以,這個專利水到渠成:一種延遲消息分發模擬Crash的方法
最終要達到的效果下圖所示:
衆裏尋他千百度,驀然回首,那人卻在,燈火闌珊處。延遲一個小時,我徹底能夠出去吃個飯、遛個彎,再回來復現這個Crash了。
問:跟當前主線程卡頓監控方案是否有衝突?
答:主線程卡頓監控主要是計算
dispatchMessage
,Dispatching
、Finished
之間的耗時,咱們對dispatchMessage
沒作任何手腳,只是延遲了消息的處理時機。
問:會不會形成卡頓?
答:UI上的不流暢主要是掉幀,每一個消息具體耗時多少,仍是取決於消息自己在作什麼,咱們跟開發者本身把
delayMillis
改長並沒什麼區別。
延遲消息分發SDK已加入NewMonkey隨身版挑戰者模式中,能作到無場景延遲Runnable
類型消息的分發,功能上線短短1天內,就發現了Android QQ 4個Crash,都獲得了開發同窗的迅速fix。
因爲本人能力、精力有限,對Android消息機制遠未啃透,如有紕漏,歡迎斧正,對其餘平臺的消息機制更是一竅不通,若對您有所啓發,深感榮幸。
道高一尺魔高一丈,在降Crash率上,依舊任重而道遠。
更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!