Android——UI卡顿分析与整理

以下内容,参考至 面对 UI 卡顿,如何入手分析解决问题

什么导致了卡顿

内存抖动是造成 UI 卡顿的最主要原因。内存抖动的原因是频繁的创建和回收对象。而回收对象的工作是 GC 进程为我们实现的。GC在工作时会触发Stop The World。因此,频繁的发生 GC 是导致卡顿的主要原因。

怎么分析卡顿问题

对于UI性能分析, Systrace 是目前使用最广的工具,它收集相关程序的运行数据,生成一个网页文件,通过 Chrome 查看该网页文件,可以看到各个方法运行的时间。

Android为我们提供了运行Systrace的脚本,在 android-sdk/platform-tools/systrace 文件夹中(我们可以在环境变量中添加该地址)。在连接 adb 的条件下,运行以下命令

python systrace.py --time=20 -o newtrace.html

会在当前目录生成一个 newtrace.html 的网页文件,我们通过 Chrome 查看 newtrace.html 。

在使用 Systrace 分析UI性能问题时,我们先学习一下屏幕的刷新机制。

CPU&GPU

我们知道,UI线程在抢占到CPU 时间片的执行权后,会做一些 UI 的绘制工作,还会处理用户的输入,View动画等事件。

CPU绘制或处理事件后,会在 ReaderThread 线程中将这些数据提交给 GPU。GPU 负责对这些数据进行 栅格化(Rasterzation)操作,并将数据缓存到一个 Buffer (缓冲区) 中。

最后手机屏幕再从 Buffer 中读取数据显示到屏幕上。实际上真正对 Buffer 中数据进行合成显示的是 SurfaceFlinger。

在上图中,有一个典型的生产者/消费者模式, GPU 向缓冲区写数据,手机屏幕从缓冲区读数据。我们知道该模式是存在并发问题的,因为它符合并发的定义:多个线程同时操作共享变量。

为了避免这类问题,Android 引入了双缓冲机制:CPU 向 BackBuffer 中提交数据,GPU 对数据做栅格化,完成之后将其交换(swap)到 Frame Buffer 中,最后被手机屏幕从 FrameBuffer 中读栅格化后的数据。

看到这里,我们不难有个疑惑:交换(swap)操作是何时触发的? 我第一个想到的是 只要CPU有数据写入BackBuffer,GPU将数据栅格化后就执行swap。但是如果数据量比较大,GPU栅格化处理数据的时间期间,CPU又写入了数据,此时,就会产生矛盾。那么 Android 是如何实现的那?

由于GPU 合成的数据会经常更新,所以它会定期的交换BackBuffer和FrameBuffer的数据,从而保证屏幕上显示的是最新数据。如果当 CPU 正在向 BackBuffer 写入数据时,GPU 会将BackBuffer锁定。如果此时正好到了交换两个Buffer的时候,那么这次swap会被忽略放弃,直接导致的结果就是屏幕上显示的依然是上一帧的数据,这就是我们常数的丢帧。因此,为了保证App能够流畅运行,我们需要在每帧 16ms(屏幕刷新率,跟硬件有关) 以内处理完所有的 CPU和GPU 的计算,绘制,渲染等操作。

Vsync

由于屏幕刷新率和GPU绘制帧率并不一定是一致的,且View绘制的时机也是随机的(调用requestLayout或invalidate的时机不固定),因此也会存在丢帧的情况。

screen refresh rate–屏幕刷新率。指的是手机屏幕每秒钟可以刷新多少次。目前在大多数的厂商手机上的屏幕刷新率是 60HZ,也就是以 16.6ms 进行一次刷新。
frame rate – GPU 绘制帧率,指的是 GPU 每秒能够合成绘制多少帧。

到这里,我们不难想到,万一CPU与GPU的操作开始的比较晚,最新的数据没有交换到 FrameBuffer,那屏幕拿到的还是前一帧的数据,岂不是要丢帧?是的,确实会丢帧。

那么怎么解决这个问题那? 首先,我们看一下出现这个问题的根本原因是什么 —— CPU与GPU 的工作开始太晚!! 那么我们只需要让他们开始的时间同屏幕刷新保持一致不就可以了。那么怎么实现的那。

Android为了解决上面的问题,引入了 Vsync 机制。每隔 16ms 硬件层发出 Vsync 信号,应用层接收到 Vsync 信号后触发 UI 的渲染流程,同时 Vsync 也会触发 SurfaceFlinger 读取BackBuffer数据进行合成,完成后GPU 进行缓冲区的交换操作,最终屏幕从 FrameBuffer中读取最新数据。

问题
应用层是怎么接收到 Vsync 信号的?

Choreographer

Choreographer翻译过来是 编舞者, 它 负责接收硬件层发出的 Vsync 信号并进行 View 绘制的操作。

ViewRootImpl.java
    @Override
        public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                checkThread();
                mLayoutRequested = true; // 与 invalidate 的区别
                scheduleTraversals();
            }
        }
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 向MessageQueue 发出一个消息屏障,阻塞同步消息
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); 
            *****
        }
    }
Choreographer.java

    private final CallbackQueue[] mCallbackQueues; // 

    private Choreographer(Looper looper, int vsyncSource) {
        mLooper = looper;
        mHandler = new FrameHandler(looper);
        ------
        // mCallbackQueues 是一个固定容量为4的CallbackQueue类型数组
        mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
        for (int i = 0; i <= CALLBACK_LAST; i++) {
            mCallbackQueues[i] = new CallbackQueue();
        }
    }

    public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }
    public void postCallbackDelayed(int callbackType,
            Runnable action, Object token, long delayMillis) {
        ----
        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }
    private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            // 将 任务加入回调队列中。可以去看下源码,同 handler 的message一样 callbackQueue也使用链表实现了对象池
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
            
            if (dueTime <= now) { // 如果到期时间到了
                scheduleFrameLocked(now);
            } else { // 向MessageQueue发送一个异步消息。至于为什么是异步消息,你猜?
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }
    private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true; // 一次操作只能进入一次
            if (USE_VSYNC) { // 使用 vsync 信号
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } else {
                final long nextFrameTime = Math.max(
                        mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
                Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, nextFrameTime);
            }
        }
    }
    // 注册 vsync 信号
    private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync(); // 执行了DisplayEventReceiver 中的scheduleVsync(),内部调用了一个native方法
        
    }
    // 接收到 vsync 信号后的处理
    void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        // 1. 进行掉帧逻辑计算,并添加用于性能分析的 Trace 日志;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // no work to do
            }

            long intendedFrameTimeNanos = frameTimeNanos;
            startNanos = System.nanoTime();
            final long jitterNanos = startNanos - frameTimeNanos;
            if (jitterNanos >= mFrameIntervalNanos) {
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                    Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                            + "The application may be doing too much work on its main thread.");
                }
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                frameTimeNanos = startNanos - lastFrameOffset;
            }

            if (frameTimeNanos < mLastFrameTimeNanos) {
                scheduleVsyncLocked();
                return;
            }

            if (mFPSDivisor > 1) {
                long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
                if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
                    scheduleVsyncLocked();
                    return;
                }
            }

            mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
            mFrameScheduled = false;
            mLastFrameTimeNanos = frameTimeNanos;
        }

        // 2. 执行各种 callbacks并做性能跟踪
        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); // 执行输入事件回调任务,例如:点击

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); // 执行animation 回调任务

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); // 执行刷新UI任务

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }
    
// vsync 信号的接收器 
FrameDisplayEventReceiver.java
    // 接收 vsync 信号的回调
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
            if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
                scheduleVsync();
                return;
            }

            long now = System.nanoTime();
            if (timestampNanos > now) {
                timestampNanos = now;
            }

            if (mHavePendingVsync) {

            } else {
                mHavePendingVsync = true;
            }

            mTimestampNanos = timestampNanos;
            mFrame = frame; // 帧号
            // 发送一个异步定时消息,在 run 方法中执行
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame);
        }
    

总结

  1. 当我们刷新UI 时,会向Choreographer维护的 mCallbackQueues 中添加type为 CALLBACK_TRAVERSAL 的一个 CallbackQueue,接着会订阅一次 vsync 信号。Android硬件会 每隔 16.6ms 会向订阅 vsync 信号的接收者 发送一个 vsync 信号,这样做的好处比较明显,当UI 没有发生改变时,我们不需要注册 vsync 信号,也就不会执行刷新操作。
  2. Choreographer 起到了一个承上启下的作用
    • 承上 接收应用层的各种 callback 任务,包括 input, animation, traversal绘制,这些任务不会立即执行,而是先被缓存到 mCallbackQueues 中
    • 启下 内部类 FrameDisplayEventReceiver 接收来自硬件层的 vsync 信号,然后在 doFrame 中执行 mCallbackQueues 中的 callback 任务,例如 traversal 事件,就会执行 ViewRootImpl 中的 mTraversalRunnable,最后执行绘制流程。

注意:每次订阅只能收到一次 vsync 信号,如果需要收到下次信号则需要重新订阅。比如 Animation 的实现就是在订阅一次信号之后,紧接着再次调用 nativeScheduleVsync 方法订阅下次 vsync 信号,因此会不断地刷新 UI。

Systrace 工具

看到上面Android整个刷新屏幕的机制后,我最大的感触就是:时间,时间,时间。 既然 Systrace 是目前使用最广的UI性能分析工具,那它肯定把每一个方法的执行时间都记录了下来,我们只需要在网页文件中,找到耗时最长的一些方法,然后去优化他们,就能解决性能部分问题。

卡顿常出现的场景

  1. 在 animation 监听回调中创建对象或执行了比较耗时的计算操作,或者 animation 有开始没有结束
  2. 在 自定义view 的draw方法创建对象或执行了比较耗时的计算操作
  3. 在 自定义 view 的measure或layout中执行了耗时操作
  4. 在 执行decodeBitmap时用时比较长。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值