Android ANR 探索

什么是 ANR?

在 Android 上,如果你的应用程序有一段时间响应不够灵敏,系统会向用户显示一个对话框,这个对话框称作应用程序无响应(ANR:Application Not Responding)对话框。用户可以选择“等待”而让程序继续运行,也可以选择“强制关闭”。所以,为了提高用户体验,一个流畅的合理的应用程序中不应该出现 ANR。

为什么会产生 ANR?

在 Android 里, 应用程序的响应能力是由 Activity Manager 和 Window Manager 系统服务来监控的。默认情况下,在 Android 中 Activity 的最长执行时间是5秒,BroadcastReceiver 的最长执行时间则是10秒。

因此,通常在以下三种情况下会弹出 ANR 对话框:

  • 5 秒内无法响应用户输入事件(键盘输入、触摸屏幕等);

  • BroadcastReceiver 在 10 秒内无法完成;

  • Service 在 20 秒内无法完成;

造成以上两种情况的首要原因就是在主线程(UI线程)里面做了过多的阻塞耗时操作,例如:文件读写、数据库读写、网络查询等。

如何避免 ANR?

其实知道了 ANR 产生的原因,那么避免的方法就很明显了。

不要在主线程中做过多的工作。

明确点说,就是在子线程中执行耗时阻塞的工作。

ANR如何定位?

1. 获取 trace 文件

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

$adb pull data/anr/traces.txt

2. 分析 trace 文件

拿到 trace 文件之后,我们可以这样开始分析:

  • 文件最开始的部分是最新产生的 ANR 的 trace 信息;
  • 前面两行表明 ANR 发生的进程pid,时间,以及进程名字(包名);
  • 一行一行地查看方法调用栈,逐步找到问题产生的根源;

3. 三种类型的 trace 文件

3.1 普通阻塞导致 ANR

因为主线程长时间阻塞导致 ANR,这时的 trace 文件可能是这样的:

----- 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 <@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)

3.2 CUP 满负荷导致 ANR

CPU 满负荷也可以导致 ANR。这时的 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

最后一句表明了:

  • CPU 占用率100%,满负荷了;
  • 其中绝大部分是被 iowait 即 I/O 操作占用了;

此时分析方法调用栈,一般来说会发现是方法中有频繁的文件读写或是数据库读写操作放在主线程来做了。

3.3 内存泄漏导致 ANR

内存泄漏也有可能会导致ANR,例如我们点击按钮启动一个大图片做背景的 Activity,就可能会产生 ANR,这时 trace 文件可能是这样的:

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(可用的)的内存已经不多了。当然这种情况下,更多地是出现 OOM 异常。

针对以上三种不同的情况,一般的处理手段如下:

  • 主线程阻塞
    使用子线程来处理耗时阻塞任务。
  • CPU 满负荷,I/O 阻塞
    I/O 阻塞一般来说就是文件读写或数据库操作在主线程上执行,可以通过使用子线程的方式异步执行。
  • 内存泄漏
    这部分应该归为内存管理方面的优化,这里就不细说了。

哪些操作是在主线程执行的呢?

1. Activity 的所有生命周期回调都是执行在主线程的;

2. Service 默认是执行在主线程的;

3. BroadcastReceiver 的 onReceive 回调是执行在主线程的;

4. 与主线程绑定的 Handler 的 handleMessage(),
post(Runnable) 是执行在主线程的;

5. AsyncTask 的回调中除了 doInBackground(),其他都是执行在主线程的;

6. View 的 post(Runnable) 是执行在主线程的;

使用子线程的方式有哪些?

1. 启用新的线程

有两种实现方法,,继承 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(100);
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(100);
new Thread(p).start();

2. 使用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);

3. 使用 HandlerThread

给 Handler 传入子线程的 looper,handleMessage() 就会执行在这个子线程中。

// 启动一个名为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) {
      // 此时handleMessage是运行在new_thread这个子线程中了.
    }
}

4. IntentService

Service 默认运行在主线程的,然而 IntentService 是运行在子线程的。

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<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);
}

特别注意,使用子线程 Handler时,建议把子线程的优先级设置低一点:

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,128评论 25 707
  • 引言 背景:Android App优化, 要怎么做? Android App优化之性能分析工具 Android A...
    anly_jun阅读 88,040评论 14 248
  • 孤独的人啊 一起晚安吧
    爱笑的逗妹儿阅读 175评论 0 0
  • 这世上伟人留给你那么多道理,不是让你照着道理活着,是希望你有一天也能活出自己的道理
    劳资迷死你阅读 152评论 0 0
  • 停机概率,又称蔡廷常数(Chaitin's constant)是将充分长的0-1随机串输入一台无前缀(prefi...
    十酒三阅读 2,103评论 3 2