我们都知道,Android是16ms刷新一帧,而通常我们所理解的刷新是“每个view的draw()方法被调用”,所以这里就有一个问题了,Android系统底层每隔16ms就发出一个垂直同步信号,那么是不是每个view的draw()方法都会每个16ms调用一次呢?如果这样的话系统消耗岂不是非常大?是不是有什么特殊优化手段?
1. 垂直同步信号的使用者——Choreographer
Choreographer是Android4.1和垂直同步信号机制一起引入的,我们都知道垂直同步信号其实是操作系统底层的一种时钟中断,那么java层是如何利用这个中断的呢?主要就是Choreographer这个类来协调接收的。
这里我们不会分析这个类的具体实现,主要简单的介绍下如何接收底层中断等一些简单的用法,便于大家理解后面的知识。
1.1 中断信号利用原则
由于中断信号时源源不断的,所以为了避免滥用中断信号,原则是:需要接收中断信号必须向系统注册一个接收者,下次产生了新的中断就会回调这个接受者的回调方法。注意,每次注册只能接收一次中断,想要继续接收必须重新注册。
1.2 中断信号接收者
首先我们看下这个信号接收者:
public abstract class DisplayEventReceiver {
//省略其他代码
/**
* Called when a vertical sync pulse is received.
* The recipient should render a frame and then call {@link #scheduleVsync}
* to schedule the next vertical sync pulse.
*
* @param timestampNanos The timestamp of the pulse, in the {@link System#nanoTime()}
* timebase.
* @param builtInDisplayId The surface flinger built-in display id such as
* {@link SurfaceControl#BUILT_IN_DISPLAY_ID_MAIN}.
* @param frame The frame number. Increases by one for each vertical sync interval.
*/
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
}
/**
* Schedules a single vertical sync pulse to be delivered when the next
* display frame begins.
*/
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);
}
}
//省略其他方法
}
这里我只列出了两个方法,onVsync就是中断会回调的方法,只会回调一次,如果希望接收下一次中断信号,就要手动调用scheduleVsync()方法。
1.3 Choreographer利用DisplayEventReceiver干了什么
// The display event receiver can only be accessed by the looper thread to which
// it is attached. We take care to ensure that we post message to the looper
// if appropriate when interacting with the display event receiver.
private final FrameDisplayEventReceiver mDisplayEventReceiver;
Choreographer有一个这样的成员变量,主要都是通过这个成员变量来接收中断信号的:
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);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
//省略其他代码
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);
}
}
可以看到,收到中断信号后向主线程Handler发送了一个消息,其实主要就是为了切换到主线程执行这里的run()方法, 换句话说每次中断信号来了最终都会回调到doFrame()方法,到了这里,Chogreographer是如何使用中断信号的就很清楚了,换句话说如果想要接收中断信号做些什么,我们只需要写在 doFrame()方法 里就好了。
1.4 如何利用doFrame()方法
现在我们知道了,利用Choreographer能够达到在垂直中断信号产生时回调doFrame()方法的目的,那么我们怎么将自己要执行的代码塞到doFrame()方法中去呢?
我们先看下doFrame()方法的源码:
void doFrame(long frameTimeNanos, int frame){
mFrameInfo.markInputHandlingStart();
//省略其他代码
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
//省略其他代码
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
//省略其他代码
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
//省略其他代码
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
//省略其他代码
}
可以看到doFrame()方法其实会执行一系列的callBack回调,我们可以将自己的任务塞到这些callBack中去得到执行,具体如下:
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
通过Choreographer.postCallback()方法,我们就可以让自己的Runnable在下一次垂直信号产生时得到执行。
2. UI绘制与刷新本质
UI界面的改变核心是一些会影响UI的变量的值的改变,这些值改变后我们接收垂直同步信号,在下一次信号中断产生时根据新的UI变量重新绘制当前界面即可做到UI的刷新。总结下主要是两点:
- UI布局变量的改变
- 注册垂直同步信号中断监听,在下一次垂直同步信号来临时重绘界面。
3. UI是如何绘制的
想要在垂直同步信号来临时重绘界面,我们必须先了解UI到具体如何绘制的。这里我就不再带领大家一步步探究,而是直接说出结论了。
Android 的ui是按照树型结构组织的,而这个树的根节点(DecorView)就是由一个ViewRootImpl持有,UI的树型遍历绘制也是由ViewRootImpl发起的。这里我们看下ViewRootImpl是如何调用DecorView的draw()方法的:
通过上面的调用图可以看到关键其实是scheduleTraversals()方法,他会通过Choreograpter.postCallback()方法注册一个回调,该回调能让整个UI树在下一次垂直同步信号来临时得到绘制。
4. 常用的刷新原理
现在我们回过头来看下我们经常用的刷新方法,主要是requestLayout()和invalidate()方法,这两个方法都会一直沿着UI树往上找,最终会调用到ViewRootImpl的scheduleTraversals()方法,这样就会在下一次垂直同步信号产生时重新绘制整个界面。
5. 结语
本文基本没有涉及什么代码,主要是重垂直同步信号的原理入手,宏观的介绍了UI的绘制与刷新原理,个人理解,如果有误,恳请指正。