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

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

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);

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

你可能感兴趣的:(Android ANR 探索)