引言
- 背景:Android App優(yōu)化, 要怎么做?
- Android App優(yōu)化之性能分析工具
- Android App優(yōu)化之提升你的App啟動(dòng)速度之理論基礎(chǔ)
- Android App優(yōu)化之提升你的App啟動(dòng)速度之實(shí)例挑戰(zhàn)
- Android App優(yōu)化之Layout怎么擺
- Android App優(yōu)化之ANR詳解
- Android App優(yōu)化之消除卡頓
- Android App優(yōu)化之內(nèi)存優(yōu)化
- Android App優(yōu)化之持久電量
- Android App優(yōu)化之如何高效網(wǎng)絡(luò)請(qǐng)求
App優(yōu)化系列已近中期, 前面分享了一些工具, 理論, 也結(jié)合了案例談了下啟動(dòng)優(yōu)化, 布局分析等.
原計(jì)劃將本文作為這個(gè)系列的一個(gè)承上啟下點(diǎn), 對(duì)前面幾篇作一個(gè)小總結(jié), 聊聊App流暢度和快速響應(yīng)的話題.
粗一縷, 發(fā)現(xiàn)內(nèi)容還是很多, 暫且拆成幾篇來慢慢寫吧, 勿怪~
今天先來聊聊ANR.
1, 你碰到ANR了嗎
在App使用過程中, 你可能遇到過這樣的情況:
恭喜你, 這就是傳說中的ANR.
1.1 何為ANR
ANR全名Application Not Responding, 也就是"應(yīng)用無響應(yīng)". 當(dāng)操作在一段時(shí)間內(nèi)系統(tǒng)無法處理時(shí), 系統(tǒng)層面會(huì)彈出上圖那樣的ANR對(duì)話框.
1.2 為什么會(huì)產(chǎn)生ANR
在Android里, App的響應(yīng)能力是由Activity Manager和Window Manager系統(tǒng)服務(wù)來監(jiān)控的. 通常在如下兩種情況下會(huì)彈出ANR對(duì)話框:
- 5s內(nèi)無法響應(yīng)用戶輸入事件(例如鍵盤輸入, 觸摸屏幕等).
- BroadcastReceiver在10s內(nèi)無法結(jié)束.
造成以上兩種情況的首要原因就是在主線程(UI線程)里面做了太多的阻塞耗時(shí)操作, 例如文件讀寫, 數(shù)據(jù)庫讀寫, 網(wǎng)絡(luò)查詢等等.
1.3 如何避免ANR
知道了ANR產(chǎn)生的原因, 那么想要避免ANR, 也就很簡單了, 就一條規(guī)則:
不要在主線程(UI線程)里面做繁重的操作.
這里面實(shí)際上涉及到兩個(gè)問題:
- 哪些地方是運(yùn)行在主線程的?
- 不在主線程做, 在哪兒做?
稍后解答.
2, ANR分析
2.1 獲取ANR產(chǎn)生的trace文件
ANR產(chǎn)生時(shí), 系統(tǒng)會(huì)生成一個(gè)traces.txt的文件放在/data/anr/下. 可以通過adb命令將其導(dǎo)出到本地:
$adb pull data/anr/traces.txt .
2.2 分析traces.txt
2.2.1 普通阻塞導(dǎo)致的ANR
獲取到的tracs.txt文件一般如下:
如下以GithubApp代碼為例, 強(qiáng)行sleep thread產(chǎn)生的一個(gè)ANR.
----- pid 2976 at 2016-09-08 23:02:47 -----
Cmd line: com.anly.githubapp // 最新的ANR發(fā)生的進(jìn)程(包名)
...
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過長時(shí)間, 阻塞導(dǎo)致無響應(yīng).
at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258)
- locked <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c)
at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166) // 產(chǎn)生ANR的那個(gè)函數(shù)調(diào)用
- locked <@addr=0x12d1e840> (a java.lang.Class<com.tencent.bugly.crashreport.CrashReport>)
at com.anly.githubapp.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23)
at com.anly.githubapp.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起點(diǎn)
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)
拿到trace信息, 一切好說.
如上trace信息中的添加的中文注釋已基本說明了trace文件該怎么分析:
- 文件最上的即為最新產(chǎn)生的ANR的trace信息.
- 前面兩行表明ANR發(fā)生的進(jìn)程pid, 時(shí)間, 以及進(jìn)程名字(包名).
- 尋找我們的代碼點(diǎn), 然后往前推, 看方法調(diào)用棧, 追溯到問題產(chǎn)生的根源.
以上的ANR trace是屬于相對(duì)簡單, 還有可能你并沒有在主線程中做過于耗時(shí)的操作, 然而還是ANR了. 這就有可能是如下兩種情況了:
2.2.2 CPU滿負(fù)荷
這個(gè)時(shí)候你看到的trace信息可能會(huì)包含這樣的信息:
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
最后一句表明了:
- 當(dāng)是CPU占用100%, 滿負(fù)荷了.
- 其中絕大數(shù)是被iowait即I/O操作占用了.
此時(shí)分析方法調(diào)用棧, 一般來說會(huì)發(fā)現(xiàn)是方法中有頻繁的文件讀寫或是數(shù)據(jù)庫讀寫操作放在主線程來做了.
2.2.3 內(nèi)存原因
其實(shí)內(nèi)存原因有可能會(huì)導(dǎo)致ANR, 例如如果由于內(nèi)存泄露, App可使用內(nèi)存所剩無幾, 我們點(diǎn)擊按鈕啟動(dòng)一個(gè)大圖片作為背景的activity, 就可能會(huì)產(chǎn)生ANR, 這時(shí)trace信息可能是這樣的:
// 以下trace信息來自網(wǎng)絡(luò), 用來做個(gè)示例
Cmdline: android.process.acore
DALVIK 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的內(nèi)存已所剩無幾.
當(dāng)然這種情況可能更多的是會(huì)產(chǎn)生OOM的異常...
2.2 ANR的處理
針對(duì)三種不同的情況, 一般的處理情況如下
主線程阻塞的
開辟單獨(dú)的子線程來處理耗時(shí)阻塞事務(wù).CPU滿負(fù)荷, I/O阻塞的
I/O阻塞一般來說就是文件讀寫或數(shù)據(jù)庫操作執(zhí)行在主線程了, 也可以通過開辟子線程的方式異步執(zhí)行.內(nèi)存不夠用的
增大VM內(nèi)存, 使用largeHeap屬性, 排查內(nèi)存泄露(這個(gè)在內(nèi)存優(yōu)化那篇細(xì)說吧)等.
3, 深入一點(diǎn)
沒有人愿意在出問題之后去解決問題.
高手和新手的區(qū)別是, 高手知道怎么在一開始就避免問題的發(fā)生. 那么針對(duì)ANR這個(gè)問題, 我們需要做哪些層次的工作來避免其發(fā)生呢?
3.1 哪些地方是執(zhí)行在主線程的
- Activity的所有生命周期回調(diào)都是執(zhí)行在主線程的.
- Service默認(rèn)是執(zhí)行在主線程的.
- BroadcastReceiver的onReceive回調(diào)是執(zhí)行在主線程的.
- 沒有使用子線程的looper的Handler的handleMessage, post(Runnable)是執(zhí)行在主線程的.
- AsyncTask的回調(diào)中除了doInBackground, 其他都是執(zhí)行在主線程的.
- View的post(Runnable)是執(zhí)行在主線程的.
3.2 使用子線程的方式有哪些
上面我們幾乎一直在說, 避免ANR的方法就是在子線程中執(zhí)行耗時(shí)阻塞操作. 那么在Android中有哪些方式可以讓我們實(shí)現(xiàn)這一點(diǎn)呢.
3.2.1 啟Thread方式
這個(gè)其實(shí)也是Java實(shí)現(xiàn)多線程的方式. 有兩種實(shí)現(xiàn)方法, 繼承Thread 或 實(shí)現(xiàn)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();
實(shí)現(xiàn)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
這個(gè)是Android特有的方式, AsyncTask顧名思義, 就是異步任務(wù)的意思.
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
// Do the long-running work in here
// 執(zhí)行在子線程
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 called
if (isCancelled()) break;
}
return totalSize;
}
// This is called each time you call publishProgress()
// 執(zhí)行在主線程
protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}
// This is called when doInBackground() is finished
// 執(zhí)行在主線程
protected void onPostExecute(Long result) {
showNotification("Downloaded " + result + " bytes");
}
}
// 啟動(dòng)方式
new DownloadFilesTask().execute(url1, url2, url3);
3.2.3 HandlerThread
Android中結(jié)合Handler和Thread的一種方式. 前面有云, 默認(rèn)情況下Handler的handleMessage是執(zhí)行在主線程的, 但是如果我給這個(gè)Handler傳入了子線程的looper, handleMessage就會(huì)執(zhí)行在這個(gè)子線程中的. HandlerThread正是這樣的一個(gè)結(jié)合體:
// 啟動(dòng)一個(gè)名為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);
}
@Override
public void handleMessage(Message msg) {
// 此時(shí)handleMessage是運(yùn)行在new_thread這個(gè)子線程中了.
}
}
3.2.4 IntentService
Service是運(yùn)行在主線程的, 然而IntentService是運(yùn)行在子線程的.
實(shí)際上IntentService就是實(shí)現(xiàn)了一個(gè)HandlerThread + ServiceHandler的模式.
以上HandlerThread的使用代碼示例也就來自于IntentService源碼.
3.2.5 Loader
Android 3.0引入的數(shù)據(jù)加載器, 可以在Activity/Fragment中使用. 支持異步加載數(shù)據(jù), 并可監(jiān)控?cái)?shù)據(jù)源在數(shù)據(jù)發(fā)生變化時(shí)傳遞新結(jié)果. 常用的有CursorLoader, 用來加載數(shù)據(jù)庫數(shù)據(jù).
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
// 使用LoaderManager來初始化Loader
getLoaderManager().initLoader(0, null, this);
//如果 ID 指定的加載器已存在,則將重復(fù)使用上次創(chuàng)建的加載器登馒。
//如果 ID 指定的加載器不存在罢艾,則 initLoader() 將觸發(fā) LoaderManager.LoaderCallbacks 方法 //onCreateLoader()桩盲。在此方法中排抬,您可以實(shí)現(xiàn)代碼以實(shí)例化并返回新加載器
// 創(chuàng)建一個(gè)Loader
public Loader<Cursor> 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<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
}
具體請(qǐng)參看官網(wǎng)Loader介紹.
3.2.6 特別注意
使用Thread和HandlerThread時(shí), 為了使效果更好, 建議設(shè)置Thread的優(yōu)先級(jí)偏低一點(diǎn):
Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
因?yàn)槿绻麤]有做任何優(yōu)先級(jí)設(shè)置的話, 你創(chuàng)建的Thread默認(rèn)和UI Thread是具有同樣的優(yōu)先級(jí)的, 你懂的. 同樣的優(yōu)先級(jí)的Thread, CPU調(diào)度上還是可能會(huì)阻塞掉你的UI Thread, 導(dǎo)致ANR的.
結(jié)語
對(duì)于ANR問題, 個(gè)人認(rèn)為還是預(yù)防為主, 認(rèn)清代碼中的阻塞點(diǎn), 善用線程. 同時(shí)形成良好的編程習(xí)慣, 要有MainThread和Worker Thread的概念的...(實(shí)際上人的工作狀態(tài)也是這樣的~~哈哈)
強(qiáng)行插入一波:
之前發(fā)的打造一款開源的Android平臺(tái)的Github客戶端的這個(gè)客戶端有了自己的名字了, 叫做CoderPub. 歡迎大家持續(xù)關(guān)注, 參與, 貢獻(xiàn)...
另外, 我也啟用了weibo賬戶anly-jun, 歡迎互粉.
轉(zhuǎn)載請(qǐng)注明出處, 歡迎大家分享到朋友圈, 微博~