blog

invalidateのソースコード解析を見る

Viewをカスタマイズする場合、Viewを更新するメソッドを使用するのが一般的ですが、この記事ではこれらのメソッドの実装を分析します。オーバーロードされたメソッドはいくつかありますが、最終的な実装は似...

Apr 8, 2020 · 13 min. read
シェア

Viewをカスタマイズする場合、Viewをリフレッシュするためにinvalidateメソッドを使用するのが一般的です。この記事では、invalidateの実装を分析します。invalidateにはいくつかのオーバーロードされたメソッドがありますが、最終的な実装は似ているので、ここではinvalidate()の分析から始めます。

frameworks/base/core/java/android/view/View.java
/**
* 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()}.
*/
//ビュー全体を無効にすると、ビューのonDrawが呼び出される、つまり、ビューを再描画する処理が発生する。
public void invalidate() {
 invalidate(true);//trueつまり、リフレッシュ・キャッシュも無効にする必要がある。
}
//trueこれは、リフレッシュ・キャッシュを無効にする必要があることを意味し、falseに設定されている場合は、ビューのコンテンツまたはサイズが変更されていないことを意味する。
void invalidate(boolean invalidateCache) {
 if (skipInvalidate()) {//この処理をスキップするかどうかは、描画をスキップする条件を満たす必要がある:ビューが表示されておらず、現在のアニメーションが実行されていない。
 return;
 }
 /*
 * 条件を更新することができる:
 * 1: 描画が必要で、境界が設定されている
 * 2現在の描画キャッシュはまだ有効だが、それを無効にする必要がある。
 * 3ビューは無効化される。
 * 4: 透明度の変更
 * */
 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 || isOpaque() != mLastIsOpaque) {
 mLastIsOpaque = isOpaque();
 mPrivateFlags &= ~PFLAG_DRAWN;//描画マーカーをリセットする
 mPrivateFlags |= PFLAG_DIRTY;//マーク PFLAG_DIRTY これは、ビューが無効化されたことを意味する。
 if (invalidateCache) {//キャッシュを無効にする必要がある
 mPrivateFlags |= PFLAG_INVALIDATED;//マーカーの失敗
 mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;//有効なマーカーのキャッシュをクリアする。
 }
 final AttachInfo ai = mAttachInfo;//ビューのアタッチ情報を取得する。
 final ViewParent p = mParent;//親ビュー
 //noinspection PointlessBooleanExpression,ConstantConditions
 if (!HardwareRenderer.RENDER_DIRTY_REGIONS) {
 if (p != null && ai != null && ai.mHardwareAccelerated) {//ハードウェア・アクセラレーションが有効になっている。
 // fast-track for GL-enabled applications; just invalidate the whole hierarchy
 // with a null dirty rect, which tells the ViewAncestor to redraw everything
 p.invalidateChild(this, null);//ハードウェアアクセラレーションが有効な場合、ビューのすべての領域がダーティとしてマークされ、viewRootはすべてのコンテンツを再描画する。
 return;
 }
 }
 //ハードウェア・アクセラレーションが有効になっていない。
 if (p != null && ai != null) {
 final Rect r = ai.mTmpInvalRect;
 r.set(0, 0, mRight - mLeft, mBottom - mTop);//Rectをビューのサイズに設定する。
 // Don't call invalidate -- we don't want to internally scroll
 // our own bounds
 p.invalidateChild(this, r);//親ビューのinvalidateChildメソッドを呼び出す。
 }
 }
}

mRight-mLeftはViewの幅、mBottom-mTopはViewの高さであるためです。ViewParentは、実際には現在のビューの親Viewまたはビュー階層のViewRootであるため、invalidateChildはViewGroupまたはViewRootImplに送られます。続きを読む

public final void invalidateChild(View child, final Rect dirty) {
 ViewParent parent = this;
 final AttachInfo attachInfo = mAttachInfo;
 if (attachInfo != null) {
 final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION)
 == PFLAG_DRAW_ANIMATION;//child viewアニメーションが実行されているかどうか
 Matrix childMatrix = child.getMatrix();
 final boolean isOpaque = child.isOpaque() && !drawAnimation &&
 child.getAnimation() == null && childMatrix.isIdentity();//リクエストを開始したビューが不透明であろうとなかろうと、アニメーションを実行するビューは不透明とはみなされない。
 int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;//child view onDrawのダーティ・フラグは
  
 //ダーティ領域の始点の座標
 final int[] location = attachInfo.mInvalidateChildLocation;
 location[CHILD_LEFT_INDEX] = child.mLeft;
 location[CHILD_TOP_INDEX] = child.mTop;
  
 
 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);
  
 } while (parent != null);
 }
}

invalidateChildは2つのパラメータを受け取ります。invalidateを開始した子ビューがダーティ領域で、ダーティ領域の開始点は親ビューの子ビューの左上隅の座標です。whileループでは、アニメーションの実行を無視し、現在の親ビューにダーティマーカーを設定します。子ビューのビューを記録するために、親ビューのこのダーティなマーカーは、その後の描画は、onDraw描画を介してビューを描画する必要があるかどうかを判断するためにマークで使用され、その後invalidateChildInParentメソッドを呼び出し、親ビューの現在の親ビューに戻り、ViewRootImplまで。このループは、実際には、Viewの親Viewの起動からViewRootの実行invalidateChildInParent処理までのView階層であり、その間にinvalidateChildInParentは実際に何かを行うのでしょうか?invalidateChildInParentに渡されるのは、現在の親Viewの左上にあるダーティエリアの座標と、ダーティエリアです。

// /frameworks/base/core/java/android/view/ViewGroup.java
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;//同時にViewParent PFLAGを設定する。_DRAWING_CACHE_VALID0としてマークされる。
 //位置を更新する
 location[CHILD_LEFT_INDEX] = left;
 location[CHILD_TOP_INDEX] = top;
 if (mLayerType != LAYER_TYPE_NONE) {
 mPrivateFlags |= PFLAG_INVALIDATED;
 mLocalDirtyRect.union(dirty);
 }
 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);//現在のVeiwGroup領域とマージする。
 }
 if (mLayerType != LAYER_TYPE_NONE) {
 mPrivateFlags |= PFLAG_INVALIDATED;//ViewGrouponMeasureとonLayoutのマークアップは無効になる。
 mLocalDirtyRect.union(dirty);
 }
 return mParent;
 }
 }
 return null;
}

invalidateChildInParentでは、まず現在の親ビューのオフセットによってダーティエリアが計算されます。開始ダーティエリアは無効化を開始した子ビューで、次に親ビューの左上隅です。次に、FLAG_CLIP_CHILDRENが設定されているかどうか、つまりレイアウトファイルでandroid:clipChildrenが設定されているかどうかを判断してダーティエリアを計算します。この属性はデフォルトでtrueに設定されています。CLIP_CHILDRENが設定されている、つまりレイアウトファイルでandroid:clipChildrenが設定されており、デフォルトではtrueに設定されている場合、この属性は子ビューが描画領域の親ビューにあることを制限するために使用されます。falseに設定すると、FLAG_CLIP_CHILDRENが設定されていない場合、子ビューが親ビューの描画領域を超えても良いことを意味します。この場合、ダーティ領域は現在の親ビューの領域に設定されます。それ以外の場合、FLAG_CLIP_CHILDREN が設定されていると、ダーティ領域は現在のダーティ領域と親ビューの領域の交点に従って設定されます。交点がない場合、dirtyは空になります。次に、現在の親ビューの左上隅の位置を更新して、親ビューのダーティエリアのオフセットの次の計算に備えます。1層の計算の後、最終的にViewRootImplのinvalidateChildInParentに戻ります。

// /frameworks/base/core/java/android/view/ViewRootImpl.java
@Override
 public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
 checkThread();
 if (dirty == null) {//ダーティエリアは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);
 }
 }
 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
 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();
 }
 
 /*ViewRootImpl同じViewParentでも、itとViewGroup invalideChildInParentの違いは以下の通りである。
 * ViewRootImplダーティ領域がカウントされた後、schedualTraversalsが実行され、描画処理が行われる。*/
 if (!mWillDrawSoon && (intersected || mIsAnimating)) {
 scheduleTraversals();
 }
 return null;
}

checkThreadは更新スレッドがuiスレッドであるかどうかをチェックします。もしdirty areaがnullであれば、ビューツリー全体を描画する必要があることを意味し、もしdirty areaがnullであるか、アニメーションが実行されていなければ、それ以上進む必要はありません。mCurScrollY が null でない場合は、ページがスクロールされたことを意味するので、それに応じてダーティ領域を再計算する必要があります。ダーティ領域は、localDrity内の現在のビューツリーのダーティ領域に追加されます。次に、現在のダーティエリアとページ全体の領域が交差されます。通常、intersectedはtrueであり、これは交差があることを意味します。

// /frameworks/base/core/java/android/view/ViewRootImpl.java
 private void performDraw() {
 boolean fullRedrawNeeded = mFullRedrawNeeded;
 mFullRedrawNeeded = false;
  
 try {
 draw(fullRedrawNeeded, updateTranformHint);
 } finally {
 mIsDrawing = false;
 Trace.traceEnd(Trace.TRACE_TAG_VIEW);
 }
  
 }
 private void draw(boolean fullRedrawNeeded, boolean updateTranformHint) {
 Surface surface = mSurface;
 if (!surface.isValid()) {//surfaceもし無効であれば
 return;
 }
  
 final Rect dirty = mDirty;
 //ページ全体を描画する場合は、dirtyを画面全体のサイズに設定する。
 if (fullRedrawNeeded) {
 attachInfo.mIgnoreDirtyState = true;
 dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
 }
  
 //再描画領域が空でないか、アニメーションが実行されている。
 if (!dirty.isEmpty() || mIsAnimating) {
 //ハードウェア・アクセラレーションが有効になっている
 if (attachInfo.mHardwareRenderer != null && attachInfo.mHardwareRenderer.isEnabled()) {
  
 }else{
  
 //ソフトウェアが描画領域を汚く描写する
 if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) {
 return;
 }
 }
 
 }
 }
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,
 boolean scalingRequired, Rect dirty) {
 Canvas canvas;
 try {
 int left = dirty.left;
 int top = dirty.top;
 int right = dirty.right;
 int bottom = dirty.bottom;
  
 //再描画領域に従ってキャンバスを取得する。
 canvas = mSurface.lockCanvas(dirty);
 }catch(...){
  
 }
 try{
 mView.draw(canvas);//view 
 }finally{
  
 surface.unlockCanvasAndPost(canvas);
  
 }
 return true;
}

これは実際には draw(boolean fullRedrawNeeded, boolean updateTranformHint) メソッドの呼び出しであり、非ハードウェアアクセラレーションシナリオでは内部的にDrawSoftwareはこのメソッドの内部で呼び出され、描画を行います。 ここでのダーティエリアは、実際には現在のビューツリーの計算によるダーティエリアであることに注意してください。もちろん、ビューの無効化を呼び出した後に計算されたダーティ領域も含まれます。このダーティエリアをmSurfaceにクロップエリアを設定してCanvasを返すことで、その後の描画はこのCanvasのクロップエリアで行われます。

Viewのドロー描画プロセスは以下のように構成されています:

  1. Draw the background
  2. If necessary, save the canvas' layers to prepare for fading
  3. Draw view's content
  4. Draw children
  5. If necessary, draw the fading edges and restore layers
  6. Draw decorations
public void draw(Canvas canvas) {
  
 final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
 (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); //サブビューが不透明でアニメーションしないかどうか。
  
 background.draw(canvas);
 if (!dirtyOpaque) onDraw(canvas);//子ビューが不透明な場合、現在のビューを覆ってしまうのでスキップする。
 dispatchDraw(canvas);
 onDrawScrollBars(canvas);
  
}

描画処理でonDrawをコールバックするかどうかはdirtyOpaqueによって決定され、dirtyOpaqueはフラグPFLAG_DIRTY_OPAQUEが設定されているかどうかによって決定されます。このフラグは、上記のinvalidateChild?このフラグは、子ビューが不透明であり、アニメーションしていないことを示します。したがって、子ビューは親ビューの上にあり、現在のビューを上書きするので、この時点でビューを描画する必要はありません。ViewGroupは、コンテナコントロールとして、デフォルトでは何も描画せず、透明なコントロールであることに注意してください。

dispatchDrawプロセスは、サブビューを描画するために使用されます。

// /frameworks/base/core/java/android/view/ViewGroup.java
 @Override
protected void dispatchDraw(Canvas canvas) {//サブビューの描画
 final int count = mChildrenCount;
 final View[] children = mChildren;
  
 if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
 for (int i = 0; i < count; i++) {
 final View child = children[i];
 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {//子ビューが表示されているか、アニメーションしている
 more |= drawChild(canvas, child, drawingTime);//サブビューの描画
 }
 }
 } else {
 for (int i = 0; i < count; i++) {
 final View child = children[getChildDrawingOrder(count, i)];
 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
 more |= drawChild(canvas, child, drawingTime);
 }
 }
 }
  
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
 return child.draw(canvas, this, drawingTime);
}

drawChildはviewの別のオーバーロードされたメソッドを呼び出します。

// /frameworks/base/core/java/android/view/View.java
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
  
 mPrivateFlags |= PFLAG_DRAWN;
 /*quickReject現在のビューの領域がキャンバスの切り抜き領域の外にあるかどうかを判断し、外にある場合はtrueを返し、現在のビューが描画をスキップすることを示す。*/ 
 if (!concatMatrix &&
 (flags & (ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS |
 ViewGroup.FLAG_CLIP_CHILDREN)) == ViewGroup.FLAG_CLIP_CHILDREN &&
 canvas.quickReject(mLeft, mTop, mRight, mBottom, Canvas.EdgeType.BW) &&
 (mPrivateFlags & PFLAG_DRAW_ANIMATION) == 0) {
 
 /* 1. view関連付けのない行列マトリックス
 * 2. view描画領域がキャンバスのトリミング領域の外にある。
 * 3. clipchildtrueに設定する
 * 4. アニメーションは実行されない
 * **/ 
 mPrivateFlags2 |= PFLAG2_VIEW_QUICK_REJECTED;
 return more;
 }
  
 if (!layerRendered) {
 if (!hasDisplayList) {
 // Fast path for layouts with no backgrounds
 if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
 mPrivateFlags &= ~PFLAG_DIRTY_MASK;
 dispatchDraw(canvas);//サブビューの描画
 } else {
 draw(canvas);//描画そのものとそのサブビュー
 }
 } else {
 mPrivateFlags &= ~PFLAG_DIRTY_MASK;
 ((HardwareCanvas) canvas).drawDisplayList(displayList, null, flags);
 }
 }
  
}

drawメソッドを見てわかるように、描画処理のたびにサブビューを描画する必要はなく、特にinvalidateをトリガーにビューを描画する場合は、Canvasがダーティエリアを元に切り出し領域を設定し、ViewGroupが描画する際にサブビューの領域が切り出し領域に入っているかどうかを判断し、入っていなければ描画する必要はありません。そうでない場合は、描画する必要がないので、そのまま返します。これはキャンバスのquickRejectによって決定されます。このフラグは、View が描画される必要があるかどうかを制御するために使用され、ViewGroups にはデフォルトで設定されています。onDrawは呼び出されません。描画させたい場合は、 setWillNotDraw(false) で PFLAG_SKIP_DRAW フラグをクリアします。これにより、ビュー(ViewGroup)の描画処理に入りますが、onDrawを呼び出せるかどうかは、以下のように判断する必要があります。


public void setWillNotDraw(boolean willNotDraw) {
 setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
void setFlags(int flags, int mask) {
  
 if ((changed & DRAW_MASK) != 0) {
 if ((mViewFlags & WILL_NOT_DRAW) != 0) {
 if (mBackground != null) {
 mPrivateFlags &= ~PFLAG_SKIP_DRAW;
 mPrivateFlags |= PFLAG_ONLY_DRAWS_BACKGROUND;
 } else {
 mPrivateFlags |= PFLAG_SKIP_DRAW;
 }
 } else {
 mPrivateFlags &= ~PFLAG_SKIP_DRAW;
 }
 requestLayout();
 invalidate(true);//invalidateによるリフレッシュ
 }
  
}

ここでは、invalidate のフローを分析します。invalidate は描画処理をトリガーしますが、onMeasure と onLayout はトリガーしないことに注意してください。

Read next

トランザクションの伝播

は、常にされている、トランザクションは、困難なポイントのビジネス開発であり、数え切れないほどの理論を読んで、常に私は良い覚えていると思う、覚えていることができますが、個人的に勉強するコードを書きに行かなかった、私はあなたが私と同じであるかどうかわからないそれ。遅かれ早かれ、ミックスのうち、今夜は意図的にトランザクションの理解と印象を深めるために、実際の生活の方法でトランザクションの伝播特性。実際には、それは同じことですが、偽の究極の心を実践しないでください。私はあなたがこの記事を読んだ後、それはノックに従うことが最善であると信じて...

Apr 8, 2020 · 3 min read