blog

ビューの無効化処理

nvalidateとnvalidateはどちらもViewの再描画を要求するためのAPIで、メインスレッドとサブスレッドで呼び出されます。 メソッド、続き......

Jan 15, 2021 · 11 min. read
シェア

前書き

invalidatepostInvalidateとの違い

public void postInvalidate() {
 postInvalidateDelayed(0);
}
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
 final AttachInfo attachInfo = mAttachInfo;
 if (attachInfo != null) {
 attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
 }
}
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
 Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
 mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

上のソースコードから、postInvalidateのサブスレッドが機能であることがすでにわかります。続いて、もう一つ見てみましょう。

public void handleMessage(Message msg) {
 switch (msg.what) {
 case MSG_INVALIDATE:
 ((View) msg.obj).invalidate();
 break;
 .....
 }
}

invalidateプロセス解析

では、invalidateがどのようにViewを再描画するのか見てみましょう。

フローチャート

旅の始まりはビューの無効パスから。

View#invalidate()メソッドを見てみましょう。

public void invalidate() {
 invalidate(true);
}
public void invalidate(boolean invalidateCache) {
 invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

invalidateはView#invalidateInternalメソッドを呼び出し、現在のViewの位置パラメータを渡します。

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
 boolean fullInvalidate) {
 
 	....
 
 //View見えるか見えないか、アニメーションの中にあるかないか
 if (skipInvalidate()) {
 return;
 }
 	// Viewのマークアップで複数の無効化を防ぐ。
 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)) {
 if (fullInvalidate) {
 mLastIsOpaque = isOpaque();
 mPrivateFlags &= ~PFLAG_DRAWN;
 }
				// ビューが再描画されることを示すフラグを設定する
 mPrivateFlags |= PFLAG_DIRTY;
				//キャッシュをクリアし、再描画が現在のViewによって開始されたことを示すフラグを設定する。
 if (invalidateCache) {
 mPrivateFlags |= PFLAG_INVALIDATED;
 mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
 }
 // 再描画が必要なView領域を親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);
 	// キーコードは、最大再描画領域を渡すために、親のViewメソッドを呼び出して、彼の描画領域を更新し、コアの目的の背後にあるすべてのこのメソッドは、キャンバスを計算する必要がある再描画領域の計算方法である
 p.invalidateChild(this, damage);
 }
 ...
 }
}

上記のコードでは、現在のViewの状態、再描画が必要かどうかを判断し、一連のマーカービットを設定します。親ViewのinvalidateChild(this, damage)メソッドを通じて、再描画が必要な領域が親Viewに渡されます。 次に、ViewGroup#invalidateChildメソッドを見てください。ここでは、メインコードのみを抜粋しています。

public final void invalidateChild(View child, final Rect dirty) {
 final AttachInfo attachInfo = mAttachInfo;
 if (attachInfo != null && attachInfo.mHardwareAccelerated) {
 // ハードウェア・アクセラレーション、ここでは気にする必要はない。
 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
 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.
 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
 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));
 }
 	// 以下は、セットチャイルドの描画領域をViewRootに達するまで走査するループである。
 do {
 View view = null;
 if (parent instanceof View) {
 view = (View) parent;
 }
						// アニメーション関連
 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
 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;
 }
 }
 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);
 }
}
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
 if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
 (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
 if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
 FLAG_OPTIMIZE_INVALIDATE) {
						// 子ビューのレイアウト位置が親ビューのレイアウト位置に変換される
 dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
 location[CHILD_TOP_INDEX] - mScrollY);
 if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
								// 描画された領域のセットをマージする
 dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
 }
 final int left = mLeft;
 final int top = mTop;
 if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
 if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
 dirty.setEmpty();
 }
 }
 mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
 location[CHILD_LEFT_INDEX] = left;
 location[CHILD_TOP_INDEX] = top;
 if (mLayerType != LAYER_TYPE_NONE) {
 mPrivateFlags |= PFLAG_INVALIDATED;
 }
 return mParent;
 } else {
 mPrivateFlags &= ~PFLAG_DRAWN & ~PFLAG_DRAWING_CACHE_VALID;
 location[CHILD_LEFT_INDEX] = mLeft;
 location[CHILD_TOP_INDEX] = mTop;
 if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
 dirty.set(0, 0, mRight - mLeft, mBottom - mTop);
 } else {
 // in case the dirty rect extends outside the bounds of this container
 dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
 }
 if (mLayerType != LAYER_TYPE_NONE) {
 mPrivateFlags |= PFLAG_INVALIDATED;
 }
 return mParent;
 }
 }
 return null;
}

上記のコードでは、offsetを使って再描画する子Viewの座標領域を親Viewの座標領域に変換します。その後、union を使用して、子 View と親 View の領域を設定し、描画する必要のある領域を取得します。次に、ViewRoot#invalidateChildInParentメソッド(ViewRootはViewではなく、ViewRootImplクラスのViewRoot実装)を見て、そのinvalidateChildInParentメソッドを確認します。

@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
	// スレッドがViewを作成したスレッドであるかどうか、つまり、Viewを作成したスレッドがこのViewRootImplを含んでいるかどうかをチェックする。
 checkThread();
 if (DEBUG_DRAW) Log.v(TAG, "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;
}
private void invalidateRectOnScreen(Rect dirty) {
 ...
 if (!mWillDrawSoon && (intersected || mIsAnimating)) {
		//キーコード、ViewTreeリスト
 scheduleTraversals();
 }
}

上記のコードは、再描画領域のチェックと同様にスレッドに入り、その後invalidateRectOnScreenメソッドが呼び出され、scheduleTraversals()メソッドが呼び出されます。

ViewRootImpl#scheduleTraversals()に移りましょう。

void scheduleTraversals() {
 if (!mTraversalScheduled) {
 mTraversalScheduled = true;
		// handlerメッセージパッシングによる描画要求
 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
 mChoreographer.postCallback(
 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
 if (!mUnbufferedInputDispatch) {
 scheduleConsumeBatchedInput();
 }
 notifyRendererOfFramePending();
 pokeDrawLockIfNeeded();
 }
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
 @Override
 public void run() {
 doTraversal();
 }
}
void doTraversal() {
 if (mTraversalScheduled) {
 mTraversalScheduled = false;
 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
 if (mProfile) {
 Debug.startMethodTracing("ViewAncestor");
 }
		// ViewTreeトラバーサルを実行する主なコード
 performTraversals();
 if (mProfile) {
 Debug.stopMethodTracing();
 mProfile = false;
 }
 }
}

ViewRootImpl#performTraversals()メソッドに移ります。

private void performTraversals() {
	...
 if (!cancelDraw && !newSurface) {
 if (!skipDraw || mReportNextDraw) {
 if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
 for (int i = 0; i < mPendingTransitions.size(); ++i) {
 mPendingTransitions.get(i).startChangingAnimations();
 }
 mPendingTransitions.clear();
 }
			// キーコード
 performDraw();
 }
 } 
	...
}
private void performDraw() {
 ...
	final boolean fullRedrawNeeded = mFullRedrawNeeded;
 mFullRedrawNeeded = false;
 mIsDrawing = true;
 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
 try {
		// キーコード
 draw(fullRedrawNeeded);
 } finally {
 mIsDrawing = false;
 Trace.traceEnd(Trace.TRACE_TAG_VIEW);
 }
 ...
}

formDraw()では、Viewが表示されているか、表示されているか、描画されているか、削除されたリストに存在するか、などを判定し、formDraw()を呼び出して描画を開始します。formDraw()では、ViewRootImplのdrawメソッドが再度呼び出され、メンバ変数mFullRedrawNeededから派生したfullRedrawNeededパラメータが渡され、Viewの全てを再描画する必要があるかどうかを示します。の描画ソースコードに移りましょう。

private void draw(boolean fullRedrawNeeded) {
 Surface surface = mSurface;
 ...
	// mDirtyを取得し、値は再描画される領域を示す。
 final Rect dirty = mDirty;
 if (mSurfaceHolder != null) {
 // The app owns the surface, we won't draw.
 dirty.setEmpty();
 if (animating) {
 if (mScroller != null) {
 mScroller.abortAnimation();
 }
 disposeResizeBuffer();
 }
 return;
 }
	// もしそうなら、ダーティ領域をフルスクリーンに設定する。
 if (fullRedrawNeeded) {
 mAttachInfo.mIgnoreDirtyState = true;
 dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
 }
 ...
	// 再描画領域、アニメーション判定
		// ハードウェアレンダリング判定
			// キーコード
 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
 return;
 }
 ...
}

drawメソッドで、fullRedrawNeededパラメータを渡して再描画が必要なダーティ領域を設定し、最後にdrawSoftwareメソッドを呼び出してパラメータを渡します。

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
 boolean scalingRequired, Rect dirty) {
 ...
 try {
 
 if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
 canvas.drawColor(0, PorterDuff.Mode.CLEAR);
 }
 dirty.setEmpty();
 mIsAnimating = false;
 mView.mPrivateFlags |= View.PFLAG_DRAWN;
 
 try {
 canvas.translate(-xoff, -yoff);
 if (mTranslator != null) {
 mTranslator.translateCanvas(canvas);
 }
 canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
 attachInfo.mSetIgnoreDirtyState = false;
			// キーコード、mView for DecorView、ビュー描画をオンにする
 mView.draw(canvas);
 drawAccessibilityFocusedDrawableIfNeeded(canvas);
 } finally {
 if (!attachInfo.mSetIgnoreDirtyState) {
 // Only clear the flag if it was not set during the mView.draw() call
 attachInfo.mIgnoreDirtyState = false;
 }
 }
 } 
	...
}

上記のコードでは、まずキャンバスのプロパティを設定します。その後、mView.draw(canvas)メソッドが呼び出され、Viewの描画が開始されます。mViewはウィンドウの最上位ビューDecorViewです(このピットについては後述しますが、ここでは最上位のViewGroupとして説明します)。

まとめ

無効化は、再描画が必要な領域をマージするためにカスケードアップと呼ばれ、ほとんどの場合、描画時に行われます。そのため、表示されるすべてのフレームは画面全体の再描画ではなく、ユーザーに表示されるビューを更新するために変更された領域の描画である可能性があり、このアルゴリズムによってパフォーマンスが大幅に向上します。

Read next

vscode eye protection green + python環境セットアップ

0 環境システム: win10 エディタ関連: python + anaconda インストール済み 1 アイプロテクショングリーン 1 プラグインのインストール設定

Jan 14, 2021 · 4 min read