您当前的位置: 首页 >  面试
  • 0浏览

    0关注

    674博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Android面试:Invalidate、RequestLayout

沙漠一只雕得儿得儿 发布时间:2021-03-13 20:32:49 ,浏览量:0

Invalidate/RequestLayout区别

先放上结论

  1. requestLayout会直接递归调用父窗口的requestLayout,直到ViewRootImpl,然后触发peformTraversals,由于mLayoutRequested为true,会导致onMeasure和onLayout被调用。不一定会触发OnDraw
  2. requestLayout触发onDraw可能是因为在在layout过程中发现l,t,r,b和以前不一样,那就会触发一次invalidate,所以触发了onDraw,也可能是因为别的原因导致mDirty非空(比如在跑动画)
  3. view的invalidate不会导致ViewRootImpl的invalidate被调用,而是递归调用父view的invalidateChildInParent,直到ViewRootImpl的invalidateChildInParent,然后触发peformTraversals,会导致当前view被重绘,由于mLayoutRequested为false,不会导致onMeasure和onLayout被调用,而OnDraw会被调用

结合requestLayout和invalidate与View三大流程关系,有如下图:

1、当视图的内容,可见性发生变化,会调用invalidate,调用后只会触发Draw 过程。 2、requestLayout 会触发Measure、Layout过程,如果尺寸发生改变,则会调用invalidate。 3、当涉及View的尺寸、位置变化时使用requestLayout。 4、当仅仅需要重绘时调用invalidate。 5、如果不确定requestLayout 是否触发invalidate,可在requestLayout后继续调用invalidate。

源码解析:

Android requestLayout与invalidate的区别 - 简书

1. invalidate不调用performMeasure和performLayout

先看下 requestLayout 方法

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
          //这里设置为true
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

再来看 performTraversal方法的大致逻辑,对应的逻辑我都写到注释中了,挺清晰的。

    private void performTraversals() {
        ......
                // 调用invalidate的话,mLayoutRequested为false,所以layoutRequested为false
                // 调用requestLayout,会把mLayoutRequested设置为true
            boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
            ......
            if (layoutRequested) {
                // Clear this now, so that if anything requests a layout in the
                // rest of this function we will catch it and re-run a full
                // layout pass.
                  //每次使用performTraversals都会把mLayoutRequested重置为false
                mLayoutRequested = false;
            }
                    // 如果layoutRequested为false,那windowShouldResize一定是false了,不可能可以调用到performMeasure了
            boolean windowShouldResize = layoutRequested && windowSizeMayChange && ......;
            
            if (mFirst || windowShouldResize || ......) {
                    ......
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                ......
            } 
            
            .......
      // invalidate没有把mLayoutRequested设置为true,因此didLayout将为false,因此也无法调用performLayout
            final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
            if (didLayout) {
                performLayout(lp, mWidth, mHeight);
            ......
            }   
            ......
            performDraw()
            }
        
2. performDraw方法与requestLayout,view.invalidate的关系
private boolean draw(boolean fullRedrawNeeded) {
    ······
    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
            ······
            // 执行真正的绘制流程
          mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback);
          ······
    }
    ······
}

可以看到,如果!dirty.isEmpty()为true,才会去绘制。

而view#invalidate时,会直接把自身的 left,right,top,bottom传递过去,并一路调用到 ViewRootImpl#invalidateRectOnScreen(Rect dirty),该方法中会调用scheduleTraversals()。从而保证可以调用performDraw()。而requestLayout是没有这一步的,因此往往不会执行绘制方法。

上面仅仅说明了单个布局Invalidate/RequestLayout联系,那么如果父布局调用了invalidate,那么子布局会走重绘过程吗?接下来列举这些关系。

这两个方法的使用场景:

ImageView的使用:

可以非常明显看到在ImageView中刷新图片时,如果宽高变了,则需要调用requestLayout。否则直接使用invalidate。

TextView的使用:

textView中大多数的都是两个方法需要同时调用:

子布局/父布局 Invalidate/RequestLayout 关系

子布局Invalidate 如果是软件绘制或者父布局开启了软件缓存绘制,父布局会走重绘过程(前提是WILL_NOT_DRAW标记没设置)。

子布局RequestLayout 父布局会重走Measure、Layout过程。

父布局Invalidate 如果是软件绘制,则子布局会走重绘过程。

父布局RequestLayout 如果父布局尺寸发生了改变,则会触发子布局Measure过程、Layout过程。

比较一下requestLayout和invalidate方法

比较一下requestLayout和invalidate方法 - 掘金

调用 View.requestLayout 方法后会依次调用 performMeasure, performLayout 和 performDraw 方法,调用者 View 及其父 View 会从上往下重新进行 measure, layout 流程,一般情况下不会执行 draw 流程(子 View 会通过判断其尺寸/顶点是否发生改变而决定是否重新 measure/layout/draw 流程)。

子线程真不能绘制UI吗

在Activity onCreate里创建子线程并展示对话框:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_group);

        new Thread(new Runnable() {
            @Override
            public void run() {
                TextView textView = new TextView(MainActivity.this);
                textView.setText("hello thread");
                Looper.prepare();
                Dialog dialog = new Dialog(MainActivity.this);
                dialog.setContentView(textView);
                dialog.show();
                Looper.loop();
            }
        }).start();
    }

答案是可以的,接下来分析为什么可以。

在分析ViewRootImpl里requestLayout/invalidate过程中,发现其内部调用了checkThread()方法:

#ViewRootImpl.java
    void checkThread() {
        //当前调用线程与mThread不是同一线程则会抛出异常
        if (mThread != Thread.currentThread()) {
            //简单翻译来说:只有创建了ViewTree的线程才能操作里边的View
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

问题的关键是mThread是什么?从哪里来?

#ViewRootImpl.java
    public ViewRootImpl(Context context, Display display) {
        ...
        //mThread 为Thread类型
        //也就是说哪个线程执行了构造ViewRootImpl对象,那么mThread就是指向那个线程
        mThread = Thread.currentThread();
        ...
    }

而创建ViewRootImpl对象是在调用WindowManager.addView(xx)过程中创建的。 关于WindowManager/Window 请移步:Window/WindowManager 不可不知之事

现在回过头来看Dialog创建就比较明朗了:

1、dialog.show() 调用WindowManager.addView(xx),此时是子线程调用,因此ViewRootImpl对象是在子线程调用的,进而mThread指向子线程。 2、当ViewRootImpl对象构建成功后,调用其setView(xx)方法,里面调用了requestLayout,此时还是子线程。 3、checkThread()判断是同一线程,因此不会抛出异常。

实际上,"子线程不能更新ui" 更合理的表述应为:View只能被构建了ViewTree的线程操作。只是通常来说,Activity 构建ViewTree的线程被称作UI(主)线程,因此才会有上述说法。

postInvalidate 流程

postInvalidate 通过ViewRootImpl 里的handler切换到UI线程,最终执行 invalidate()。 ViewRootImpl 里的hanlder绑定的线程即是UI线程。

源码分析_Android UI何时刷新_Choreographer

我们调用View的requestLayout或者invalidate时,最终都会触发ViewRootImp执行scheduleTraversals()方法。这个方法中ViewRootImp会通过Choreographer来注册个接收Vsync的监听,当接收到系统体层发送来的Vsync后我们就执行doTraversal()来重新绘制界面。通过上面的分析我们调用invalidate等刷新操作时,系统并不会立即刷新界面,而是等到Vsync消息后才会刷新页面。

源码分析_Android UI何时刷新_Choreographer - 简书

关注
打赏
1657159701
查看更多评论
立即登录/注册

微信扫码登录

0.1747s