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

地址

http://www.jianshu.com/u/c187b887771f


什么是ADR

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

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

  • 5s内无法响应用户输入事件(例如键盘输入, 触摸屏幕等).
  • BroadcastReceiver在10s内无法结束.
  • ServiceTimeout(20 seconds) -- 小概率类型(Service在特定的时间内无法处理完成)

如何避免ADR

造成ADR情况的首要原因就是在主线程(UI线程)里面做了太多的阻塞耗时操作, 例如文件读写, 数据库读写, 网络查询等等,知道了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了. 这就有可能是以下情况了:

1. 当是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 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

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

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的内存已所剩无几.


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赋值给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这个子线程中了.
    }
}

e. IntentService

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

以上HandlerThread的使用代码示例也就来自于IntentService源码.f. LoaderAndroid 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);
}

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

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

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);

原文发布于微信公众号 - 项勇(xiangy_life)

原文发表时间:2017-11-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏河湾欢儿的专栏

探究 css touch-action 属性

今天在写vue项目的时候碰到一个 Unable to preventDefault inside passive event listener due to ...

831
来自专栏IMWeb前端团队

iconfont关于content值的坑

font-carrier是一个功能强大的字体操作库,使用它你可以随心所欲的操作字体。让你可以在svg的维度改造字体的展现形状。 //可以设置某个字对应的形状,当...

18810
来自专栏非著名程序员

一个优秀的Android应用从建项目开始

1.项目结构 现在的MVP模式越来越流行。就默认采用了。 如果项目比较小的话: app——Application Activity Fragment Prese...

17910
来自专栏何俊林

如何进行网络视频截图/获取视频的缩略图

小编导读:获取视频的缩略图,截图正在播放的视频某一帧,是在音视频开发中,常遇到的问题。本文是主要用于点播中截图视频,同时还可以获取点播视频的缩略图进行显示,留下...

5277
来自专栏为数不多的Android技巧

Android 插件化原理解析——Activity生命周期管理

之前的 Android插件化原理解析 系列文章揭开了Hook机制的神秘面纱,现在我们手握倚天屠龙,那么如何通过这种技术完成插件化方案呢?具体来说,插件中的Act...

811
来自专栏代码GG之家

android调用dialog.hide()引起的输入事件派发错误问题追踪

问题描述:某个界面启动后,上面的actionbar的item点击不起作用 问题调研: 00 在activity的启动过程中,创建了一个Fragment.jav...

1987
来自专栏上善若水

012android初级篇之Handler机制

Handler类被用来注册到一个线程中,这样可以提供一个简单的通信渠道,用来发送数据到这个线程。 可作为UI线程与后台线程交互的几种方法之一。

886
来自专栏微信终端开发团队的专栏

iOS 事件处理机制与图像渲染过程

致歉声明: Peter在开发公众号功能时触发了一个bug,导致群发错误。对此我们深表歉意,并果断开除了Peter。以下交回给正文时间: iOS 事件处理机制与...

7878
来自专栏everhad

小知识 安卓线程和ui

多线程环境下的ui修改   开发过程中,经常需要开启新的线程,并且在其它线程中改变ui线程的ui对象的状态。Android设计出于性能考虑,ui对象为非线程安全...

17810
来自专栏Nian糕的私人厨房

Vue2.0 歌手列表滚动及右侧快速入口实现

本次的系列博文的知识点讲解和代码,主要是来自于 黄轶 在慕课网的 Vue 2.0 高级实战-开发移动端音乐WebApp 课程,由个人总结并编写,其代码及知识点...

855

扫码关注云+社区