探讨View、Window和Activity的关系

640?wx_fmt=png&wxfrom=5&wx_lazy=1


今日科技快讯


5月4日,据国外媒体报道,富士康集团计划大力发展半导体业务,最近其调整了公司架构,设立了一个“半导体子集团”,还准备进入半导体的制造环节,已经要求半导体业务集团展开有关建设两座12英寸芯片厂的可行性研究。


作者简介


周一早上好!新的一周继续努力吧!

本篇来自 冷漠的学徒 的投稿,分享了从setContentView()角度分析View、Window、Activity三者关系,一起来看看!希望大家喜欢。

冷漠的学徒 的博客地址:

https://blog.csdn.net/lz8362


前言


我们在Activity的onCreate()方法中设置setContentView(),但是一直不明白其中的原理,正好公司在开展技术交流活动,分到的课题是View、Activity、Window的关系,借这个机会梳理一番。Activity生命周期的调用时通过ActivityThread管控的,我们在设置应用页面时,都是在onCreate()中调用setContentView()加载布局,这样就产生了三个疑惑:

  1. 为什么要在onCreate()中设置。

  2. setContentView是如何起作用的。

  3. DecorView和PhoneWindow如何结合。


正文


下面将通过源码的解读来分析这三个问题。

首先介绍一下如何在AndroidStudio中查看布局结构树,在Eclipse中,提供了工具hierarchyviewer.bat(在tools文件下),AndroidStudio将这个工具合并到自身中,并命名为Layout Inspector

使用步骤如下:

  1. 连接手机,并确保可以adb shell。

  2. 开启AndroidStudio,运行一个本地应用。

  3. 在菜单栏选择Tools->Android->Layout Inspector,弹出检索框,点击确定。

便会分析当前手机页面的布局结构。效果如图所示:

640?wx_fmt=png

640?wx_fmt=png

整个分析页面分为左中右三部分,左边是视图结构树,中间是页面展示,右边是选中视图的信息展示,我们需要关注的是视图结构树,对于视图信息我们关注红圈标记的mID就行了。

为什么要在onCreate()中设置

借助LayoutInspector的分析,会发现应用布局的外层有一个根视图DecorView,DecorView本质是一个系统自定义的FrameLayout。这个DecorView是如何出现在我们的布局中的呢?先从setContentView开始溯源找起。通过溯源会发现,在Activity中调用setContentView(int layoutID)实际上执行的是 Window.setContentView(int layoutID)。代码如下:

public void setContentView(@LayoutRes int layoutResID) {  

     getWindow().setContentView(layoutResID);  

     initWindowDecorActionBar();  

 }  

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

640?wx_fmt=png

在一个Activity对象被创建的初期,会首先依靠WindowManagerGlobal和WWM建立通信关系,WindowManagerGlobal用来向WindowManagerService注册,主要是获取到 WindowManagerService 代理对象。对外提供与WindowManagerService(WWM)的底层通信。随后ActivityThread通过performLaunchActivity调用Activity生命周期,调用顺序如下:

640?wx_fmt=png

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);  

setContentView(int layoutID)最终执行的是PhoneView.setContentView(int layoutID),源码如下:

@Override  
public void setContentView(int layoutResID) {  

  // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window  

   // decor, when theme attributes and the like are crystalized. Do not check the feature  

   // before this happens.  

   if (mContentParent == null) {  

       <strong>installDecor();</strong>  

   } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {  

       mContentParent.removeAllViews();  

   }  

   if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {  

       final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());  
       transitionTo(newScene);  

   } else {  

      <strong> mLayoutInflater.inflate(layoutResID, mContentParent);</strong>  

   }  

   mContentParent.requestApplyInsets();  

   final Callback cb = getCallback();  

   if (cb != null && !isDestroyed()) {  

       cb.onContentChanged();  

   }  

   mContentParentExplicitlySet = true;  

}

mContentParent是一个ViewGroup全局变量,在setContetView初次执行时,势必会先执行installDecor(),顾名思义是加载装饰,由Decor会想到布局结构树中的根布局DecorView,看来DecorView和PhoneWindow是息息相关。installDecor()源码很长,下面展示主要的部分:

private void installDecor() {  

   mForceDecorInstall = false;  

   if (mDecor == null) {  

      <strong> mDecor = generateDecor(-1);</strong>  

       mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);  

       mDecor.setIsRootNamespace(true);  

       if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {  

           mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);  

       }  

   } else {  

       mDecor.setWindow(this);  

   }  

   if (mContentParent == null) {  

       <strong>mContentParent = generateLayout(mDecor);</strong>  

      ...  

       final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(<span style="color:#ccffff;">R.id.decor_content_parent</span>);  

       if (decorContentParent != null) {  

           mDecorContentParent = decorContentParent;  

           mDecorContentParent.setWindowCallback(getCallback());  

           if (mDecorContentParent.getTitle() == null) {  

               mDecorContentParent.setWindowTitle(mTitle);  

           }  



           final int localFeatures = getLocalFeatures();  

           for (int i = 0; i < FEATURE_MAX; i++) {  

               if ((localFeatures & (1 << i)) != 0) {  

                   mDecorContentParent.initFeature(i);  

               }  

           }  

          ...  

       } else {  

           mTitleView = findViewById(R.id.title);  

           if (mTitleView != null) {  
              ...  
           }  
       }  
       ...  
   }  

}  

这里出现了一个mDecor对象,它就是一个DecorView对象,所以installDecor()的实质是实例化当前窗口的DecorView对象和ViewGroup对象。其中generateDecor(-1)实例化mDeocr,generateLayout(mDecor)实例化mContentParent,代码再往下走,出现了多处的findViewById()操作,说明不管是decorContentParent还是mTitleView,它们作为控件都有可能出现在根布局中,而这个根布局的加载一定是在generateLayout(mDecor)或generateDecor(-1)中完成的。先看generateDecor(-1)的源码:

protected DecorView generateDecor(int featureId) {  

// System process doesn't have application context and in that case we need to directly use  

// the context we have. Otherwise we want the application context, so we don't cling to the  

     // activity.  

     Context context;  

     if (mUseDecorContext) {  

         Context applicationContext = getContext().getApplicationContext();  

         if (applicationContext == null) {  

             context = getContext();  

         } else {  

             context = new DecorContext(applicationContext, getContext().getResources());  

             if (mTheme != -1) {  

                 context.setTheme(mTheme);  

             }  

         }  

     } else {  

         context = getContext();  

     }  

     return new DecorView(context, featureId, this, getAttributes());  

 }    

其本质就是返回一个初始化好了的DecorView对象,参看DecorView的构造函数,可以发现DecorView和PhoneWindow之间具有对应关系,即一个PhoneWindow对应一个DeocrView,构造函数如下:

DecorView(Context context, int featureId, PhoneWindow window, WindowManager.LayoutParams params) {     

既然generateDecor(-1)没有,再来看generateLayout(mDecor)中的内容,主要源码如下:

protected ViewGroup generateLayout(DecorView decor) {  

     // Apply data from current theme.  
     TypedArray a = getWindowStyle();  
 ...  

 mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);  

 ...  

     if (a.hasValue(R.styleable.Window_windowFixedWidthMinor)) {  

         if (mFixedWidthMinor == null) mFixedWidthMinor = new TypedValue();  

         a.getValue(R.styleable.Window_windowFixedWidthMinor,  

                 mFixedWidthMinor);  

     }  

 ...  

     if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) {  

         requestFeature(FEATURE_ACTIVITY_TRANSITIONS);  

     }  
 ...  

     // Inflate the window decor.  

     int layoutResource;  

     int features = getLocalFeatures();  

     // System.out.println("Features: 0x" + Integer.toHexString(features));  

     if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {  

         layoutResource = <span style="color:#ff0000;">R.layout.screen_swipe_dismiss</span>;  

         setCloseOnSwipeEnabled(true);  

     } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {  

         if (mIsFloating) {  

             TypedValue res = new TypedValue();  

             getContext().getTheme().resolveAttribute(  

                     R.attr.dialogTitleIconsDecorLayout, res, true);  

             layoutResource = res.resourceId;  

         } else {  

             layoutResource = <span style="color:#ff6666;">R.layout.screen_title_icons</span>;  

         }  

         // XXX Remove this once action bar supports these features.  

         removeFeature(FEATURE_ACTION_BAR);  

         // System.out.println("Title Icons!");  

     } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0  

             && (features & (1 << FEATURE_ACTION_BAR)) == 0) {  

         // Special case for a window with only a progress bar (and title).  

         // XXX Need to have a no-title version of embedded windows.  

         layoutResource = <span style="color:#ff6666;">R.layout.screen_progress</span>;  

         // System.out.println("Progress!");  

     } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {  

         // Special case for a window with a custom title.  

         // If the window is floating, we need a dialog layout  

         if (mIsFloating) {  

             TypedValue res = new TypedValue();  

             getContext().getTheme().resolveAttribute(  

                     R.attr.dialogCustomTitleDecorLayout, res, true);  

             layoutResource = res.resourceId;  

         } else {  

             layoutResource = <span style="color:#ff6666;">R.layout.screen_custom_title</span>;  

         }  

         // XXX Remove this once action bar supports these features.  

         removeFeature(FEATURE_ACTION_BAR);  

     } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {  

         // If no other features and not embedded, only need a title.  

         // If the window is floating, we need a dialog layout  

         if (mIsFloating) {  

             TypedValue res = new TypedValue();  

             getContext().getTheme().resolveAttribute(  

                     R.attr.dialogTitleDecorLayout, res, true);  

             layoutResource = res.resourceId;  

         } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {  

             layoutResource = a.getResourceId(  

                     R.styleable.Window_windowActionBarFullscreenDecorLayout,  

                     <span style="color:#ff6666;">R.layout.screen_action_bar</span>);  

         } else {  

             layoutResource = <span style="color:#ff6666;">R.layout.screen_title</span>;  

         }  

         // System.out.println("Title!");  

     } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {  

         layoutResource = <span style="color:#ff6666;">R.layout.screen_simple_overlay_action_mode</span>;  

     } else {  

         // Embedded, so no decoration is needed.  

         layoutResource = <span style="color:#ff6666;">R.layout.screen_simple</span>;  

         // System.out.println("Simple!");  

     }  

     mDecor.startChanging();  

     mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);  

     <span style="background-color:rgb(51,102,255);">ViewGroup contentParent = (ViewGroup)findViewById(<span style="color:#99ffff;">ID_ANDROID_CONTENT</span>);</span>  

     if (contentParent == null) {  

         throw new RuntimeException("Window couldn't find content container view");  

     }  

     if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {  

         registerSwipeCallbacks(contentParent);  

     }  
     ...  
     mDecor.finishChanging();  
     return contentParent;  

 }    

该方法的返回值是一个ViewGroup,返回的对象contentParent是通过findViewById(ID_ANDROID_CONTENT)操作获得的,在Window中ID_ANDROID_CONTENT是一个常量,声明如下:

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;     

也就是说generateLayout(mDecor)返回的是一个系统布局文件中ID为content的控件,以@+id/content为关键词在framewors/base/core/res/res/layout进行检索,发现了如下文件:

640?wx_fmt=png

这些系统布局文件,正好对应generateLayout(mDecor)里layoutResource的赋值(红色标记处),这些布局文件虽然结构不同但是都会存在一个ID为content的FrameLayout。即mContentParent实质是一个FrameLayout。layoutResource根据前期属性样式的划分,被赋予不同的layoutID,在赋值前执行的一系列样式和属性的判断设置中,有我们经常用到的setFlag()和requestFeature(),这也解释了一个问题为什么要在 setContentView() 之前执行设置Window的Flag或者requestWindowFeature()才会起作用。layoutResource通过DecorView的onResourcesLoaded()方法添加到DecorView自身。源码如下:

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {  

    mStackId = getStackId();  



    if (mBackdropFrameRenderer != null) {  

        loadBackgroundDrawablesIfNeeded();  

        mBackdropFrameRenderer.onResourcesLoaded(  

                this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,  

                mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),  

                getCurrentColor(mNavigationColorViewState));  

    }  

    mDecorCaptionView = createDecorCaptionView(inflater);  

    final View root = inflater.inflate(layoutResource, null);  

    if (mDecorCaptionView != null) {  

        if (mDecorCaptionView.getParent() == null) {  

            addView(mDecorCaptionView,  

                    new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));  

        }  

        mDecorCaptionView.addView(root,  

                new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));  

    } else {  

        // Put it below the color views.  

        addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));  

    }  

    mContentRoot = (ViewGroup) root;  

    initializeElevation();  

}    

这里可以无视mDecorCaptionView这个对象,因为不论if判断怎么走,最终都会调用addView方法。我们会看到传入的layoutResource通过LayoutInflater获得了rootView,

这个rootView没有任何父布局依赖,而是直接以addView的方式最终加载到DecorView本身上。DecorView作为FrameLayout成为了rootView的最终父布局。

现在梳理一下generateLayout(mDecor)中的流程:layoutResource通过属性设置被赋值一个系统布局ID,DecorView将layoutResource添加到自身当中,再将布局中的ID为content的FrameLayout返回给mContentParent。重新回到PhoneWindow.installDecor()中。执行完generateLayout(mDecor)和generateDecor(-1)操作,后续就是针对根布局的标题、图标、菜单等一些列设置。设置完成就回到了setContentView(int layoutID)中,进入下一个if判断,代码如下:

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {  

        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,  

                getContext());  

        transitionTo(newScene);  

    } else {  

       <strong> mLayoutInflater.inflate(layoutResID, mContentParent);</strong>  

    }      

这里的hasFeature()其实是false的,这个判断的目的是是否使用默认的切换管理器TransitionManager(切换管理器控制的就是窗口切换的动画过程),如果你更改了FEATURE_CONTENT_TRANSITIONS,就会创建一个新的TransitionManager,否则就使用默认的。所以代码会直接运行mLayoutInflater.inflate(layoutResID, mContentParent);这句话等价于inflate(layoutResID, mContentParent, mContentParent != null),因为此时mContentParent!= null,无需执行addView操作,inflate会自动将我们传入的Actvity布局添加在了mContentParent上了,这样setContentView的工作就完成了。

回看一下整个过程,一共出现了两次findViewById的场景(蓝色标记场景),其实这两个ID都可以在Layout Inspector中查看到,我使用的是Android7.0的手机运行的程序,所以看到,它实际上用的系统布局是screen_action_bar.xml,如下图:

640?wx_fmt=png

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

640?wx_fmt=png

View和Window如何结合

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

640?wx_fmt=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中不要处理耗时任务。

主要的几个bool变量的状态标记如下:

  • mStartedActivity在Activity中默认是false,顾名思义,只有在创建Activity实例时才会执行向Window添加视图操作。

  • mVisibleFromClient对应的是属性值windowNoDisplay的非,即mVisibleFromClient = !windowNoDisplay,windowNoDisplay默认是false,只有样式设置为Theme.NoDisplay时才会为true,所以mVisibleFromClient 默认是true。

  • mWindowAdded是Window添加开关,默认是false,以保证Window的添加操作在Activity创建过程中只能执行一次。

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

640?wx_fmt=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之间便真正的结合起来了。其完成流程图如下:

640?wx_fmt=png

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

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

640?wx_fmt=png

一个Activity对应一个PhoneWindow,一个PhoneWindow对应一个DecorView。形象的比喻的话,应用就像一个博物馆,Activity对应一个展区,PhoneWindow就是展柜,DecorView是展台,而我们的自定义布局就是展台里的展品。

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


总结


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

640?wx_fmt=png


欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号

640.png?

640?wx_fmt=jpeg

猜你喜欢

转载自blog.csdn.net/c10WTiybQ1Ye3/article/details/80223768