Project Butter与invalidate

一、Project Butter

google希望摆脱android UI交互方面的滞后、卡顿问题,在android4.1提出了Project Butter,力争UI如黄油般丝滑。

为了确保一致的帧速率,Android 4.1 将 Vsync 计时扩展到了所有由 Android 框架完成的绘图和动画中。包括应用渲染、轻触事件、画面构成和显示刷新在内的一切操作均按照 16 毫秒的 Vsync 检测信号同步运行,因此帧不会提前或延迟。

Android 4.1 还在图形管道中添加了三重缓冲,以实现更加一致的渲染,让滚动、分页和动画等更加流畅。

Android 4.1 通过以下两种方式降低轻触延迟:将轻触同步到 Vsync 计时,以及实际预测屏幕刷新时您的手指所在的位置。轻触延迟降低后,轻触响应速度会更快、更一致。此外,在处于不活动状态一定时间后,Android 会在下一次轻触事件发生时应用 CPU 输入增强,以确保不会出现延迟。

1.1 Vsync 计时

android使用cpu进行测量和布局,使用gpu进行绘制和渲染,最终效果在屏幕上显示。由于绘制渲染和屏幕显示都是按分辨率像素点逐行进行的,需要一定的时间消耗, 如果绘制渲染和显示使用同一个缓冲区buffer,那么就会同时进行渲染和显示,界面显示两个及以上不同帧数据最终导致界面撕裂。

所以,android界面渲染和显示一般采用双缓冲机制,分为Back Buffer和Frame Buffer,基本原理就是采用两块buffer,Back Buffer也称离屏缓冲区用于CPU/GPU后台绘制,Frame Buffer用户前台屏幕显示,当Back Buffer数据准备好后,他们才会交换内存地址。

 

android绘制的双缓冲机制确实解决了屏幕撕裂问题,但是这里面还有一个问题,交换内存地址的时机选择。什么时候交换数据屏幕开始显示准备好的数据并且CPU/GPU开始准备下一帧数据呢?假定CPU/GPU绘制频率大于界面的绘制频率,如果CPU/GPU不知道当前界面是否显示完成,CPU/GPU绘制完第一帧数据后等了很久才去绘制第二帧数据,那么这期间屏幕上只能一直显示这个多余的(Jank)第一帧数据,出现界面的掉帧、卡顿现象。

如何解决这个场景呢?这就需要设定一个时间间隔定时的告知CPU/GPU可以进行数据交互并准备绘制下一帧数据了,假定CPU/GPU绘制频率大于界面的绘制频率,那么这个时间间隔就取决于界面显示所消耗的时间。这里就引入两个概念:行频(Horizontal scanning)和场频(Vertical scanning,行频又称“水平扫描频率”代表屏幕每秒钟从左到右扫描的次数;场频也称“垂直扫描频率”代表每秒钟整个屏幕刷新的次数。当扫描完整个屏幕设备重新回到第一行开始下轮扫描,这中间有个间隙,称为Vertical Blanking Interval(VBI),如果在这个时间间隔去交换buffer效果最佳。Vsync(Vertical synchronization,垂直同步)就是利用VBI时期的Vertical Sync pulse来保证双缓冲交换的最佳时间点。

1.2 三重缓冲

Vsync可以解决双缓冲机制下PU/GPU绘制频率大于界面的绘制频率的场景,那么如果CPU/GPU绘制频率小于界面的绘制频率呢?

如上图所示,约定上图未显示第一个Vsync信号,那么图中的第一个Vsync信号实际上是第二个Vsync信号,依次递增。当界面显示第一帧数据时,CPU/GPU在准备第二帧数据,但是GPU准备第二帧数据时耗时比较就,在第二个Vsync信号到来时还未准备好,Back Buffer被锁定,那么界面只能继续显示第一帧数据,当GPU准备好第二帧数据时却没有Vsync信号,这就导致CPU和很大一部分GPU资源浪费。终于到了第三个Vsync信号到来,此时终于显示之前超时的第二帧数据了,同时CPU/GPU在准备第四帧数据,和之前一样,GPU准备第四帧数据又超时了,那么第四个Vsync信号到来时又只能显示第二帧数据,这就导致再次卡顿。从中可以发现规律,由于CPU/GPU绘制频率小于界面的绘制频率,基本上每隔一个Vsync信号就卡一次,这就导致界面刷新频率降低的一半。

如何解决呢?那么如果两个缓冲不行,那就再加一个缓冲区,三个缓冲区应该可以了吧。看下图

图中可以看到,由于CPU/GPU绘制频率小于界面的绘制频率,生产者的生产速度不能满足消费者的消费速度了,三缓冲情况是Frame Buffer还是一个,Back Buffer变成了两个,通过增加生产者数量来满足消费者。当第二个Vsync信号到来时发现GPU还没绘制完到当前Back Buffer,那么就启动备选的第三个Back Buffer重新开启一次新的绘制,虽然第二帧Vsync信号到来时还是只能显示第一帧数据,但是由于适时增加了一个Back Buffer使得后面就不再卡顿了(可以被打脸一次,但是不会一直被打脸啦)。

二、invalidate流程

讲到invalidate流程先要解释几个关键类。

1.1 SurfaceFlinger 

SurfaceFlinger 接受来自多个源的数据缓冲区,然后将它们进行合成并发送到显示屏。

在屏幕处于两次刷新之间时,屏幕会向 SurfaceFlinger 发送 VSYNC 信号。当 SurfaceFlinger 接收到 VSYNC 信号后,SurfaceFlinger 会遍历其层列表(层是 Surface 和 SurfaceControl 实例的组合),以查找新的缓冲区。如果 SurfaceFlinger 找到新的缓冲区,SurfaceFlinger 会获取缓冲区;否则,SurfaceFlinger 会继续使用上一次获取的那个缓冲区。

1.2 Hardware Composer HAL (HWC)

硬件混合渲染器,Hardware Composer HAL (HWC) 确定使用可用硬件合成缓冲区的最有效的方法,作为 HAL,其实现是特定于设备的,而且通常由显示硬件原始设备制造商 (OEM) 完成。

1.3 Choreographer

Choreographer(编舞者)可以周期性的接收到屏幕发出的Vsync信号,然后知道动画、输入以及绘制。ValueAnimator.start()View#postOnAnimation等方法底层实现就是Choreographer,Choreographer也经常与ViewRootImpl一起协作,当然你可以可以直接使用Choreographer.getInstance().postFrameCallback()定制一些你自己的绘制流程。下一节通过View.invalidate方法展示Choreographer与ViewRootImpl协作渲染的流程。

1.4 View.invalidate流程

1.4.1 invalidateInternal

View.invalidate先调用View.invalidateInternal方法,.invalidateInternal方法中调用父view的invalidateChild方法,实际上最终的ViewParent实现就是ViewRootImpl。

//View.java
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {
    ……
        // Propagate the damage rectangle to the parent view.
        final AttachInfo ai = mAttachInfo;
        final ViewParent p = mParent;
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            p.invalidateChild(this, damage);
        }
    ……
}

1.4.2 invalidateChild

ViewRootImpl中的invalidateChild方法调用invalidateChildInParent方法,invalidateChildInParent方法调用了invalidateRectOnScreen方法,invalidateRectOnScreen方法调用了大家熟悉的scheduleTraversals()方法。

//ViewRootImpl.java
private void invalidateRectOnScreen(Rect dirty) {
    ……
    if (!mWillDrawSoon && (intersected || mIsAnimating)) {
        scheduleTraversals();
    }
}

1.4.3 scheduleTraversals

大家都知道scheduleTraversals()方法里面有一个TraversalRunnable类型的action用于执行View的测量、布局和绘制三大操作,但是这个action是如何触发的呢?这里调用了Choreographer的postCallback方法并把TraversalRunnable作为参数传过去。上面有提到Back buffer的绘制以及Frame buffer的显示都需要Vsync触发,那么这里的mChoreographer.postCallback应该就和Vsync信号申请有关了。

//ViewRootImpl.java
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); //设置同步屏障,同步消息被堵塞,只有异步消息可以执行
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); //调用mChoreographer申请Vsync
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending(); //通知下一帧的硬件渲染即将开始
        pokeDrawLockIfNeeded();
    }
}

 

1.4.4 postCallback

Choreographer的postCallback方法调用了零延迟的postCallbackDelayed方法,postCallbackDelayed又调用了postCallbackDelayedInternal方法,postCallbackDelayedInternal方法首先将TraversalRunnable放入到任务序列mCallbackQueues[callbackType]中,由于delayMillis传入的是0,所以这里会调用scheduleFrameLocked方法。

//Choreographer.java
private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long 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);
        }
    }
}

 

 

1.4.5 scheduleFrameLocked

scheduleFrameLocked方法会根据是否使用USE_VSYNC变量从而执行不同操作,USE_VSYNC变量值可以通过SystemProperties属性设置,如果使用USE_VSYNC那么就从硬件屏幕获取VSYNC信号触发后面的绘制工作,否则就软件模拟延迟一定时间去执行MSG_DO_FRAME消息,最终执行doFrame方法,实际上使用USE_VSYNC变量最终也是会执行doFrame方法。分析下使用USE_VSYNC的情况,根据当前是否在looper线程上执行又分为两个分支,实际上不管是通过scheduleVsyncLocked方法还是MSG_DO_SCHEDULE_VSYNC消息最终都会通过调用FrameDisplayEventReceiver对象的scheduleVsync方法来申请底层的VSYNC信号。scheduleVsync()方法会调用FrameDisplayEventReceiver的父类DisplayEventReceiver.java的native方法,逻辑比较复杂,这里就不展开了。

//Choreographer.java
// Enable/disable vsync for animations and drawing.
private static final boolean USE_VSYNC = SystemProperties.getBoolean(
        "debug.choreographer.vsync", true);
……
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        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);//调用doFrame方法
            msg.setAsynchronous(true);//设置异步
            mHandler.sendMessageAtTime(msg, nextFrameTime);
        }
    }
}

1.4.6 FrameDisplayEventReceiver

上面提到如果使用USE_VSYNC变量时会调用FrameDisplayEventReceiver对象的scheduleVsync方法来申请底层的VSYNC信号,FrameDisplayEventReceiver继承自抽象类DisplayEventReceiver并实现了onVsync方法,根据onVsync方法的注释可以看出onVsync方法在VSYNC脉冲到来时会被调用从而开始新一轮的数据帧渲染。所以,当VSYNC脉冲到来时FrameDisplayEventReceiver的onVsync方法会调用。onVsync方法会发起一个异步消息,这里FrameDisplayEventReceiver类自身实现了Runnable接口并且实现了run接口方法,所以这个handler最终调用的是FrameDisplayEventReceiver类自身的run()方法,真是一波骚操作,我不禁竖起了大拇指。那么最终doFrame方法会执行。

//Choreographer$FrameDisplayEventReceiver.java
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable {
……

    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
        ……
        mTimestampNanos = timestampNanos;
        mFrame = frame;
        Message msg = Message.obtain(mHandler, this);//第二个参数是自身,FrameDisplayEventReceiver自身实现了Runnable接口
        msg.setAsynchronous(true);//异步消息
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

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

1.4.7 doFrame

doFrame方法调用了四次doCallbacks方法并传入了不同的类型分别是Choreographer.CALLBACK_INPUT(对应输入事件,优先级最高)、Choreographer.CALLBACK_ANIMATION(动画事件,优先级次之)、Choreographer.CALLBACK_TRAVERSAL(绘制事件,优先级更低)、Choreographer.CALLBACK_COMMIT(完成事件,优先级最低)。这里主要分析Choreographer.CALLBACK_TRAVERSAL绘制事件。

//Choreographer.java
void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
   ……

    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);

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

        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    } finally {
        AnimationUtils.unlockAnimationClock();
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
   ……
}

 

1.4.8 doCallbacks

doCallbacks方法遍历取出了之前添加到1.4.4中添加到mCallbackQueues[callbackType]中的Runable任务,并挨个执行,那么1.4.3中的TraversalRunnable任务就会执行,我们熟悉的测量、布局、绘制开始启动。

//Choreographer.java
void doCallbacks(int callbackType, long frameTimeNanos) {
     CallbackRecord callbacks;
     ……
     try {
         Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
         for (CallbackRecord c = callbacks; c != null; c = c.next) {//遍历取出之前添加到1.4.4中添加到mCallbackQueues[callbackType]中的Runable任务
             if (DEBUG_FRAMES) {
                 Log.d(TAG, "RunCallback: type=" + callbackType
                         + ", action=" + c.action + ", token=" + c.token
                         + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
             }
             c.run(frameTimeNanos); //执行Runable任务
         }
     } finally {
         synchronized (mLock) {
             mCallbacksRunning = false;
             do {
                 final CallbackRecord next = callbacks.next;
                 recycleCallbackLocked(callbacks);
                 callbacks = next;
             } while (callbacks != null);
         }
         Trace.traceEnd(Trace.TRACE_TAG_VIEW);
     }
 }

 

 

 

 

 

 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值