Android App 优化之 ANR 详解

为了便于阅读, 应邀将Android App性能优化系列, 转移到掘金原创上来.
掘金的新出的"收藏集"功能能够用来作系列文集了.html

今天先来聊聊ANR.java

1, 你碰到ANR了吗

在App使用过程当中, 你可能遇到过这样的状况:android

恭喜你, 这就是传说中的ANR.git

1.1 何为ANR

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

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

 
  
  
  

 
  
  
  

 
  
  
  

 
  
  
  

 复制代码

拿到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.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的异常...

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

    @Override
    public 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的概念的...(实际上人的工做状态也是这样的~~哈哈)

相关文章
相关标签/搜索