从setContentView探讨View,Window与Activity的关系

前言

Activity生命周期的调用时通过ActivityThread管控的,我们在设置应用页面时,都是在onCreate()中调用setContentView()加载布局,这样就产生了三个疑惑:

1:为什么要在onCreate()中设置setContentView()。
2: setContentView是如何起作用的。
3: DecorView和PhoneWindow如何结合。

我么利用android studio 查看布局结构树发现:
从setContentView探讨View,Window与Activity的关系_第1张图片
E3C27C6A-D864-4C1C-922D-9023369ADCFD.png

通过布局结构树我们发现:应用布局的外层有一个根视图DecorView,那么这个DecorView是如何出现在我们的布局中的呢?

我们发现,setContentView调用了PhoneWindow中的setContentView方法。
public void setContentView(int layoutResID) {  
     getWindow().setContentView(layoutResID);                 
     initActionBar();  
} 

Window是一个抽象类,注解中声明Window是一个管理窗口外观和属性策略的抽象类,它的实现类将会以顶层视图的形式添加到窗口管理器中。它提供了标准的UI策略。且有一个唯一的实现类:PhoneWindow。重新回到Activity源码中搜索PhoneWindow,确实找到了这个类,同时也是getWindow()的返回值类型。注解中声明PhoneView所在包为android.view,但实际上通过检索PhoneView已经被移到了android.internal.policy下。


从setContentView探讨View,Window与Activity的关系_第2张图片
640.png

在一个Activity对象被创建的初期,会首先依靠WindowManagerGlobal和WMS建立通信关系,WindowManagerGlobal用来向WindowManagerService注册,主要是获取到 WindowManagerService 代理对象。对外提供与WindowManagerService(WMS)的底层通信。随后ActivityThread通过performLaunchActivity调用Activity生命周期。
Activity.attach()是Activity实例化后最先被调用的,这就保证了Window实例化对象的可用性。而onCreate()和onStart()是初始阶段唯一可以重写的方法,其他的都是final类型,鉴于Activity本质是管理页面交互,布局加载时机越早越有益于页面的展示。所以此时不设,更待何时呢。setConteneView(int layoutID)就在onCreate()中调用了。这样第一个问题就回答完了。

setContentView是如何起作用的?

Activity在attach()中实例化了PhoneWindow对象,并且进行了绑定操作,操作如下:
mWindow = new PhoneWindow(this, window);  
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);  
mWindow.setOnWindowDismissedCallback(this);

根据分析可知:
手机展示的页面实际上是一个层层嵌套的样式,一个Activity启动后,首先实例化PhoneWindow对象,调用setContentView时,首先执行installDecor(),通过generateDecor()实例化一个DecorView对象,将PhoneWindow和DecorView进行了关联绑定,通过generateLayout()加载系统布局到DecorView上,并将ID为content的FrameLayout赋值给mContentParent,最后执行inflate()将我们的布局文件自动添加到mContentParent。

View和Window如何结合?

当setContentView()执行完毕后,此时PhoneWindow和DecorView都已经创建完成,但是DecorView并没有添加到PhoneWindow上,这个操作需要在onResume()才会触发,ActivityThread在执行完performLaunchActivity后,便会执行handlerResumeActivity(),具体流程和源码如下图所示:


从setContentView探讨View,Window与Activity的关系_第3张图片
640.png
 final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {  
//执行到 onResume()  
ActivityClientRecord r = performResumeActivity(token, clearHide);  

if (r != null) {  
    final Activity a = r.activity;  
    boolean willBeVisible = !a.mStartedActivity;  
    ...  
    if (r.window == null && !a.mFinished && willBeVisible) {  
        r.window = r.activity.getWindow();  
        View decor = r.window.getDecorView();  
        decor.setVisibility(View.INVISIBLE);  
        ViewManager wm = a.getWindowManager();  
        WindowManager.LayoutParams l = r.window.getAttributes();  
        a.mDecor = decor;  
        l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;  
        l.softInputMode |= forwardBit;  
        if (a.mVisibleFromClient) {  
            a.mWindowAdded = true;  
            wm.addView(decor, l);  
        }  
    }  
     ...  
    if (!r.activity.mFinished && willBeVisible  
            && r.activity.mDecor != null && !r.hideForNow) {  
        ...  
        mNumVisibleActivities++;  
        if (r.activity.mVisibleFromClient) {  
            r.activity.makeVisible();   
        }  
    }  
    ...  
}   

在这段代码中,内部创建了好多临时变量,其实仔细分析的话,只是两个变量在执行操作,一个就是wm(PhoneWindow自身的WindowManager),一个是decor(setContentView()创建出来的DecorView)。两个变量的最终交互就是wm.addView(decor, l)。同时我们还会发现addView()执行的大前提是等待onResume()执行完毕,如果我们在onResume()中处理耗时操作,那就意味着应用页面的显示时间被延后,为了保障页面尽快进入绘制阶段,onResume中不要处理耗时任务。

理解完这些,我们再来看一下addView()到底做了什么,WindowManager是一个接口类,PhoneWindow的WindowManager对象是WindowManagerImpl,WindowManagerImpl其内部方法始终持有WindowManagerGlobal的引用,我们在ActivityThread的handlerLanuchActivity()中已经知道WindowManagerGlobal是用来和WindowManagerService(WWM)进行通信,在WindowManagerImpl.addView中其实质是把DecorView对象交付给WindowManagerGlobal的视图链中,并通知WWM对当前Window进行管理。引用流程及源码如下:


640-2.png
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {  
...  
ViewRootImpl root = new ViewRootImpl(view.getContext(), display);          
view.setLayoutParams(wparams);      
mViews.add(view);      
mRoots.add(root);      
mParams.add(wparams);          
root.setView(view, wparams, panelParentView);  
...  
} 

WindowManagerGlobal分别存储着View链表和ViewRootImpl的链表,ViewRootImpl就是一个ViewParent视图管理类。每个传入的DecorView都会创建一个对应的ViewRootImpl来管理。它将实际控制着DecorView的绘制周期,同时还可以与WWM进行Binder通信。ViewRootImpl在调用setView后,即向WWM发起了添加请求,WWM便会将当前的PhoneWindow放入自身管理的Window列表中,将DecorView添加到PhoneWindow上,同时通知ViewRootImpl进行绘制操作(绘制操作将涉及到SurfaceFlinger,在这里暂不探讨),代码走到这里时,View和Window之间便真正的结合起来了。其完成流程图如下:


从setContentView探讨View,Window与Activity的关系_第4张图片
640-3.png

需要注意的是,当获得DecorView对象后,先执行了一次setVisibility(View.INVISIBLE)操作,执行完addView()操作后才会重新设置为VISIBLE,我觉得此处的做法类似于SurfaceView绘制过程对Canvas的锁操作,页面的显示需要由过渡动画管理器TranslateManager进行控制,如果直接在可见状态下进行页面绘制,会给用户一种页面加载卡顿的感觉,而等待页面全部加载绘制完毕后再整体展示给用户可以有效的避免这个问题。

通过对以上三个问题的探究,明确的了解了应用布局的加载过程,一个应用展示在手持设备上时,其布局结构实际如下图所示:


从setContentView探讨View,Window与Activity的关系_第5张图片
640-4.png

一个Activity对应一个PhoneWindow,一个PhoneWindow对应一个DecorView。
布局加载的整个过程中系统布局对外提供的都是FrameLayout,所以当你看到有些性能优化书籍提出的合并布局方案,建议用代替FrameLayout作父布局的原因就在这里。同时应用页面视图只会添加在ID为content的FrameLayout中,即系统布局的内容部分。不论开发者配置的样式或者主题有何区别,系统布局中必定会有一个ID为content的控件。

总结

阅读源码时发现,在setContentView()中,频繁用到了inflate()方法,源码中使用的是两参数形式的,而我们在使用inflate()时,更多的是用三参数的,在这列就顺便提一下,inflate(layoutResID, mContentParent)实际上等价于inflate(layoutResID, mContentParent, mContentParent !=null)。mContentParent设置的意义在于协助第一个参数layoutResID所指定布局的根节点生成布局参数,避免宽高设置等属性失效。属性表示一个控件在容器中的大小,就是说这个控件必须在容器中,这个属性才有意义。

你可能感兴趣的:(从setContentView探讨View,Window与Activity的关系)