笔记37 | Android App优化之ANR详解

来源:互联网 发布:软件质量管理计划 编辑:程序博客网 时间:2024/05/22 15:13

地址

http://blog.csdn.net/xiangyong_1521/article/details/78579099


什么是ADR

ANR全名Application Not Responding, 也就是”应用无响应”. 当操作在一段时间内系统无法处理时, 系统层面会弹出上图那样的ANR对话框.

在Android里, App的响应能力是由Activity Manager和Window Manager系统服务来监控的. 通常在如下两种情况下会弹出ANR对话框:

  • 5s内无法响应用户输入事件(例如键盘输入, 触摸屏幕等).
  • BroadcastReceiver在10s内无法结束.
  • ServiceTimeout(20 seconds) – 小概率类型(Service在特定的时间内无法处理完成)
    造成以上情况的首要原因就是在主线程(UI线程)里面做了太多的阻塞耗时操作, 例如文件读写, 数据库读写, 网络查询等等.

如何避免ADR

知道了ANR产生的原因, 那么想要避免ANR, 也就很简单了, 就一条规则:

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


如何分析ADR

a. ANR产生时, 系统会生成一个traces.txt的文件放在/data/anr/下. 可以通过adb命令将其导出到本地:

$adb pull data/anr/traces.txt .

b. 如下以GithubApp代码为例, 强行sleep thread产生的一个ANR.

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 <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c)  at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166)  // 产生ANR的那个函数调用  - 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的起点  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文件该怎么分析:

  1. 文件最上的即为最新产生的ANR的trace信息.
  2. 前面两行表明ANR发生的进程pid, 时间, 以及进程名字(包名).
  3. 寻找我们的代码点, 然后往前推, 看方法调用栈, 追溯到问题产生的根源.

以上的ANR trace是属于相对简单, 还有可能你并没有在主线程中做过于耗时的操作, 然而还是ANR了. 这就有可能是以下情况了:

a. 当是CPU占用100%, 满负荷了.

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 major4.6% 2976/com.anly.githubapp: 0.7% user + 3.7% kernel /faults: 52 minor 19 major0.9% 252/com.android.systemui: 0.9% user + 0% kernel...100%TOTAL: 5.9% user + 4.1% kernel + 89% iowait

b. 其中绝大数是被iowait即I/O操作占用了.

c. 其实内存原因有可能会导致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=-1344001376atdalvik.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 totalsize: 17036 23111 N/A 40147allocated: 16484 20675 N/A 37159free: 296 2436 N/A 2732

可以看到free的内存已所剩无几.


ANR的处理

总结针对三种不同的情况, 一般的处理情况如下

a. 主线程阻塞的(开辟单独的子线程来处理耗时阻塞事务)

b. CPU满负荷, I/O阻塞的
I/O阻塞一般来说就是文件读写或数据库操作执行在主线程了, 也可以通过开辟子线程的方式异步执行.

c. 内存不够用的
增大VM内存, 使用largeHeap属性, 排查内存泄露(这个在内存优化那篇细说吧)等.


怎么提前避免ANR

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

使用子线程的方式有哪些

a. 继承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();

b. 实现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();

c. 使用AsyncTask

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {    // 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 called            if (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);

d. HandlerThread

// 启动一个名为new_thread的子线程HandlerThread thread = new HandlerThread("new_thread");thread.start();// 取new_thread赋值给ServiceHandlerprivate 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) {      // 此时handleMessage是运行在new_thread这个子线程中了.    }}

e. IntentService

Service是运行在主线程的, 然而IntentService是运行在子线程的.
实际上IntentService就是实现了一个HandlerThread + ServiceHandler的模式.

以上HandlerThread的使用代码示例也就来自于IntentService源码.
f. Loader
Android 3.0引入的数据加载器, 可以在Activity/Fragment中使用. 支持异步加载数据, 并可监控数据源在数据发生变化时传递新结果. 常用的有CursorLoader, 用来加载数据库数据.

// Prepare the loader.  Either re-connect with an existing one,// or start a new one.// 使用LoaderManager来初始化LoadergetLoaderManager().initLoader(0, null, this);//如果 ID 指定的加载器已存在,则将重复使用上次创建的加载器。//如果 ID 指定的加载器不存在,则 initLoader() 将触发 LoaderManager.LoaderCallbacks 方法 //onCreateLoader()。在此方法中,您可以实现代码以实例化并返回新加载器// 创建一个Loaderpublic 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);}

注意:使用Thread和HandlerThread时, 为了使效果更好, 建议设置Thread的优先级偏低一点:

因为如果没有做任何优先级设置的话, 你创建的Thread默认和UI Thread是具有同样的优先级的, 你懂的. 同样的优先级的Thread, CPU调度上还是可能会阻塞掉你的UI Thread, 导致ANR的.

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);