Android App 優化之 ANR 詳解

為了便于閱讀, 應邀將Android App性能優化系列, 轉移到掘金原創上來.
掘金的新出的"收藏集"功能可以用來做系列文集了.

今天先來聊聊ANR.

1, 你碰到ANR了嗎

在App使用過程中, 你可能遇到過這樣的情況:

恭喜你, 這就是傳說中的ANR.

1.1 何為ANR

ANR全名Application Not Responding, 也就是"應用無響應". 當操作在一段時間內系統無法處理時, 系統層面會彈出上圖那樣的ANR對話框.

1.2 為什么會產生ANR

在Android里, App的響應能力是由Activity Manager和Window Manager系統服務來監控的. 通常在如下兩種情況下會彈出ANR對話框:

  • 5s內無法響應用戶輸入事件(例如鍵盤輸入, 觸摸屏幕等).
  • BroadcastReceiver在10s內無法結束.

造成以上兩種情況的首要原因就是在主線程(UI線程)里面做了太多的阻塞耗時操作, 例如文件讀寫, 數據庫讀寫, 網絡查詢等等.

1.3 如何避免ANR

知道了ANR產生的原因, 那么想要避免ANR, 也就很簡單了, 就一條規則:

不要在主線程(UI線程)里面做繁重的操作.

這里面實際上涉及到兩個問題:

  1. 哪些地方是運行在主線程的?
  2. 不在主線程做, 在哪兒做?

稍后解答.

2, ANR分析

2.1 獲取ANR產生的trace文件

ANR產生時, 系統會生成一個traces.txt的文件放在/data/anr/下. 可以通過adb命令將其導出到本地:

$adb pull data/anr/traces.txt .復制代碼

2.2 分析traces.txt

2.2.1 普通阻塞導致的ANR

獲取到的tracs.txt文件一般如下:

如下以GithubApp代碼為例, 強行sleep thread產生的一個ANR.

----- pid 2976 at 2016-09-08 23:02:47 -----
Cmd line: com.anly.githubapp  // 最新的ANR發生的進程(包名)...DALVIK THREADS (41):
"main" prio=5 tid=1 Sleeping| group="main" sCount=1 dsCount=0 obj=0x73467fa8 self=0x7fbf66c95000| sysTid=2976 nice=0 cgrp=default sched=0/0 handle=0x7fbf6a8953e0| state=S schedstat=( 0 0 0 ) utm=60 stm=37 core=1 HZ=100| stack=0x7ffff4ffd000-0x7ffff4fff000 stackSize=8MB| held mutexes=at java.lang.Thread.sleep!(Native method)- sleeping on <0x35fc9e33> (a java.lang.Object)at java.lang.Thread.sleep(Thread.java:1031)- locked <0x35fc9e33> (a java.lang.Object)at java.lang.Thread.sleep(Thread.java:985) // 主線程中sleep過長時間, 阻塞導致無響應.at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258)- locked  (a com.tencent.bugly.crashreport.crash.c)at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166)  // 產生ANR的那個函數調用- locked  (a java.lang.Class)at com.anly.githubapp.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23)at com.anly.githubapp.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起點at com.anly.githubapp.ui.module.main.MineFragment_ViewBinding$2.doClick(MineFragment_ViewBinding.java:47)at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)at android.view.View.performClick(View.java:4780)at android.view.View$PerformClick.run(View.java:19866)at android.os.Handler.handleCallback(Handler.java:739)at android.os.Handler.dispatchMessage(Handler.java:95)at android.os.Looper.loop(Looper.java:135)at android.app.ActivityThread.main(ActivityThread.java:5254)at java.lang.reflect.Method.invoke!(Native method)at java.lang.reflect.Method.invoke(Method.java:372)at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)@addr=0x12d1e840>@addr=0x12dadc70>0x35fc9e33>0x35fc9e33>復制代碼

拿到trace信息, 一切好說.
如上trace信息中的添加的中文注釋已基本說明了trace文件該怎么分析:

  1. 文件最上的即為最新產生的ANR的trace信息.
  2. 前面兩行表明ANR發生的進程pid, 時間, 以及進程名字(包名).
  3. 尋找我們的代碼點, 然后往前推, 看方法調用棧, 追溯到問題產生的根源.

以上的ANR trace是屬于相對簡單, 還有可能你并沒有在主線程中做過于耗時的操作, 然而還是ANR了. 這就有可能是如下兩種情況了:

2.2.2 CPU滿負荷

這個時候你看到的trace信息可能會包含這樣的信息:

Process:com.anly.githubapp
...
CPU usage from 3330ms to 814ms ago:
6% 178/system_server: 3.5% user + 1.4% kernel / faults: 86 minor 20 major
4.6% 2976/com.anly.githubapp: 0.7% user + 3.7% kernel /faults: 52 minor 19 major
0.9% 252/com.android.systemui: 0.9% user + 0% kernel
...100%TOTAL: 5.9% user + 4.1% kernel + 89% iowait復制代碼

最后一句表明了:

  1. 當是CPU占用100%, 滿負荷了.
  2. 其中絕大數是被iowait即I/O操作占用了.

此時分析方法調用棧, 一般來說會發現是方法中有頻繁的文件讀寫或是數據庫讀寫操作放在主線程來做了.

2.2.3 內存原因

其實內存原因有可能會導致ANR, 例如如果由于內存泄露, App可使用內存所剩無幾, 我們點擊按鈕啟動一個大圖片作為背景的activity, 就可能會產生ANR, 這時trace信息可能是這樣的:

// 以下trace信息來自網絡, 用來做個示例
Cmdline: android.process.acoreDALVIK THREADS:
"main"prio=5 tid=3 VMWAIT
|group="main" sCount=1 dsCount=0 s=N obj=0x40026240self=0xbda8
| sysTid=1815 nice=0 sched=0/0 cgrp=unknownhandle=-1344001376
atdalvik.system.VMRuntime.trackExternalAllocation(NativeMethod)
atandroid.graphics.Bitmap.nativeCreate(Native Method)
atandroid.graphics.Bitmap.createBitmap(Bitmap.java:468)
atandroid.view.View.buildDrawingCache(View.java:6324)
atandroid.view.View.getDrawingCache(View.java:6178)...MEMINFO in pid 1360 [android.process.acore] **
native dalvik other total
size: 17036 23111 N/A 40147
allocated: 16484 20675 N/A 37159
free: 296 2436 N/A 2732復制代碼

可以看到free的內存已所剩無幾.

當然這種情況可能更多的是會產生OOM的異常...

2.2 ANR的處理

針對三種不同的情況, 一般的處理情況如下

  1. 主線程阻塞的
    開辟單獨的子線程來處理耗時阻塞事務.

  2. CPU滿負荷, I/O阻塞的
    I/O阻塞一般來說就是文件讀寫或數據庫操作執行在主線程了, 也可以通過開辟子線程的方式異步執行.

  3. 內存不夠用的
    增大VM內存, 使用largeHeap屬性, 排查內存泄露(這個在內存優化那篇細說吧)等.

3, 深入一點

沒有人愿意在出問題之后去解決問題.
高手和新手的區別是, 高手知道怎么在一開始就避免問題的發生. 那么針對ANR這個問題, 我們需要做哪些層次的工作來避免其發生呢?

3.1 哪些地方是執行在主線程的

  1. Activity的所有生命周期回調都是執行在主線程的.
  2. Service默認是執行在主線程的.
  3. BroadcastReceiver的onReceive回調是執行在主線程的.
  4. 沒有使用子線程的looper的Handler的handleMessage, post(Runnable)是執行在主線程的.
  5. AsyncTask的回調中除了doInBackground, 其他都是執行在主線程的.
  6. View的post(Runnable)是執行在主線程的.

3.2 使用子線程的方式有哪些

上面我們幾乎一直在說, 避免ANR的方法就是在子線程中執行耗時阻塞操作. 那么在Android中有哪些方式可以讓我們實現這一點呢.

3.2.1 啟Thread方式

這個其實也是Java實現多線程的方式. 有兩種實現方法, 繼承Thread 或 實現Runnable接口:

繼承Thread

class PrimeThread extends Thread {long minPrime;PrimeThread(long minPrime) {this.minPrime = minPrime;}public void run() {// compute primes larger than minPrime. . .}
}PrimeThread p = new PrimeThread(143);
p.start();復制代碼

實現Runnable接口

class PrimeRun implements Runnable {long minPrime;PrimeRun(long minPrime) {this.minPrime = minPrime;}public void run() {// compute primes larger than minPrime. . .}
}PrimeRun p = new PrimeRun(143);
new Thread(p).start();復制代碼

3.2.2 使用AsyncTask

這個是Android特有的方式, AsyncTask顧名思義, 就是異步任務的意思.

private class DownloadFilesTask extends AsyncTask{// Do the long-running work in here// 執行在子線程protected Long doInBackground(URL... urls) {int count = urls.length;long totalSize = 0;for (int i = 0; i < count; i++) {totalSize += Downloader.downloadFile(urls[i]);publishProgress((int) ((i / (float) count) * 100));// Escape early if cancel() is calledif (isCancelled()) break;}return totalSize;}// This is called each time you call publishProgress()// 執行在主線程protected void onProgressUpdate(Integer... progress) {setProgressPercent(progress[0]);}// This is called when doInBackground() is finished// 執行在主線程protected void onPostExecute(Long result) {showNotification("Downloaded " + result + " bytes");}
}// 啟動方式
new DownloadFilesTask().execute(url1, url2, url3);復制代碼

3.2.3 HandlerThread

Android中結合Handler和Thread的一種方式. 前面有云, 默認情況下Handler的handleMessage是執行在主線程的, 但是如果我給這個Handler傳入了子線程的looper, handleMessage就會執行在這個子線程中的. HandlerThread正是這樣的一個結合體:

// 啟動一個名為new_thread的子線程
HandlerThread thread = new HandlerThread("new_thread");
thread.start();// 取new_thread賦值給ServiceHandler
private ServiceHandler mServiceHandler;
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);private final class ServiceHandler extends Handler {public ServiceHandler(Looper looper) {super(looper);}@Overridepublic void handleMessage(Message msg) {// 此時handleMessage是運行在new_thread這個子線程中了.}
}復制代碼

3.2.4 IntentService

Service是運行在主線程的, 然而IntentService是運行在子線程的.
實際上IntentService就是實現了一個HandlerThread + ServiceHandler的模式.

以上HandlerThread的使用代碼示例也就來自于IntentService源碼.

3.2.5 Loader

Android 3.0引入的數據加載器, 可以在Activity/Fragment中使用. 支持異步加載數據, 并可監控數據源在數據發生變化時傳遞新結果. 常用的有CursorLoader, 用來加載數據庫數據.

// Prepare the loader.  Either re-connect with an existing one,
// or start a new one.
// 使用LoaderManager來初始化Loader
getLoaderManager().initLoader(0, null, this);//如果 ID 指定的加載器已存在,則將重復使用上次創建的加載器。
//如果 ID 指定的加載器不存在,則 initLoader() 將觸發 LoaderManager.LoaderCallbacks 方法 //onCreateLoader()。在此方法中,您可以實現代碼以實例化并返回新加載器// 創建一個Loader
public Loader onCreateLoader(int id, Bundle args) {// This is called when a new Loader needs to be created.  This// sample only has one Loader, so we don't care about the ID.// First, pick the base URI to use depending on whether we are// currently filtering.Uri baseUri;if (mCurFilter != null) {baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,Uri.encode(mCurFilter));} else {baseUri = Contacts.CONTENT_URI;}// Now create and return a CursorLoader that will take care of// creating a Cursor for the data being displayed.String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("+ Contacts.HAS_PHONE_NUMBER + "=1) AND ("+ Contacts.DISPLAY_NAME + " != '' ))";return new CursorLoader(getActivity(), baseUri,CONTACTS_SUMMARY_PROJECTION, select, null,Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}// 加載完成
public void onLoadFinished(Loader loader, Cursor data) {// Swap the new cursor in.  (The framework will take care of closing the// old cursor once we return.)mAdapter.swapCursor(data);
}復制代碼

具體請參看官網Loader介紹.

3.2.6 特別注意

使用Thread和HandlerThread時, 為了使效果更好, 建議設置Thread的優先級偏低一點:

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);復制代碼

因為如果沒有做任何優先級設置的話, 你創建的Thread默認和UI Thread是具有同樣的優先級的, 你懂的. 同樣的優先級的Thread, CPU調度上還是可能會阻塞掉你的UI Thread, 導致ANR的.

結語

對于ANR問題, 個人認為還是預防為主, 認清代碼中的阻塞點, 善用線程. 同時形成良好的編程習慣, 要有MainThread和Worker Thread的概念的...(實際上人的工作狀態也是這樣的~~哈哈)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/457591.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/457591.shtml
英文地址,請注明出處:http://en.pswp.cn/news/457591.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

微信高級群發接口正文亂碼解決方案

content里面的內空如果含有html標簽的話&#xff0c;需要對內容進行一下轉義。如果里面含有style".."類似于這樣的帶""號的內容的話&#xff0c;就更需要注意了。 foreach ($news as &$item) {foreach ($item as $key > $val){if ($key content){$…

python *args和**kwargs以及序列解包

DAY 8. *args和**kwargs *args&#xff1a;多值元組&#xff0c;**kwargs多值字典&#xff0c;他們是python函數傳參時兩個特殊的參數&#xff0c;args和kwargs并不是強制的&#xff0c;但習慣使用這兩個&#xff0c;如果在函數參數列表中聲明了*args&#xff0c;則允許傳遞任…

解讀直播連麥與點播加密

近年來&#xff0c;直播熱潮持續升溫。有需求就會有變革&#xff0c;直播的相關技術也在不斷更新&#xff0c;為直播行業帶來更好地服務。如&#xff1a;直播連麥與點播加密技術等。 直播連麥&#xff0c;即主播與連麥者通過互動直播中心進行實時互動&#xff0c;信息在云端混流…

血紅蛋白判斷access程序答案_普渡大學開發智能手機應用程序 幫助評估貧血癥情況...

醫生診斷貧血疾病的方法之一&#xff0c;就是通過觀察患者的眼皮&#xff0c;判斷眼皮是否發紅&#xff0c;從而判斷紅細胞的數量。但對醫生來說&#xff0c;面臨的挑戰是&#xff0c;這種簡單的測試不夠精確&#xff0c;無法不從患者身上抽取血樣就能給出診斷。美國普渡大學的…

學習筆記:AC自動機

話說AC自動機有什么用......我想要自動AC機 AC自動機簡介&#xff1a; 首先簡要介紹一下AC自動機&#xff1a;Aho-Corasick automation&#xff0c;該算法在1975年產生于貝爾實驗室&#xff0c;是著名的多模匹配算法之一。一個常見的例子就是給出n個單詞&#xff0c;再給出一段…

python閉包和裝飾器

DAY 9. 閉包和裝飾器 9.1 閉包 閉包就是內部函數對外部函數作用域內變量的引用 可以看出 閉包是針對函數的&#xff0c;還有兩個函數&#xff0c;內部函數和外部函數閉包是為了讓內部函數引用外部函數作用域內的變量的 我們先寫兩個函數 def fun1():print("我是fun1&q…

學歷是銅牌,能力是銀牌,人脈是金牌,思維是王牌

有人工作&#xff0c;有人上學&#xff0c;大家千萬不要錯過這篇文章&#xff0c;能看到這篇文章也是一種幸運&#xff0c;真的受益匪淺&#xff0c;對我有很大啟迪&#xff0c;這篇文章將會改變你我的一生&#xff0c;真的太好了&#xff0c;希望與有緣人分享&#xff0c;也希…

石頭剪刀布python編程_《python核心編程第二版》練習題——游戲:石頭剪刀布

習題里比較有意思的一個題目&#xff0c;實現石頭剪刀布這個游戲&#xff0c;起初設計的時候走彎路了(主要時被習題里那個“盡量少用if判斷”給整暈了)&#xff0c;想的太復雜&#xff0c;后來發現其實非常簡單&#xff0c;完全可以不寫if語句。還是枚舉法&#xff1a;#! /usr/…

SpringMvc面試題

f-sm-1. 講下SpringMvc和Struts1,Struts2的比較的優勢 性能上Struts1>SpringMvc>Struts2 開發速度上SpringMvc和Struts2差不多,比Struts1要高f-sm-2. 講下SpringMvc的核心入口類是什么,Struts1,Struts2的分別是什么 SpringMvc的是DispatchServlet,Struts1的是ActionServl…

python 鴨子類型

DAY 10. 鴨子類型 這個概念來源于美國印第安納州的詩人詹姆斯惠特科姆萊利&#xff08;James Whitcomb Riley,1849-1916&#xff09;的詩句&#xff1a;”When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.”…

thinkphp一句話疑難解決筆記

URL_PATHINFO_DEPR, depr表示 網頁路徑"分隔符",用"-", 有利于seo,注意是從 sername/index.php(開始的)/home-user-login-var-value開始的,pathinfo也支持普通的參數傳值(僅僅支持參數...). 在thinkphp中,有兩個地方使用depr,另一個就是tpl的文件目錄組織分…

python選取特定行_pandas.DataFrame選取/排除特定行的方法

pandas.DataFrame選取特定行使用Python進行數據分析時&#xff0c;經常要使用到的一個數據結構就是pandas的DataFrame&#xff0c;如果我們想要像Excel的篩選那樣&#xff0c;只要其中的一行或某幾行&#xff0c;可以使用isin()方法&#xff0c;將需要的行的值以列表方式傳入&a…

學校選址_洛谷U3451_帶權中位數

題目描述 在一條大路一旁有許多棟樓&#xff0c;每棟樓里有許多小學生&#xff08;哈哈哈一波小學生來襲&#xff01;&#xff09;。但是這條路上沒有小學&#xff01;&#xff01;&#xff01;&#xff01;所以唯恐世界不亂的牛A打算在路上&#xff08;汽車什么的都不敢來這個…

python 重載的實現(single-dispatch generic function)

DAY 11. python 重載 函數重載是指允許定義參數數量或類型不同的同名函數&#xff0c;程序在運行時會根據所傳遞的參數類型選擇應該調用的函數 &#xff0c;但在默認情況下&#xff0c;python是不支持函數重載的&#xff0c;定義同名函數會發生覆蓋 def foo(a:int):print(fin…

SQL中的多表查詢,以及JOIN的順序重要么?

說法是&#xff0c;一般來說&#xff0c;JOIN的順序不重要&#xff0c;除非你要自己定制driving table。 示例&#xff1a; SELECT a.account_id, c.fed_id, e.fname, e.lname-> FROM account AS a INNER JOIN customer AS c-> ON a.cust_id c.cust_id-> INNER JOIN …

python可變對象 不可變對象_【Python】可變對象和不可變對象

在 Python 中一切都可以看作為對象。每個對象都有各自的 id, type 和 value。id: 當一個對象被創建后&#xff0c;它的 id 就不會在改變&#xff0c;這里的 id 其實就是對象在內存中的地址&#xff0c;可以使用 id() 去查看對象在內存中地址。type: 和 id 一樣當對象唄創建之后…

MySQL 調優基礎(三) Linux文件系統

Linux的文件系統有點像MySQL的存儲引擎&#xff0c;它支持各種各樣的文件系統。它最上層是通過 virtual files system虛擬文件系統作為一個抽象接口層來對外提供調用的。然后下層的各種文件系統實現這些調用接口就行了。 1. Linux 中的 日志文件系統和非日志文件系統 文件內容的…

python 經典類和新式類

DAY 12. python新式類和舊式類 繼承自object基類的類叫做新式類&#xff0c;否則叫做舊式類&#xff0c;python3中的類默認是新式類&#xff0c;之前版本默認是舊式類 rootkail:~# python python 2.7.15 (default,Jul 28 2018,11:29:29) [GCC 8.1.0] on linux2 Type "he…

Why does pthread_cond_signal not work?【轉】

轉自&#xff1a;http://stackoverflow.com/questions/16819169/why-does-pthread-cond-signal-not-work# 0 down vote favorite I am currently learing all around POSIX threads (pthread). I now have created a simple program which increased a shared value by 7 until…

Android開發技術周報 Issue#72

新聞 Android N 最初預覽版&#xff1a;開發者 API 和工具教程 Gradle依賴的統一管理 理解Java垃圾回收機制 淺談 Android 編程思想和架構 由Android 65K方法數限制引發的思考 Android音頻開發&#xff08;1&#xff09;&#xff1a;基礎知識 Android音頻開發&#xff08;…