美文网首页Android开发使用技巧Android进阶Android窗口机制
Android窗口机制(四)ViewRootImpl与View和

Android窗口机制(四)ViewRootImpl与View和

作者: Hohohong | 来源:发表于2017-01-15 15:05 被阅读3353次

Android窗口机制系列

Android窗口机制(一)初识Android的窗口结构
Android窗口机制(二)Window,PhoneWindow,DecorView,setContentView源码理解
Android窗口机制(三)Window和WindowManager的创建与Activity
Android窗口机制(四)ViewRootImpl与View和WindowManager
Android窗口机制(五)最终章:WindowManager.LayoutParams和Token以及其他窗口Dialog,Toast

在前篇第(三)文章中,我们讲到了在DecorView在handleResumeActivity方法中被绑定到了WindowManager,也就是调用了windowManager.addView(decorView)。而WindowManager的实现类是WindowManagerImpl,而它则是通过WindowManagerGlobal代理实现addView的,我们看下addView的方法

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...
        ViewRootImpl root;
        View panelParentView = null;
        
        ...
            
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
        //ViewRootImpl开始绘制view
        root.setView(view, wparams, panelParentView);
        ...
    }

可以看到在WindowManagerGlobal的addView中,最后是调用了ViewRootImpl的setView方法,那么这个ViewRootImpl到底是什么。

ViewRootImpl

看到ViewRootImpl想到可能会有ViewRoot类,但是看了源码才知道,ViewRoot类在Android2.2之后就被ViewRootImpl替换了。我们看下说明

/* The top of a view hierarchy, implementing the needed protocol between View
 * and the WindowManager.  This is for the most part an internal implementation
 * detail of {@link WindowManagerGlobal}.
 */

ViewRootImpl是一个视图层次结构的顶部,它实现了View与WindowManager之间所需要的协议,作为WindowManagerGlobal中大部分的内部实现。这个好理解,在WindowManagerGlobal中实现方法中,都可以见到ViewRootImpl,也就说WindowManagerGlobal方法最后还是调用到了ViewRootImpl。addView,removeView,update调用顺序
WindowManagerImpl -> WindowManagerGlobal -> ViewRootImpl

我们看下前面调用到了viewRootImpl的setView方法

  public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                ...
                // Schedule the first layout -before- adding to the window  
                // manager, to make sure we do the relayout before receiving  
                // any other events from the system.
                requestLayout();
                ...
                try {
                ...
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                } 
    }

在setView方法中,
首先会调用到requestLayout(),表示添加Window之前先完成第一次layout布局过程,以确保在收到任何系统事件后面重新布局。requestLayout最终会调用performTraversals方法来完成View的绘制。

接着会通过WindowSession最终来完成Window的添加过程。在下面的代码中mWindowSession类型是IWindowSession,它是一个Binder对象,真正的实现类是Session,也就是说这其实是一次IPC过程,远程调用了Session中的addToDisPlay方法。

 @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            Rect outOutsets, InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }

这里的mService就是WindowManagerService,也就是说Window的添加请求,最终是通过WindowManagerService来添加的。

View通过ViewRootImpl来绘制

前面说到,ViewRootImpl调用到requestLayout()来完成View的绘制操作,我们看下源码

 @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

View绘制,先判断当前线程

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

如果不是当前线程则抛出异常,这个异常是不是感觉很熟悉啊。没错,当你在子线程更新UI没使用handler的话就会抛出这个异常

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

抛出地方就是这里,一般在子线程操作UI都会调用到view.invalidate,而View的重绘会触发ViewRootImpl的requestLayout,就会去判断当前线程。

接着看,判断完线程后,接着调用scheduleTraversals()

  void scheduleTraversals() {
        if (!mTraversalScheduled) {
            ...
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
          ...
        }
    }

scheduleTraversals中会通过handler去异步调用mTraversalRunnable接口

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

接着

  void doTraversal() {
            ...
            performTraversals();
            ...
    }

可以看到,最后真正调用绘制的是performTraversals()方法,这个方法很长核心便是

private void performTraversals() {  
        ......  
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ......  
        performDraw();
        }
        ......  
    }  

而这个方法各自最终调用到的便是

        ......  
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);  
        ....
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
        ......  
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());  
        ......  
mView.draw(canvas);  

会开始触发测量绘制。
performTraversals方法会经过measure、layout和draw三个过程才能将一个View绘制出来,所以View的绘制是ViewRootImpl完成的,另外当手动调用invalidate,postInvalidate,requestInvalidate也会最终调用performTraversals,来重新绘制View。

View与WindowManager联系

那么View和WindowManager之间是怎么通过ViewRootImpl联系的呢。

从第三篇文章中我们知道,WindowManager是继承于ViewManager接口的,而ViewManager提供了添加View,删除View,更新View的方法。就拿setContentView来说,当Activity的onCreate调用到了setContentView后,view就会被绘制了吗?肯定不是,setContentView只是把需要添加的View的结构添加保存在DecorView中。此时的DecorView还并没有被绘制(没有触发view.measure,layout,draw)。

DecorView真正的绘制显示是在activity.handleResumeActivity方法中DecorView被添加到WindowManager时候,也就是调用到windowManager.addView(decorView)。而在windowManager.addView方法中调用到windowManagerGlobal.addView,开始创建初始化ViewRootImpl,再调用到viewRootImpl.setView,最后是调用到viewRootImpl的performTraversals来进行view的绘制(measure,layout,draw),这个时候View才真正被绘制出来。

这也就是为什么我们在onCreate方法中调用view.getMeasureHeight() = 0的原因,我们知道activity.handleResumeActivity最后调用到的是activity的onResume方法,但是按上面所说在onResume方法中调用就可以得到了吗,答案肯定是否定的,因为ViewRootImpl绘制View并非是同步的,而是异步(Handler)。

难道就没有得监听了吗?相信大家以前获取使用的大多是

view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
    // TODO Auto-generated method stub
             
    }
});

没错,的确是这个,为什么呢,因为在viewRootImpl的performTraversals的绘制最后,调用了

 {
        if (triggerGlobalLayoutListener) {
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
        }
        ...
        performDraw();
}

dispatchOnGlobalLayout会触发OnGlobalLayoutListener的onGlobalLayout()函数回调
但此时View并还没有绘制显示出来,只是先调用了measure和layout,但也可以得到它的宽高了。

Paste_Image.png

另外,前面说到,ViewRootImpl在调用requestLayout准备绘制View的时候会先判断线程,这里我们前面分析了,但也只是分析了一点。

 @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

为什么这么说呢?
先看Activity下这段代码

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = (TextView) findViewById(R.id.tv);
        new Thread(new Runnable() {
            @Override
            public void run() {
                tv.setText("Hohohong Test");
            }
        }).start();
    }

我是在onCreate里面的子线程去更新UI的,那么会报错吗?测试后你就会知道不会报错,如果你放置个Button点击再去调用的话则会弹出报错。为什么会这样?
答案就是跟ViewRootImpl的初始化有关,因为在onCreate的时候此时View还没被绘制出来,ViewRootImpl还未创建出来,它的创建是在activity.handleResumeActivity的调用到windowManager.addView(decorView)时候,如前面说的ViewRootImpl才被创建起来

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...
        ViewRootImpl root;
        ...
           
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        //ViewRootImpl保存在一个集合List中
        mRoots.add(root);
        mParams.add(wparams);
        //ViewRootImpl开始绘制view
        root.setView(view, wparams, panelParentView);
        ...
    }

此时创建完才会去判断线程。是不是有种让你豁然开朗的感觉!

View与ViewRootImpl的绑定

另外View和ViewRootImpl是怎么绑定在一起的呢?通过view.getViewRootImpl可以获取到ViewRootImpl。

    public ViewRootImpl getViewRootImpl() {
        if (mAttachInfo != null) {
            return mAttachInfo.mViewRootImpl;
        }
        return null;
    }

而这个AttachInfo则是View里面一个静态内部类,它的构造方法

   AttachInfo(IWindowSession session, IWindow window, Display display,
                ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer) {
            mSession = session;
            mWindow = window;
            mWindowToken = window.asBinder();
            mDisplay = display;
            mViewRootImpl = viewRootImpl;
            mHandler = handler;
            mRootCallbacks = effectPlayer;
        }

可以看到viewRootImpl在它的构造方法里赋值了,那么这个方法肯定是在ViewRootImpl创建时创建的,而ViewRootImpl的创建是在调用WindowManagerGlobal.addView的时候

     root = new ViewRootImpl(view.getContext(), display);

而构造方法中

 public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        ...
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
        ...
    }

可以看到View与ViewRootImpl绑定一起了。
之后就可以通过view.getViewRootImpl获取到,而在Window里面也可以获取到ViewRootImpl,因为Window里面有DecorView(这里说的Window都是讲它的实现类PhoneWindo),前三篇已经介绍过了,通过DecorView来获取到ViewRootImpl

 private ViewRootImpl getViewRootImpl() {
        if (mDecor != null) {
            ViewRootImpl viewRootImpl = mDecor.getViewRootImpl();
            if (viewRootImpl != null) {
                return viewRootImpl;
            }
        }
        throw new IllegalStateException("view not added");
    }

另外,一个View会对应一个ViewRootImpl吗?我们做个测试,在一个布局中打印两个不同控件的ViewRootImpl的内存地址

  Log.e(TAG, "getViewRootImpl: textView: " + tv.getViewRootImpl() );
  Log.e(TAG, "getViewRootImpl: button: " + btn.getViewRootImpl() );

结果

Paste_Image.png

可以看到,都是同一个对象,共用一个ViewRootImpl。

小结

  • 之所以说ViewRoot是View和WindowManager的桥梁,是因为在真正操控绘制View的是ViewRootImpl,View通过WindowManager来转接调用ViewRootImpl
  • 在ViewRootImpl未初始化创建的时候是可以进行子线程更新UI的,而它创建是在activity.handleResumeActivity方法调用,即DecorView被添加到WindowManager的时候
  • ViewRootImpl绘制View的时候会先检查当前线程是否是主线程,是才能继续绘制下去

ViewRootImpl的功能可不只是绘制,它还有事件分发的功能,想要了解的深入的话可以看下
ViewRootImpl源码分析事件分发

下篇文章将介绍Dialog,PopWindow,Toast这些窗口机制

相关文章

网友评论

  • andyhaha007:怒赞楼主,这是我看过的最好的一篇能够揭示setContentView所发生的一切,而且能够在更高的高度来解释了view的整个绘制流程。读后有种豁然开朗的感觉,万分感谢!
  • ab72c065186a:老哥,很6,期待你后续的文章。
  • shixinBook:博主,第一段下面应该是调用ViewRootImpl的setView方法,而不是add方法,可能您笔误了。
    Hohohong:已更改。
  • 0f100e7189b2:Log.e(TAG, "getViewRootImpl: textView: " + tv.getViewRootImpl() );
    Log.e(TAG, "getViewRootImpl: button: " + btn.getViewRootImpl() );


    这个btn.getViewRootImpl API 是hide的,你是用framework.jar (包含隐藏还有内在APIs) 作为调试的代码的?
    Hohohong:当时好像是下载过Android源码的,忘了0 0。也可以简单反射调用View的这个方法
  • 橘子香蕉苹果:请教一下,绘制和窗口什么关系,绘制完成窗口应该还没有显示吧?我看了requestLayout()在 res = mWindowSession.addToDisplay()之前执行的,意思是先执行了绘制,然后再调用WMS addwindow。另外再问一下,WMS是服务端,管理多个窗口,windowManager是客户端,是不是只管理一个窗口(对应Activity)?
    橘子香蕉苹果:@Hohohong 我应该是理解错了,刚刚看了windowmanagerimpl code,发现windowmanager不是管理窗口,就像你说的,只是负责添加窗口而已,可以把windowmanager理解成一个传输带吗?向wms输送窗口
    橘子香蕉苹果:@Hohohong windowmanager是客户端,管理窗口是可以管理多个窗口还是只能对应一个窗口呢?
    Hohohong:绘制完窗口还没有显示,通过addToDisplay通知需要添加窗口,底层会通过SurfaceFlinger来触发硬件绘制显示。
    WMS可以理解为是用来控制窗口显示的,而WindowManager则是用来添加管理窗口,窗口不只是Activity,还有可能是系统的,比如Toast,添加完后最终的显示都是交给WMS来用在SurfaceFlinger绘制。
  • 安浪创想:老项目到安卓6报错,权限是怎么弄?申明和申请那个权限才可以用WindowManager
  • WALLEG:mAttachInfo.mTreeObserver.dispatchOnGlobalLayout(); 这个什么时候才会执行? onresume之后?
    WALLEG:@Hohohong 恩 3q~ 我发现如果一个fragment没有走onresume的时候 etViewTreeObserver().addOnGlobalLayoutListener()这个的回调不起作用,我就在onresume里调用了一次就可以了~
    Hohohong:在onResume之后,因为绘制测量View的过程与onResume并不是同步的,而是通过Handler分发到主线程中,所以在时间上onResume会被先回调,之后到了handler处理的时候才调用到dispatchOnGlobalLayout接口回调。文章中中间scheduleTraversals的时候有提及到
  • 嘿大魔王:想问一下“ 远程调用了Session中的addToDisPlay方法。”中addToDisPlay的作用是什么??为什么要调用?谢谢
    嘿大魔王:@Hohohong 这样 那WindowManagerService的addWindow只是传递条件过去,具体的创建还是在WMS中吧
    嘿大魔王:@Hohohong 所以它的意思就是在WMS创建一个符合要求的窗口,是吗??
    Hohohong:好的,调用session.addToDisPlay方法中,如果你有查看Session的源码的话会发现里面调用到了WindowManagerService的addWindow方法,而在WMS这个方法里面,会根据当前窗口的token值请判断是否符合条件创建窗口。详细的话可以看下第五篇,有讲到token这些
    可以说addToDisPlay是像WMS申请添加窗口

本文标题:Android窗口机制(四)ViewRootImpl与View和

本文链接:https://www.haomeiwen.com/subject/ornybttx.html