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

作者: 项勇 | 来源:发表于2017-11-20 09:20 被阅读35次

    地址

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


    什么是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 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
    

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

    c. 其实内存原因有可能会导致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. 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);
    }
    

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

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

    Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
    

    相关文章

      网友评论

        本文标题:笔记37 | Android App优化之ANR详解

        本文链接:https://www.haomeiwen.com/subject/rcbmvxtx.html