前言
- 实验条件
AndroidStudio 3.2.1
jdk1.8
android-28源码 - 原因
这两天找解决项目bug的方法,忽然翻看到一位老铁的非 UI 线程能调用 View.invalidate()的奇异问题文章,感觉他说的不是问题之所在,就对Invalidate查看了一番。
读取文章:
Android中的ViewRootImpl类源码解析
安卓中invalidate和requestLayout的实现和区别
真理
1.节点非ViewRootImpl的直接子View的parent一定是ViewGroup,这点在从ViewParent只被ViewGroup与ViewRootImpl继承+invalidate中的一段源码可以体现出来,其结构必定如下图,凡是有child的节点必为ViewGroup。
2.
3.屏幕坐标是怎么样的,矩形计算可依据此坐标来
4.invalidate()调用层级关系图
源码中的方法
- postInvalidate
方法调用情况
View类:
postInvalidate()
→postInvalidateDelayed(0)→
ViewRootImpl类
→dispatchInvalidateDelayed(View view, long delayMilliseconds)
发送消息由Handler处理
/**
* Cause an invalidate to happen on a subsequent cycle through the event loop.
* Use this to invalidate the View from a non-UI thread.
*
* This method can be invoked from outside of the UI thread
* only when this View is attached to a window.
* 只有仅仅当此View附加到窗口时,才能从UI线程以外调用此方法
* @see #invalidate()
* @see #postInvalidateDelayed(long)
*/
public void postInvalidate() {
postInvalidateDelayed(0);
}
↓
postInvalidateDelayed(long delayMilliseconds)
/**
* Cause an invalidate to happen on a subsequent cycle through the event
* loop. Waits for the specified amount of time.
*
* This method can be invoked from outside of the UI thread
* only when this View is attached to a window.
*
* @param delayMilliseconds the duration in milliseconds to delay the
* invalidation by
*
* @see #invalidate()
* @see #postInvalidate()
*/
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
//我们仅仅尝试使用AttachInfo,因为如果没有附加的window是没有刷新节点的。
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
此处开始直接调用根视图(ViewRootImpl)的发送方法
↓
dispatchInvalidateDelayed(View view, long delayMilliseconds)
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
此时它将需要刷新的view使用消息发送给消息队列去处理刷新
↓
((View) msg.obj).invalidate();
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
postInvalidate感悟:通过以上代码可以看出postInvalidate()方法的原理其实最为简单,就是0s延时的给消息队列添加一个消息,然后由Handler处理,其最终也还是调用的invalidate,只不过因为handleMessage在UI线程运行,所以我们平时可以在子线程中直接使用。
- invalidate()
invalide()方法调用情况
View类:
invalidate()
→ invalidate(true)
→ invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true)
→ parent.invalidateChild(this, damage)→
ViewGroup类:
→ invalidateChild(View child, final Rect dirty)
→ parent.invalidateChildInParent(location, dirty)
此处出现分支:1.未到达根布局调用VG的invalidateChildInParent 2.到达根布局调用ViewRootImpl的invalidateChildInParent
→ 1、invalidateChildInParent(final int[] location, final Rect dirty) 直接向上循环至根布局
→2、invalidateChildInParent
正常遍历到根布局后
→ invalidateRectOnScreen(Rect dirty)
→ *scheduleTraversals()传入一个runnable给编排类,时机到时编排类安排绘制
→ *
invalide()
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
*
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*/
public void invalidate() {
invalidate(true);
}
↓
invalidate(boolean invalidateCache)
/**
* This is where the invalidate() work actually happens. A full invalidate()
* causes the drawing cache to be invalidated, but this function can be
* called with invalidateCache set to false to skip that invalidation step
* for cases that do not need it (for example, a component that remains at
* the same dimensions with the same content).
这是invalidate()工作实际发生的地方。 完全invalidate()会导致绘图缓存失效,
但可以将invalidateCache设置为false来调用此函数,以跳过不需要它的情况下的无效步骤
(例如,与该节点保持相同维度的组件) 相同的内容)
* @param invalidateCache Whether the drawing cache for this view should be
* invalidated as well. This is usually true for a full
* invalidate, but may be set to false if the View's contents or
* dimensions have not changed.
invalidateCache字段决定此视图的绘图缓存是否也应该无效。 通常完全刷新的话为true,
但如果视图的内容或尺寸未改变的话设置为false.
* @hide
*/
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
此处内部刷新方法参数mRight-mLeft,mBottom-mTop 为其当前 距离父控件右侧-距离父控件左侧,bottom-top同理,其实在为后面的刷新画一个脏区域即待刷新区域,待更新即为脏了,擦拭完就是新面向即为重绘,这个命名还是挺好理解的。
↓
invalidateInternal(l,t,r,b,invalidateCache,fullInvalidate)
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
if (mGhostView != null) {
mGhostView.invalidate(true);
//此处GhostView与当前视图的INVISIBLE属性相关,貌似是一个占位View,与当前视图显示状态相反
return;
}
//如果当前视图不可见且无动画(含父布局动画)正在执行,则跳过此次绘制刷新
if (skipInvalidate()) {
return;
}
当前View没有正在执行该方法或绘制缓存可用或未重绘过或透明度发生改变
//PFLAG_DRAWN会在该方法内去改标志位
//PFLAG_INVALIDATED会在View.draw()方法执行时去掉该标志位
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
//默认为true,除非开发者手动调用invalidate(false)
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
// Propagate the damage rectangle to the parent view.
此处使用的是传入的Rect,是当前控件距离parent的左、上、右、下距离形成待刷新的Rect脏区域
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);
从此处开始View将这次刷新的操作交接给ViewGroup类去处理,因为其直接父节点必为ViewGroup,
也只有ViewGroup与ViewRootImpl继承了ViewParent拥有处理孩子刷新的能力,此处印证了此文的【真理1】。
}
// Damage the entire projection receiver, if necessary.
if (mBackground != null && mBackground.isProjected()) {
final View receiver = getProjectionReceiver();
if (receiver != null) {
receiver.damageInParent();
}
}
}
}
/**
* Do not invalidate views which are not visible and which are not running an animation. They
* will not get drawn and they should not set dirty flags as if they will be drawn*/
当前视图不可见或者没有正在运行的动画(包含父布局的过度动画)则不刷新视图。
如果它们将要绘制将不会获得绘制,脏区域标记也不会被设置。
private boolean skipInvalidate() {
return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
(!(mParent instanceof ViewGroup) ||
!((ViewGroup) mParent).isViewTransitioning(this));
}
↓
开始进入ViewGroup类中方法
invalidateChild(View child, final Rect dirty)
/**
* Don't call or override this method. It is used for the implementation of
* the view hierarchy.
*
* @deprecated Use {@link #onDescendantInvalidated(View, View)} instead to observe updates to
* draw state in descendants.
刷新重绘操作可能对子View的内容,尺寸,相对坐标进行处理
*/
@Deprecated
@Override
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
// HW accelerated fast path
onDescendantInvalidated(child, child);
return;
}
ViewParent parent = this;
if (attachInfo != null) {
// If the child is drawing an animation, we want to copy this flag onto
// ourselves and the parent to make sure the invalidate request goes
// through
如果传进来的child正在执行动画,我们希望将此标志复制到我们自己和parent身上,以确保刷新请求通过
final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0;
// Check whether the child that requests the invalidate is fully opaque
// Views being animated or transformed are not considered opaque because we may
// be invalidating their old position and need the parent to paint behind them.
isOpaque布尔值由三个条件决定:1.child不透明 2.child没有动画在执行也没有与之相关联的动画
3.child的视图矩阵未发生旋转、缩放、透视
Matrix childMatrix = child.getMatrix();
final boolean isOpaque = child.isOpaque() && !drawAnimation &&
child.getAnimation() == null && childMatrix.isIdentity();
// Mark the child as dirty, using the appropriate flag
// Make sure we do not set both flags at the same time
使用适当的标志将child标记为脏区域。确保我们不会同时设置两个标志
int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;
if (child.mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
final int[] location = attachInfo.mInvalidateChildLocation;
location[CHILD_LEFT_INDEX] = child.mLeft;
location[CHILD_TOP_INDEX] = child.mTop;
矩阵发生改变则根据改变后的矩阵调整脏区域
if (!childMatrix.isIdentity() ||
(mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
Matrix transformMatrix;
if ((mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
Transformation t = attachInfo.mTmpTransformation;
boolean transformed = getChildStaticTransformation(child, t);
if (transformed) {
transformMatrix = attachInfo.mTmpMatrix;
transformMatrix.set(t.getMatrix());
if (!childMatrix.isIdentity()) {
transformMatrix.preConcat(childMatrix);
}
} else {
transformMatrix = childMatrix;
}
} else {
transformMatrix = childMatrix;
}
transformMatrix.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
}
此处开始逐层向上遍历
do {
View view = null;
上面说其非根View的直接child,parent必为ViewGroup,那么此处判断为View其实就是为了判断根视图的,
为ViewRootImpl(此类非View,仅仅为一个桥梁)准备的,最后一次它就是View,也是在最后一次做的主线程检查
if (parent instanceof View) {
view = (View) parent;
}
如果子View正在执行动画,设置遍历的父布局View的动画标识
if (drawAnimation) {
if (view != null) {
view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
} else if (parent instanceof ViewRootImpl) {
((ViewRootImpl) parent).mIsAnimating = true;
}
}
// If the parent is dirty opaque or not dirty, mark it dirty with the opaque
// flag coming from the child that initiated the invalidate
若这个parent是不透明的脏区域或者不是脏区域,发起刷新操作的child就使用不透明标记将其标记为脏区域。
由于此处parent给child刷新绘制的方法,我理解其实就是将当前的区域强行设置为脏区域去刷新
if (view != null) {
if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
view.getSolidColor() == 0) {
opaqueFlag = PFLAG_DIRTY;
}
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
}
}
设置完标记开始执行绘制刷新操作并返回当前View的parent,通过此循环无意外的话一直向上遍历至根布局,
返回ViewRootImpl对象,然后跳出循环,有意外的话无法到达ViewRootImpl且parent返回为null跳出此循环,
结束invalidate()方法调用的刷新操作,location为AttachInfo类里大小为2的存储以当前为parent的child 的x、y数组,
分别存储left,top即为child距离parent的x,y坐标值
parent = parent.invalidateChildInParent(location, dirty);
if (view != null) {
// Account for transform on current parent
Matrix m = view.getMatrix();
if (!m.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
m.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
}
}
} while (parent != null);
}
}
↓
invalidateChildInParent(final int[] location, final Rect dirty)
/**
* Don't call or override this method. It is used for the implementation of
* the view hierarchy.
*
* This implementation returns null if this ViewGroup does not have a parent,
* if this ViewGroup is already fully invalidated or if the dirty rectangle
* does not intersect with this ViewGroup's bounds.
*
* @deprecated Use {@link #onDescendantInvalidated(View, View)} instead to observe updates to
* draw state in descendants.
*/
@Deprecated
@Override
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
// either DRAWN, or DRAWING_CACHE_VALID
如果ViewGroup有没有动画执行或者动画已经完成
if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
!= FLAG_OPTIMIZE_INVALIDATE) {
dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
location[CHILD_TOP_INDEX] - mScrollY);
如果当前ViewGroup需要裁剪其child,则将当前ViewGroup的区域与脏区域区域做求并集的操作
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
final int left = mLeft;
final int top = mTop;
//如果当前ViewGroup需要裁剪其child,且ViewGroup区域与脏区域没有交集,则dirty置空
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
dirty.setEmpty();
}
}
此处赋值为下一次循环做准备下一次循环时,当前VG即为child,记载的为当前VG相对于它的parent的距离
location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;
} else {
如果有动画正在执行,可能是缩放动画
若当前VG需要裁剪child
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
脏区域就设置为当前VG大小即可
dirty.set(0, 0, mRight - mLeft, mBottom - mTop);
} else {
// in case the dirty rect extends outside the bounds of this container
不需要裁剪child则刷新绘制区域变为脏区域与VG并集区域
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
location[CHILD_LEFT_INDEX] = mLeft;
location[CHILD_TOP_INDEX] = mTop;
mPrivateFlags &= ~PFLAG_DRAWN;
}
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
}
return mParent;
}
return null;
}
由于此处方法一旦返回null就意味着上面的do-while循环结束,所以如果此处未到ViewRootImpl就返回为null的话会导致线程 检测失效,直接表现为我们可以在子线程更新UI却不被报错,不过虽然不报错,却也容易引发UI“假死”系统也无任何ANR等反应的现象。
跳出循环的3个条件(也是子线程更新报错失效的条件):
1.parent为null
2.待刷新脏区域与当前ViewGroup区域不重叠(人为手动调用invalidate(rect)的方法貌似能做到)
3.ViewGroup已经完全失效
↓
invalidateChildInParent(int[] location, Rect dirty)
循环无意外最后一次执行时到达ViewRootImpl,将使用ViewRootImpl的invalidateChildInParent方法
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
此处便是子线程刷新UI时调用的线程检测,子线程更新UI抛出的异常也是它干的
checkThread();
if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
if (mCurScrollY != 0 || mTranslator != null) {
mTempRect.set(dirty);
dirty = mTempRect;
if (mCurScrollY != 0) {
dirty.offset(0, -mCurScrollY);
}
if (mTranslator != null) {
mTranslator.translateRectInAppWindowToScreen(dirty);
}
if (mAttachInfo.mScalingRequired) {
dirty.inset(-1, -1);
}
}
invalidateRectOnScreen(dirty);
return null;
}
//检查当前方法是否位于主线程运行
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
Rect类:
/**
* Set the rectangle to (0,0,0,0)
*/
public void setEmpty() {
left = right = top = bottom = 0;
}
↓
invalidateRectOnScreen(Rect dirty)
private void invalidateRectOnScreen(Rect dirty) {
final Rect localDirty = mDirty;
if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
mAttachInfo.mSetIgnoreDirtyState = true;
mAttachInfo.mIgnoreDirtyState = true;
}
// Add the new dirty rect to the current one
取根布局与脏区域的并集成为新的脏区域
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
// Intersect with the bounds of the window to skip
// updates that lie outside of the visible region
拿到兼容模式的缩放比值,计算脏区域是否与当前窗口兼容缩放后的区域相交,如果不想交则将脏区域大小设置为0
final float appScale = mAttachInfo.mApplicationScale;
final boolean intersected = localDirty.intersect(0, 0,
(int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
if (!intersected) {
localDirty.setEmpty();
}
若非正在执行performTraversals(mWillDrawSoon只有在performTraversals()方法开始时置为true,
结束时置false,此处判断即为判断performTraversals()是否正在执行)&&
此根布局与脏区域不相交 || 动画正在执行则延迟遍历刷新
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
此方法后续补充
scheduleTraversals();
}
}
↓
scheduleTraversals()
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
此处调用Handler的同步屏障,同步屏障是一个无target的Message,使用同步屏障用于
立即推迟所有后续发布的同步消息执行,直到调用removeSyncBarrier释放屏障,
但此时异步消息会继续执行,相当于将同步信息阻塞去执行异步消息,
mTraversalBarrier为同步屏障的token,用于释放它,不释放会导致程序挂起
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
ViewRootImpl内部类
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
mTraversalRunnable 在编导类里被发送给FrameHandler处理,才开始做遍历
↓
做遍历,在获取到vsync信号后,便会调用到peformTraversals()方法,invalidate()调用只会走performDraw()->draw()由ViewTreeObserver调用dispatchOnDraw()分发绘制,
layout与mesure被if语句过滤掉了
vsync游戏玩家可以理解为垂直同步,当整个屏幕刷新完毕,即一个垂直刷新周期完成,会有短暂的空白期
doTraversal()
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//执行遍历代码有824行左右,行数太大无法在此贴入代码。
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
Choreographer类(编导类)[1]
/**
* Posts a callback to run on the next frame.
* The callback runs once then is automatically removed.
* @param callbackType The callback type.
* @param action The callback action to run during the next frame.
* @param token The callback token, or null if none.
* @see #removeCallbacks
* @hide
*/
@TestApi
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
相关好文:
源码分析篇 - Android绘制流程(三)requestLayout()与invalidate()流程及Choroegrapher类分析
View绘制流程及源码解析(一)——performTraversals()源码分析
-
编排类is what?
协调动画事件、输入事件和绘制事件。编排类从显示子系统接收定时脉冲(例如垂直同步),然后安排相关执行工作作为下一个帧渲染的一部分。
基本上这个类就是利用vsync信号对一些事件进行编排使其按屏幕刷新周期有序执行,按帧绘制
这里是其它博客的概括【 “编舞类”Choreoprapher的作用是编排输入事件、动画事件和绘制事件的执行,通过调用Choreoprapher.postCallback()方法,向Choreoprapher加入需要编排的事件,而Choreoprapher则通过请求Vsync信号,来控制这些事件按照屏幕刷新周期有规律的执行,即是实现了按帧绘制的机制。】
应用程序通常使用动画框架或视图层次结构中的更高级抽象来间接地与编舞者交互,以下是使用更高级别API可以执行的操作的一些示例:
①.要在与显示帧渲染同步的情况下定期发布要处理的动画,使用android.animation.ValueAnimator #start。
②.要在下一帧前发送一次Runnable,使用View#postOnAnimation。
③.要在延迟后发布一个Runnable以在下一个显示帧的开头调用一次,使用View#postOnAnimationDelayed。
④.要在下一帧开始前调用一次View#incalidate(),请使用View#postInvalidateOnAnimation()或View#postInvalidateOnAnimation(int,int,int,int)。
④.为了确保View的内容平滑滚动并与显示帧渲染同步绘制,请不执行任何操作。 这已经自动发生了。 View#onDraw将在适当的时候调用。
但是,在某些情况下,您可能希望直接在应用程序中使用编排器的功能。 这里有些例子:
①.如果您的应用程序在不同的线程中进行渲染(可能使用GL),或者根本不使用动画框架或视图层次结构,并且您希望确保它与显示器正确同步,那么请使用{@link Choreographer#postFrameCallback}。
每个Looper的宿主线程都有自己的编排器。 其他线程可以发布回调以在编排器上运行,但是它们将在编排器所属的Looper上运行。 ↩