V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
winlee28
V2EX  ›  Android

如何监测 Android 应用卡顿?这篇就够了

  •  1
     
  •   winlee28 · 2019-12-05 11:15:42 +08:00 · 9516 次点击
    这是一个创建于 1817 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本文首发于微信公众号「 Android 开发之旅」,欢迎关注

    卡顿介绍

    用户在使用我们应用的时候,很多问题是很难被及时的发现的比如内存占用高,耗费流量等,但是一旦发生卡顿就会被用户直观的感受到。所以应用卡顿是很影响用户体验的。另外一方面,对于开发者来说,卡顿的问题很难定位,发生问题的原因错综复杂,比如:代码问题、内存问题、绘制问题以及 IO 操作等等。而且线上发生的卡顿问题在线下我们很难复现,因为这和用户当时的系统环境有很大的关系,因此我们需要在用户发送卡顿的时候记录下用户使用的场景等。比如:内存消耗,磁盘空间,用户行为路径等等。

    优化工具

    CPU Profile

    目前 Android Studio 以及自带了 CPU Profiler 工具,它可以以图形化的形式展示执行的时间、调用栈等信息。收集的信息比较全面,包含了所有线程。但是由于其收集信息全面,导致了运行时内存开销严重,App 函数整体运行都会变慢,可能会带偏我们的优化方向。

    使用方式: Debug.startMethodTracing(); ... Debug.stopMethodTracing(); 最终生成的文件在 sd 卡:Android/data/packagename/files 目录下。

    Systrace

    Systrace 之前文章已经讲解过,它是轻量级的框架,而且开销小,可以直观反映 CPU 的利用率而且右侧 alter 可以针对一些问题给出相关的建议。 比如绘制慢或者 GC 频繁等。

    StrictMode

    Android2.3 引入的一个工具类:严苛模式。是一种运行时检测机制。可以帮助开发人员检测代码当中不规范的问题。StrictMode 主要检测线程策略和虚拟机策略。

    线程策略包括:

    • 自定义的耗时调用,detectCustimSlowCalls

    • 磁盘读取操作,detectDiskReads

    • 网络操作,detectNetwork

    虚拟机策略:

    • Activity 泄漏,detectActivityLeaks

    • Sqlite 对象泄漏,detectLeakedSqlLiteObjects

    • 检测实例数量,setClassInstanceLimit

    我们在 Application 中使用:

        private void initStrictMode() {
            if (BuildConfig.DEBUG) {
                //线程
                StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                        .detectCustomSlowCalls() //API 等级 11,使用 StrictMode.noteSlowCode
                        .detectDiskReads()
                        .detectDiskWrites()
                        .detectNetwork()// 或者直接使用 .detectAll() 手机全部信息
                        .penaltyLog() //在 Logcat 中打印违规异常信息,还可以选择弹框提示或者直接奔溃等
                        .build());
                //虚拟机
                StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                        .detectLeakedSqlLiteObjects()
                        .setClassInstanceLimit(StrictModeTest.class, 1)
                        .detectLeakedClosableObjects() //API 等级 11
                       .penaltyDropBox()
                        .build());
            }
        }
    

    StrictMode 本身也是耗性能的,所以我们只在 debug 模式下开启。当出现不符合检测策略的时候就会在控制台打印日志,输入 StrictMode 关键词过滤即可。

    自动化检测卡顿方法

    CPU Profiler 和 Systrace 都是适合线下使用的,无法带到线上。那我们如何做到线上监测卡顿呢?

    我们都知道一个进程中只有个 Looper 对象,我们通过查看 Looper 源码发现,在其 loop 方法中的死循环中有个 mLogging 对象,在执行的时候打印了一个 Dispatching to 日志,执行完成的时候有打印了一个 Finished to 日志。如:

    public static void loop() {
     
           // ....省略开始代码...
           
            for (;;) {
                Message msg = queue.next(); // might block
                if (msg == null) {
                    // No message indicates that the message queue is quitting.
                    return;
                }
    ​
                // This must be in a local variable, in case a UI event sets the logger
                final Printer logging = me.mLogging;
                if (logging != null) {
                    //重点 开始打印
                    logging.println(">>>>> Dispatching to " + msg.target + " " +
                            msg.callback + ": " + msg.what);
                }
                
                // ...省略中间代码...
                
                if (logging != null) {
                    //重点 完成打印
                    logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
                }
    ​
                // ...省略最后代码...
            }
        }
    

    所以我们可以自定义 Printer 对象,让 Handler 的日志都通过我们自定义的 Printer 进行打印,然后收集日志信息,匹配 Dispatching to 和 Finished to 字段,如果在设定的某个时间内只有 Dispatching to 字段而没有 Finished to 字段,那么就说明发生了卡顿。发生卡顿后我们就收集此时的调用栈信息。相反如果两个字段都存在则说明应用运行的很流畅。

    字段 Printer 设置给 mLogging 对象:

    Looper.getMainLooper().setMessageLogging(new Printer() {
        @Override
        public void println(String log) {
              Log.e("printer","==println=="+log);
        }
    });
    

    代码中的 log 字段就是我们需要的 Dispatch 和 Finished 字段,我们监测这两个字段并收集调用栈信息将其发送到后端进行分析使用。

    那么这里其实还存在一个问题就是可能我们收集的信息不够准确,为什么呢?就是我们收集的调用栈信息是最后收集的,那么这个时候有可能卡顿已经执行完成了,此刻搜集到的信息有可能不是卡顿发生的关键信息。就像 OOM 一样,它是一个随时都有可能发生的。所以我们需要高频率的收集日志信息,高频率的收集对后端有一定的压力,而我们高频收集的信息有很大一部分也是重复的,所以就需要日志去重操作。

    ANR 异常

    ANR 异常全称 Application Not Responding,即应用无响应。如果你的应用程序有一段时间响应不够灵敏,系统会向用户显示一个对话框,这个对话框称作应用程序无响应对话框,用户可以选择“等待”而让程序继续运行,也可以选择“强制关闭”。所以一个流畅的合理的应用程序中不能出现 anr。因为这很影响用户的使用体验,当然由于厂商深度定制系统的原因,在某些手机上发生 ANR 也不会弹框的。

    发生 ANR 到弹框在不同的组件之间时间定义是不一样的,按键是 5 秒。前台广播 10 秒,后台广播 60 秒。前台服务 20 秒,后台服务 200 秒。这些数据都定义在 AMS 中,读者可以去看看。

    ANR 发生执行的流程:

    ANR 的日志在 data/anr/traces.txt 目录下。

    我们在线下的时候可以直接通过 ADB 命令来查看日志:

    adb pull data/anr/traces.txt 你的目录 这样可以详细分析 CPU、IO、锁等操作的问题所在。

    线上我们可以使用 FileObserver 监控文件变化,但是这种方法在高版本系统中有权限问题。另外一种就是使用 AnrWatchDog 框架。这也是一个开源框架,地址: https://github.com/SalomonBrys/ANR-WatchDog。 它的原理就是通过修改值的方式判断 UI 线程是否发生卡顿。

    这个库使用也非常简单,首先在 gradle 中配置:

    compile 'com.github.anrwatchdog:anrwatchdog:1.4.0'
    

    然后在 Application 中进行初始化:

    new ANRWatchDog().start();
    

    这样就可以了。默认检测到卡顿就直接抛 ANRError 异常将应用奔溃,我们可以复写 Listener 接口来抓取堆栈信息。

    ANRWatchDog 是继承之 Thread 线程的,那么我们就看下核心方法 run 方法中的代码逻辑。

        // post 的操作
        private final Runnable _ticker = new Runnable() {
            @Override public void run() {
                _tick = 0;
                _reported = false;
            }
        };
    
    @Override
        public void run() {
            // 首先对线程进行重命名
            setName("|ANR-WatchDog|");
    ​
            long interval = _timeoutInterval;
            while (!isInterrupted()) {
                boolean needPost = _tick == 0;
                _tick += interval;
                if (needPost) {
                    // 发送 post
                    _uiHandler.post(_ticker);
                }
    ​
                try {
                    // 睡眠
                    Thread.sleep(interval);
                } catch (InterruptedException e) {
                    _interruptionListener.onInterrupted(e);
                    return ;
                }
    ​
                // If the main thread has not handled _ticker, it is blocked. ANR.
                if (_tick != 0 && !_reported) {
                    //noinspection ConstantConditions
                    if (!_ignoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) {
                        Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
                        _reported = true;
                        continue ;
                    }
    ​
                    interval = _anrInterceptor.intercept(_tick);
                    if (interval > 0) {
                        continue;
                    }
    ​
                    final ANRError error;
                    if (_namePrefix != null) {
                        error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);
                    } else {
                        error = ANRError.NewMainOnly(_tick);
                    }
                    _anrListener.onAppNotResponding(error);
                    interval = _timeoutInterval;
                    _reported = true;
                }
            }
        }
    
    

    使用 ANRWatchDog 的原因就是它是非侵入式的,并且可以弥补高版本权限问题。二者结合使用。

    以上就是对应用卡顿检测的方法。那么具体如何规避卡顿,这就要求我们在平时的开发中养成良好的代码习惯。书写高质量代码。

    如果觉得文章对你有帮助,扫描下方二维码关注公众号,及时获取文章推送。

    二维码

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1007 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 21:22 · PVG 05:22 · LAX 13:22 · JFK 16:22
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.