卡顿优化①Android UI渲染和刷新机制

丢帧和卡顿

卡顿,是字面意思上来讲,就是画面不流畅,即页面刷新不连贯。Android系统默认的页面刷新频率是60帧,每秒刷新60次,即屏幕上的画面16.6ms刷新一次,这个频率是由手机设备的屏幕硬件来控制的。如果16.6ms没有完成一次刷新,就造成了丢帧。大部分的App偶尔间歇性地出现几次丢帧,不会造成明显的卡顿,只有连续或者短时间多次出现丢帧,就会让用户感觉到明显的卡顿现象。

UI渲染机制

手机屏幕由一个个的像素点组成,如1920x1080分辨率的屏幕就表示在屏幕上横向每行有1080个像素点,纵向每列有1920个像素点。也可以想象成二维数组。数组中的每个数值,代表对应的像素点显示的颜色。屏幕画面每隔16.6ms刷新一次,也就是更新每个像素点要显示的颜色。而这个16.6ms的刷新频率,也就是60FPS,是由手机屏幕硬件来控制的。

系统是依据什么来确定每个像素点应该显示的颜色呢?由此引出了Frame Buffer的概念。Frame Buffer是系统的帧缓冲区,可以理解为屏幕显示的抽象。
image.png
结合上面的图示,每一个Surface都对应一个Buffer Queue,由SufaceFlinger管理。Buffer Queue内部包含两个Graphic Buffer。页面刷新时,绘制内容最终经过栅格化后存放在Graphic Buffer中。其中Front Buffer里存放的是页面当前正在显示这一帧的内容。页面需要更新时,将先下一帧要显示的内容存放在Offscreen Buffer,再通过Swap Buffer复制到Front Buffer中。

同时可能会有多个Surface,它们可能来自不同的应用,也可能是同一个应用里面类似SurfaceView和TextureView,它们也都有自己单独的Surface。屏幕刷新时,SufaceFlinger从每一个Surface的Front Buffer中,拿到要显示的内容。然后将所有Surface要显示的内容,统一交给Hardware Composer,它会根据位置,Z-Order顺序等信息合成最终要显示显示在屏幕上的内容,而这个内容就会交给系统的帧缓冲区Frame Buffer来显示。也就是说,系统根据Frame Buffer中的内容来确定每一个像素点在每一帧要显示的颜色。
至此,我们了解了Frame Buffer的作用,并由此引出了Surface,SurfaceFlinger,Graphic Buffer等图形组件的概念。这里引用一篇文章中的比喻来让大家对整个图形绘制系统的整体架构有个大概的了解,然后再进一步深入了解。
如果把应用程序的页面渲染过程当做是一次绘画过程,那么绘画过程中Android的各个图形组件的作用是:

  • 画笔:Skia或者OpenGL。我们可以用Skia画笔来绘制2D图形,也可以用OpenGL画笔来绘制2D/3D图形。前者使用CPU绘制,或者使用GPU绘制。
  • 画布:Surface。所有的内容元素都在Surface这张画纸上进行绘制和渲染。在Android中,Window是View的容器,每一个Window都会关联一个Surface。而WindowManager负责管理这些Window,并把他们的数据传递给SurfaceFlinger。
  • 画板:Graphic Buffer。Graphic Buffer缓冲用于应用程序的页面绘制,在Android4.1之前使用的是双缓冲机制。Android4.1之后,使用的是三缓冲机制。
    显示:SurfaceFlinger。它将WindowManager提供的所有Surface,通过硬件合成器Hardware Composer合成并输出到显示屏。

CPU和GPU

整个UI渲染机制主要依赖三个硬件:CPU、GPU和屏幕。上面已经介绍过了。下图则展示了在UI渲染过程中,CPU和GPU分别负责的工作。
image.png

UI组件绘制到屏幕之前,都需要经过栅格化(Rasterization)操作,而栅格化操作又是一个相对耗时的操作。相比于CPU,GPU更擅长处理图形运算,可以加快栅格化过程。这也是通常所说的硬件加速绘制的原理,将CPU不擅长的图形计算转换为GPU的专有指令来处理。

Android3.0之前,或者没有启动硬件加速时,系统会使用软件方式来渲染UI,即上图中的软件绘制。
image.png
整体流程是,系统遍历Window中的每个View,执行其onDraw方法,在onDraw方法中,通过传入的Canvas对象,执行各自的绘制逻辑。这个Canvas对象是通过Window关联的Surface的lock函数获取的,Canvas可以理解为Skia底层接口的封装。View的绘制工作完成后,通过Canvas以及Skia将绘制内容栅格化到Graphic Buffer(也就是前面提到的Offscreen Buffer)中。然后经过Swap Buffer过程后,把Front Buffer里的内容交给SurfaceFlinger,最后由硬件合成器Hardware Composer合成并放入Frame Buffer,最终输出到屏幕上。

硬件加速绘制

在软件绘制过程中,由于CPU对图形计算的处理不是那么高效,这个过程完全没有利用到GPU在图形处理方面的优势。所以在Android 3.0之后,Android开始支持硬件加速绘制。到Android 4.0时,系统会默认开启硬件加速。
image.png

硬件加速绘制与软件绘制的整个流程差异很大,最核心的就是硬件加速绘制是通过OpenGL ES的接口调用,由GPU完成栅格化操作,然后将绘制内容放入Graphic Buffer。此外硬件加速绘制还引入了DisplayList的概念。每个View内部都有一个DisplayList,当某个View需要重绘(比如调用invalidate方法)时,将DisplayList标记为Dirty。这样当页面刷新时,就只需要重绘一个View的DisplayList,而不是像软件绘制那样触发整个视图层级的遍历。从而减少了需要重绘的View的数量,提高了绘制渲染的效率。
image.png

Peoject Butter

Google 在 2012 年的 I/O 大会上宣布了Project Butter黄油计划,在Android4.1上,对Android Display系统进行了重构,引入了三个核心要素:VSYNC、Tripe Buffer和Choreographer。

VSYNC

VSYNC信号,可以理解为一种定时中断,是一种在PC上已经很早就广泛使用的技术。由系统底层控制VSYNC信号的发送频率。由于大部分屏幕硬件的刷新频率都是60FPS,所以VSYNC信号的发送频率也是60FPS,即系统底层16.6ms发出一个VSYNC信号。每次收到VSYNC信号,CPU会立即开始计算需要绘制的数据(可以理解为执行View的measure、layout、draw的过程),这也是应用的页面下一帧绘制的开始,然后由GPU对数据进行栅格化并填充到Offscreen Buffer。收到VSYNC信号的同时,SurfaceFlinger开始收集每一个Surface的Front Buffer中的内容,交给Hardware Composer进行合成并输出到Frame Buffer,最终完成屏幕刷新。也就是说系统每次发出VSYNC信号时,CPU开始进行下一帧绘制的准备,最终由GPU完成下一帧显示内容的绘制处理。而SurfaceFlinger则完成的是当前这一帧内容的显示操作。这就是双缓冲机制的原理。
image.png
Tripe Buffer

Tripe Buffer,即三重缓冲机制,三个Graphic Buffer。如果你理解了双缓冲机制的原理,就可以想象一下这样一个问题。当前页面正在显示Front Buffer的内容,GPU正在往Offscreen Buffer填充下一帧的显示内容,而在GPU进行这一操作时,会将Offscreen Buffer锁定,如果CPU和GPU的绘制处理过程耗时太长,超过了一个VSYNC信号周期,就会导致本该进行Swap Buffer操作时,因为Offscreen Buffer被锁定,无法正常进行Swap Buffer,从而导致Front Buffer里的内容也不能更新,还是保留上一帧的内容。从而出现丢帧现象。
image.png

如果再提供一个缓冲区,CPU、GPU 和显示设备都能使用各自的缓冲区工作,互不影响。简单来说,三缓冲机制就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用VSYNC信号周期的空闲时间,带来的坏处是多使用了一个 Graphic Buffer 所占用的内存。
image.png
ChoreoGrapher

ChoreoGrapher,主要作用是接受VSYNC信号。系统发出VSYNC信号的频率是60FPS,那是不是意味着,不管App的页面是否需要刷新,都会接收VSYNC信号,然后开始由CPU准备下一帧绘制需要的数据呢?其实不然,如果App的页面不需要刷新,App就不会接收到VSYNC信号。只有当页面需要刷新时,才会由ChoreoGrapher来执行一个类似注册监听VSYNC信号的操作,然后当系统下一次发出VSYNC信号时,ChoreoGrapher就会接收到VSYNC信号,来执行页面绘制的相关工作。可能这样的说法有点抽象,接下来通过介绍ChoreoGrapher的相关源码来解释说明它的作用。
了解View的绘制机制的同学都知道,当页面有视图变化,需要刷新时,都会执行到ViewRootImpl的requestLayoiut方法,很多介绍View绘制原理的文章,都以requestLayout方法作为页面绘制过程真正的开始点。它内部又会调用scheduleTraversals方法。

//ViewRootImpl.java
@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            //mLayoutRequested用来辅助判断是否需要执行View的measure和layout过程
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //向主线程的Messagequeue中添加同步屏障,目的是让页面绘制的相关任务能尽快执行
            //页面绘制的相关任务是以异步消息的方式发到主线程,在添加同步屏障之后,异步消息的任务将优先执行
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

在scheduleTraversals方法中,我们看到了mChoreographer。此处调用了它的postCallback方法。第一个参数表示回调类型,第二个参数表示一个待执行的任务。

//Choreographer.java
@TestApi
    public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }
@TestApi
    public void postCallbackDelayed(int callbackType,
            Runnable action, Object token, long delayMillis) {
        if (action == null) {
            throw new IllegalArgumentException("action must not be null");
        }
        if (callbackType < 0 || callbackType > CALLBACK_LAST) {
            throw new IllegalArgumentException("callbackType is invalid");
        }

        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }
private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        if (DEBUG_FRAMES) {
            Log.d(TAG, "PostCallback: type=" + callbackType
                    + ", action=" + action + ", token=" + token
                    + ", delayMillis=" + delayMillis);
        }

        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

如上述代码所示,经过层层调用,完成实际工作的是postCallbackDelayedInternal方法,主要完成两件事:
第一,将postCallback方法传进来的待执行任务,封装成一个Callback,保存在mCallbackQueues队列中,mCallbackQueues是CallbackQueue类型的数组。CallbackQueue是Choreographer的内部类,其实质是一个单向链表,其中的每一个Callback按dueTime的先后排序。

//Choreographer#CallbackQueue
@UnsupportedAppUsage
        public void addCallbackLocked(long dueTime, Object action, Object token) {
            CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
            CallbackRecord entry = mHead;
            if (entry == null) {
                mHead = callback;
                return;
            }
            if (dueTime < entry.dueTime) {
                callback.next = entry;
                mHead = callback;
                return;
            }
            while (entry.next != null) {
                if (dueTime < entry.next.dueTime) {
                    callback.next = entry.next;
                    break;
                }
                entry = entry.next;
            }
            entry.next = callback;
        }

第二件事就是向主线程中添加一个MSG_DO_SCHEDULE_VSYNC类型的异步消息,根据duetime的时序判断是直接添加还是在指定的dutime时间添加,而添加MSG_DO_SCHEDULE_VSYNC消息的工作就是在scheduleFrameLocked方法中完成

//Choreographer.java
private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            // mFrameScheduled保证16ms内,只会申请一次垂直同步信号
            // scheduleFrameLocked可以被调用多次,但是mFrameScheduled保证下一个vsync到来之前,不会有新的请求发出
            // 多余的scheduleFrameLocked调用被无效化
            mFrameScheduled = true;
            if (USE_VSYNC) {
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "Scheduling next frame on vsync.");
                }

                // If running on the Looper thread, then schedule the vsync immediately,
                // otherwise post a message to schedule the vsync from the UI thread
                // as soon as possible.
                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);
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
                }
                Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, nextFrameTime);
            }
        }
    }

首先,需要注意的是mFrameScheduled,它是当前Choreographer是否正在监听VSYNC的标识,同时也能防止重复监听,Choreographer每次接收到VSYNC信号时,将mFrameScheduled置为false,当需要监听VSYNC信号时,再将mFrameScheduled置为true。这就意味着Choreographer每次接收到VSYNC信号,处理完后续逻辑,之后监听下一个VSYNC信号时,需要重新注册。
根据代码中的注释,方法开头的USE_VSYNC用来区分是否启用了vysnc机制,默认为true。我们主要关注的也是USE_VSYNC为true的情况。同样根据代码中的注释,如果当前线程是主线程就直接执行scheduleVsyncLocked方法,否则就通过异步消息的方式,让主线程执行scheduleVsyncLocked方法

ChoreoGrapher.java
@UnsupportedAppUsage
    private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }

mDisplayEventReceiver是抽象类DisplayEventReceiver的对象。它是来定义SYNC信号的接收器。它的主要功能有两个:“注册”VSYNC信号的监听和接收VSYNC信号。它的scheduleVsync方法内部会调用nativeScheduleVsync方法,这个native方法是最终实现VSYNC信号监听的"注册"。这里不再对nativeScheduleVsync进一步分析,所以这里说的注册,并不一定和Android的BroadcasrReceiver机制的注册一个概念。有兴趣的同学可以继续深入到native方法中进一步了解。

@UnsupportedAppUsage
    public void scheduleVsync() {
        if (mReceiverPtr == 0) {
            Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                    + "receiver has already been disposed.");
        } else {
            nativeScheduleVsync(mReceiverPtr);
        }
    }

ChoreoGrapher的内部类FrameDisplayEventReceiver,继承自DisplayEventReceiver,重写了onVsync方法。而从DisplayEventReceiver源码的注释可以得知,onVsync方法就是接收到VSYNC信号的回调方法。由于FrameDisplayEventReceiver实现了Runnable接口,可以将其当做是一个可执行任务。而FrameDisplayEventReceiver的onVsync方法,就做了一件事,就是向主线程发送异步消息,在主线程中执行它的run方法。

//Choreographer#FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;

        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
            super(looper, vsyncSource);
        }

        // TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
        // the internal display and DisplayEventReceiver#scheduleVsync only allows requesting VSYNC
        // for the internal display implicitly.
        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
            // Post the vsync event to the Handler.
            // The idea is to prevent incoming vsync events from completely starving
            // the message queue.  If there are no messages in the queue with timestamps
            // earlier than the frame time, then the vsync event will be processed immediately.
            // Otherwise, messages that predate the vsync event will be handled first.
            long now = System.nanoTime();
            if (timestampNanos > now) {
                Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                        + " ms in the future!  Check that graphics HAL is generating vsync "
                        + "timestamps using the correct timebase.");
                timestampNanos = now;
            }

            if (mHavePendingVsync) {
                Log.w(TAG, "Already have a pending vsync event.  There should only be "
                        + "one at a time.");
            } else {
                mHavePendingVsync = true;
            }

            mTimestampNanos = timestampNanos;
            mFrame = frame;
            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);
        }
    }

如上述代码所示,在主线程中通过执行FrameDisplayEventReceiver的run方法,也就是执行Choreographer的doFrame方法,其中需要重点关注的相关代码如下

//Choreographer.java
@UnsupportedAppUsage
    void doFrame(long frameTimeNanos, int frame) {
            ...
            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);
            doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
            ...
    }

这个Choreographer.CALLBACK_TRAVERSAL很眼熟,在ViewRootImpl的schduleTraversals方法中调用Choreographer的postCallback方法时传入的第一个参数也是Choreographer.CALLBACK_TRAVERSAL。

//Choreographer.java
void doCallbacks(int callbackType, long frameTimeNanos) {
            ...
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
            if (callbacks == null) {
                return;
            }
            ...
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "RunCallback: type=" + callbackType
                            + ", action=" + c.action + ", token=" + c.token
                            + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
                }
                c.run(frameTimeNanos);
            }finally {
            synchronized (mLock) {
                mCallbacksRunning = false;
                do {
                    final CallbackRecord next = callbacks.next;
                    recycleCallbackLocked(callbacks);
                    callbacks = next;
                } while (callbacks != null);
            }
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
            ....
}

在doCallbacks方法中,根据callbackType也就是Choreographer.CALLBACK_TRAVERSAL。取出对应的CallbackQueue。在schduleTraversals调用postCallback方法时传入的第二个参数mTraversalRunnable,就保存在上述代码的某一个CallbackRecord中,查看CallbackRecord的run方法,其实就是调用了封装在其中的action的run方法。从以上分析,这里的action,就是在mTraversalRunnable。分析到这一步,就可以回到ViewRootImpl的代码中了。

//ViewRootImpl.java
final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

mTraversalRunnable的run方法,调用了doTraversal方法。到此,熟悉View的绘制过程的同学,应该就很清楚接下来的流程了。doTraversal内部会调用performTraversals方法。在performTraversals方法中会,执行当前页面窗口对应的DecorView的整个视图层级的绘制流程。

//ViewRootImpl.java
void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

总结

本文主要介绍了

  • 卡顿和丢帧的概念
  • UI渲染机制的原理,其中涉及到的重要组件是Canvas、Surface、Graphic Buffer、和SurfaceFlinger。
  • 软件绘制和硬件加速绘制的概念和区别。CPU和GPU在绘制渲染过程中,各自完成的工作。以及Skia库和OpenGL ES库的简单介绍。
  • Project Butter的三要素,VSYNC信号和Triple Buffer的原理,着重从源码角度分析了Choreographer的工作机制。

本文参考
Android开发高手课 UI 优化(上):UI 渲染的几个关键概念
Android 屏幕刷新机制

你可能感兴趣的:(卡顿优化①Android UI渲染和刷新机制)