Android invalidate是如何导致View重绘的

先来一张流程图


Android invalidate是如何导致View重绘的.jpg

我以前一直以为invalidate是刷新的意思,查了下词典才知道原来是废弃,使无效的意思。在Android中即意味着view的某个显示区域内容变脏了,该显示区域需要被重新绘制。

invalidate.png

invalidate方法是在View类中声明的。

/**
 * 废弃整个view。如果view可见,在将来的某个时间点view的
 * {onDraw(android.graphics.Canvas)} 方法将会被调用
 * 
 * 必须在主线程调用。在非UI线程请调用 {#postInvalidate()}.
 */
public void invalidate() {
    invalidate(true);
}

/**
 * 这里是invalidate() 工作真正发生的地方。一个完整的invalidate()会导致绘制缓存被废 
 * 弃。如果不需要废弃绘制缓存,调用此方法的时候可以将参数invalidateCache设为false
 * 来跳过废弃绘制缓存的步骤(例如,一个组件的尺寸和内容都没有改变的时候)。
 *
 * @param invalidateCache view的绘制缓存是否也要被废弃。true,表示完全废弃。
 *        false,不需要废弃绘制缓存
 */
public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

内部调用了invalidateInternal方法。View的invalidateInternal方法精简版。

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {

    //如果当前view不可见,并且没有在执行动画,直接return
    if (skipInvalidate()) {
            return;
        }   
    //...
   // 把需要重绘的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);
       //注释1处,调用parent的invalidateChild方法,向上传递重绘事件
        p.invalidateChild(this, damage);
    }

    //...
}

方法内部判断如果的主流程就是设置重绘区域,然后调用ViewParent的invalidateChild方法,并传入自身和重绘区域。

ViewParent的invalidateChild方法

/**
 * 所有或者部分子view需要被重新绘制
 * 
 * @param 需要被重新绘制的子view
 * @param r 子view废弃的矩形区域
 *
 * @deprecated Use {@link #onDescendantInvalidated(View, View)} instead.
 */
@Deprecated
public void invalidateChild(View child, Rect r);

ViewGroup和ViewRootImpl都实现了ViewParent的invalidateChild方法。普通的view的parent是ViewGroup。我们先看一下ViewGroup的实现。

ViewGroup的invalidateChild方法精简版

public final void invalidateChild(View child, final Rect dirty) {
    final AttachInfo attachInfo = mAttachInfo;
    //...

    ViewParent parent = this;
    if (attachInfo != null) {
        //...
        //保存子view的left和top
        final int[] location = attachInfo.mInvalidateChildLocation;
        location[CHILD_LEFT_INDEX] = child.mLeft;
        location[CHILD_TOP_INDEX] = child.mTop;
            
            //...
            //设置绘制区域
            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;
            if (parent instanceof View) {
                view = (View) parent;
            }
            //...      
            parent = parent.invalidateChildInParent(location, dirty);
            if (view != null) {
                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);
    }
}

在上述方法中,设置了需要重绘的区域dirty。之后再do...while方法中,反复的调用parent = parent.invalidateChildInParent(location, dirty)方法,来调用父类的invalidateChildInParent对View的重绘请求进行传递。

ViewParent的invalidateChildInParent方法。

/**
 * 所有或者部分子view需要被重新绘制。
 *
 * location数组保存了两个int值,分别代表需要重绘的子view的左和上的位置。
 *
 * <p>如果指定的矩形区域必须在当前ViewParent中被废弃, 那么这方法必须返回当前
* ViewParent的parent。如果指定的矩形区域不需要在当前ViewParent中被废弃,
 * 或者当前ViewParent对象的parent不存在,那么这个方法必须返回null。 
 *
 * 如果此方法返回了一个非null值,那么这个loaction数组一定被更新为当前ViewParent左和上的坐标了。
 * @param location 一个有两个值的数组,保存要被重绘的子view的左和上的坐标。
 * @param r 子view要被重绘的矩形区域
 *
 * @return 当前ViewParent的parent或者null。
 * @deprecated Use {@link #onDescendantInvalidated(View, View)} instead.
 */
@Deprecated
public ViewParent invalidateChildInParent(int[] location, Rect r);

ViewGroup和ViewRootImpl都实现了ViewParent的invalidateChildInParent方法。我们先看一下ViewGroup的实现。

public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
    if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
        //...
        //返回mParent
        return mParent;
    }

    return null;
}

在这里我们直接认定ViewGroup会返回mParent。如果当前ViewGroup对象是DecorView了。那么DecorView返回的mParent就不是ViewGroup了,而是ViewRootImpl。

ViewRootImpl实现了ViewParent

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
        //...
}

ViewRootImpl的invalidateChildInParent(View child, Rect dirty)方法

@Override
public void invalidateChild(View child, Rect dirty) {
    invalidateChildInParent(null, dirty);
}

ViewRootImpl的invalidateChildInParent(int[] location, Rect dirty)方法精简版

@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    //注释1处,检查线程
    checkThread();
    //...
    //注释2处
    invalidateRectOnScreen(dirty);
    return null;
}

在上面方法的注释1处,检查当前线程是否是view的原始线程。

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
      "Only the original thread that created a view hierarchy can touch its views.");
    }
}

这个异常信息也是很眼熟啊。

注释2处,调用了ViewRootImpl的invalidateRectOnScreen方法。

private void invalidateRectOnScreen(Rect dirty) {
     //...
    if (!mWillDrawSoon && (intersected || mIsAnimating)) {
       //调用scheduleTraversals方法
       scheduleTraversals();
    }
}

ViewRootImpl的scheduleTraversals方法

void scheduleTraversals() {
   if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //注释1处
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //注释2处
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
           //...
    }
}

在注释1处,向消息队列发送了一个同步屏障,这样,looper会暂停处理同步消息,优先处理异步消息。直到同步屏障被解除。在注释2处,调用Choreographer的postCallback发送了一个异步消息。这样做的原因是防止消息队列中堆积大量的同步消息,导致我们的重绘操作延迟,造成界面更新不及时。在异步消息被处理完毕后会解除同步屏障,恢复正常的消息处理。

我们注意一下我们postCallback发送的mTraversalRunnable对象。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

//mTraversalRunnable对象
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

当我们的异步消息被处理的时候,mTraversalRunnable对象的run方法会被执行,从而调用doTraversal方法。

void doTraversal() {
   if (mTraversalScheduled) {
         mTraversalScheduled = false;
         //注释1处,移除同步屏障
         mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        //注释2处
        performTraversals();
        //...
    }
}

在上面的注释1处,移除了同步屏障,在注释2处调用了ViewRootImpl的的performTraversals方法。

ViewRootImpl的的performTraversals方法精简版

private void performTraversals() {

   performMeasure();
   performLayout();
   performDraw();

}

我们只看performDraw方法。

private void performDraw() {
    //...
    try {
        //调用draw方法
        boolean canUseAsync = draw(fullRedrawNeeded);
        if (usingAsyncReport && !canUseAsync) {
            mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null);
            usingAsyncReport = false;
        }
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    //...
}

内部调用了ViewRootImpl的draw方法。

private boolean draw(boolean fullRedrawNeeded) {
        
   //...
   //注释1处
   if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
        return false;
    }
}

注释1处,调用了ViewRootImpl的drawSoftware方法。

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
        boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
         
     //注释1处
     mView.draw(canvas);
}

注释1处,调用DecorView的draw方法,DecorView是继承FrameLayout的,这个时候开始就进入了常规的ViewGroup的绘制流程。

参考链接

  1. Android 自定义 View——invalidate 传递与绘制流程分析
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容