美文网首页
Android UI 卡顿及ANR检测原理

Android UI 卡顿及ANR检测原理

作者: Miss_Ella | 来源:发表于2022-02-19 18:29 被阅读0次

    [转]Android UI 卡顿及ANR检测原理

    一:背景

    众所周知,Android不允许在UI线程中做耗时的操作,否则有可能发生ANR的可能,默认情况下,在Android中Activity的最长执行时间是5秒,BroadcastReceiver的最长执行时间则是10秒。如果操作这个时长,则会产生ANR。而本文所要介绍的主要内容是检测UI线程中的耗时操作,从而能够定位一些老代码中的各种耗时的操作,作为性能优化的依据。

    二:常见方案

    1. 通过UI 线程looper
    2. 通过Choreographer

    2.1 通过UI 线程looper的打印日志

    在github上也有许多开源库是基于该原理,比较有代表行的有
    AndroidPerformanceMonitor
    ANR-WatchDog

    下面以AndroidPerformanceMonitor为例子来进行原理的介绍
    AndroidPerformanceMonitor
    通过如下代码可以看到,只存在一个主线程的Looper,所有通过主线程Handler发送的消息,都会发送到这里。

    /**
         * Initialize the current thread as a looper, marking it as an
         * application's main looper. The main looper for your application
         * is created by the Android environment, so you should never need
         * to call this function yourself.  See also: {@link #prepare()}
         */
        public static void prepareMainLooper() {
            prepare(false);
            synchronized (Looper.class) {
                if (sMainLooper != null) {
                    throw new IllegalStateException("The main Looper has already been prepared.");
                }
                sMainLooper = myLooper();
            }
        }
    
        /**
         * Returns the application's main looper, which lives in the main thread of the application.
         */
        public static Looper getMainLooper() {
            synchronized (Looper.class) {
                return sMainLooper;
            }
        }
    

    继续仔细观察Looper的源码可以发现,msg.target.dispatchMessage(msg); 这句用来进行消息的分发处理,在处理前后分别会打印日志,如果我们在消息处理之前计一个时,在消息处理之后计算一个值,如果这两者的阈值,大于了我们设定的门限,那么就可以认为卡顿。

        public static void loop() {
            final Looper me = myLooper();
                    ...
         
         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
                Printer logging = me.mLogging;
                if (logging != null) {
                    logging.println(">>>>> Dispatching to " + msg.target + " " +
                            msg.callback + ": " + msg.what);
                }
    
                msg.target.dispatchMessage(msg);
    
                if (logging != null) {
                    logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
              }
                  ...
            }
        }
    

    但是MainLooper里面的默认打印的消息,并没有记录时间,这时,我们需要通过Looper.setMessageLogging来设置自定义的 Printer 。

    public void setMessageLogging(@Nullable Printer printer) {
            mLogging = printer;
        }
    

    在BlockCanary中 定义了自定义的Printer
    class LooperMonitor implements Printer
    其println()方法逻辑就是消息处理之后各记录时间值,计算这两者的阈值,判断是否block。

    @Override
        public void println(String x) {
            if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
                return;
            }
            if (!mPrintingStarted) {
                mStartTimestamp = System.currentTimeMillis();
                mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
                mPrintingStarted = true;
                startDump();
            } else {
                final long endTime = System.currentTimeMillis();
                mPrintingStarted = false;
                if (isBlock(endTime)) {
                    notifyBlockEvent(endTime);
                }
                stopDump();
            }
        }
    

    流程图


    image.png

    2.2 通过UI 线程looper循环发送消息

    比较有代表性的作品

    ANR-WatchDog

    实现十分的简单,主要的类就是一个Thread, 通过使用主线程的Handler,不断的发送任务, 使得变量_tick的值不断的增加,然后再去做判断,_tick的值是否能得到,如果_tick的值没有变,则认为UI线程已经卡顿.

    private final Runnable _ticker = new Runnable() {
            @Override public void run() {
                _tick = (_tick + 1) % Integer.MAX_VALUE;
            }
        };
    
    @Override
        public void run() {
            setName("|ANR-WatchDog|");
    
            int lastTick;
            int lastIgnored = -1;
            while (!isInterrupted()) {
                lastTick = _tick;
                _uiHandler.post(_ticker);
                try {
                      //睡眠一段时间,确保_tick的值更新后再做判断。
                    Thread.sleep(_timeoutInterval);
                }
                catch (InterruptedException e) {
                    _interruptionListener.onInterrupted(e);
                    return ;
                }
    
                // If the main thread has not handled _ticker, it is blocked. ANR.
                if (_tick == lastTick) {
                    if (!_ignoreDebugger && Debug.isDebuggerConnected()) {
                        if (_tick != lastIgnored)
                            Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
                        lastIgnored = _tick;
                        continue ;
                    }
    
                    ANRError error;
                    if (_namePrefix != null)
                        error = ANRError.New(_namePrefix, _logThreadsWithoutStackTrace);
                    else
                        error = ANRError.NewMainOnly();
                    _anrListener.onAppNotResponding(error);
                    return;
                }
            }
    }
    
    通过Choreographer

    Takt
    Android系统从4.1(API 16)开始加入Choreographer这个类来控制同步处理输入(Input)、动画(Animation)、绘制(Draw)三个UI操作。其实UI显示的时候每一帧要完成的事情只有这三种。

    image.png

    Choreographer接收显示系统的时间脉冲(垂直同步信号-VSync信号),在下一个frame渲染时控制执行这些操作。
    Choreographer.getInstance().postFrameCallback(new FPSFrameCallback());
    把你的回调添加到Choreographer之中,那么在下一个frame被渲染的时候就会回调你的callback.
    通过判断两次回调doFrame执行的时间差,来判断是否发生ANR
    以Takt为例。以下为其核心代码,主要代码都添加了注释。

    @Override 
    public void doFrame(long frameTimeNanos) {
        long currentTimeMillis = TimeUnit.NANOSECONDS.toMillis(frameTimeNanos);
    
        if (frameStartTime > 0) {
          // take the span in milliseconds
          final long timeSpan = currentTimeMillis - frameStartTime;
             //渲染次数+1
             framesRendered++;  
     if (timeSpan > interval) {
           //超过阈值,计算刷新频率
            final double fps = framesRendered * 1000 / (double) timeSpan;
    
            frameStartTime = currentTimeMillis;
            framesRendered = 0;
    
            for (Audience audience : listeners) {
    //回调处理
              audience.heartbeat(fps);
            }
          }
        } else {
          //第一次 frameStartTime=0;
          frameStartTime = currentTimeMillis;
        }
          choreographer.postFrameCallback(this);
    }
    

    三:参考资料

    BlockCanary — 轻松找出Android App界面卡顿元凶
    Cockroach
    Choreographer源码分析
    Android应用ANR分析

    相关文章

      网友评论

          本文标题:Android UI 卡顿及ANR检测原理

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