Android DecorView 一窥全貌(上)

作者: 小鱼人爱编程 | 来源:发表于2021-11-04 22:56 被阅读0次

    前言

    我们都知道DecorView是最顶层View(根View),它是怎么创建和使用的呢?
    通过本篇文章,你将了解到:

    1、DecorView创建过程。
    2、DecorView与Window/Activity关系
    3、DecorView各个子View创建

    DecorView创建过程

    来回顾一下Activity创建过程:


    image.png

    AMS管理着Activity生命周期,每当切换Activity状态时通过Binder告诉ActivityThread,ActivityThread通过Handler切换到主线程(UI线程)执行,最终分别调用到我们熟知的onCreate(xx)/onResume()方法。
    更多细节请移步:Android Activity创建到View的显示过程
    本次关注的重点是setContentView(xx)方法。

    简单布局分析

    相信大家都知道,该方法是将我们布局(layout)文件添加到一个id为“android.R.id.content”的ViewGroup里,我们只需要关心layout的内容。那么R.id.content是整个View树的根布局吗?来看看一个最简单的Activity的布局:

    my_layout.xml
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:background="@color/colorGreen"
        android:id="@+id/my_root"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
        <Button
            android:id="@+id/btn"
            android:text="hello"
            android:onClick="onClick"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
        </Button>
    </FrameLayout>
    

    效果图:


    image.png

    把布局观察器打开:


    image.png
    可以看出,"content"布局对应的是ContentFrameLayout,它并不是View树的根布局,根布局是DecorView,DecorView和ContentFrameLayout之间还有几个View,接下来我们就来了解上图的这些View是怎么确定的。

    setContentView(xx)源码解析

    从Activity onCreate(xx)看起

    AppCompatDelegateImpl

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            //先调用父类构造函数
            //初始化一些必要变量
            super.onCreate(savedInstanceState);
            setContentView(R.layout.layout_path);
        }
    

    创建及初始化AppCompatDelegateImpl

    AppCompatActivity.java
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            final AppCompatDelegate delegate = getDelegate();
            delegate.installViewFactory();
            //交给AppCompatDelegate代理
            delegate.onCreate(savedInstanceState);
            super.onCreate(savedInstanceState);
        }
    

    AppCompatActivity将工作交给了AppCompatDelegate处理,而AppCompatDelegate是抽象类,真正工作的是其子类:AppCompatDelegateImpl。

        @Override
        public void onCreate(Bundle savedInstanceState) {
            ensureWindow();
        }
        private void ensureWindow() {
            //省略
            //mHost为创建此代理的Activity
            if (mWindow == null && mHost instanceof Activity) {
                attachToWindow(((Activity) mHost).getWindow());
            }
        }
        private void attachToWindow(@NonNull Window window) {
            if (mWindow != null) {
                throw new IllegalStateException(
                        "AppCompat has already installed itself into the Window");
            }
            //接收各种事件的window callback,实际就是将callback再包了一层
            mAppCompatWindowCallback = new AppCompatWindowCallback(callback);
            window.setCallback(mAppCompatWindowCallback);
            //省略
            //使用activity的window
            mWindow = window;
        }
    

    上面代码的主要工作是关联AppCompatDelegateImpl和Activity的window变量。

    接着来看看setContentView(R.layout.layout_path)本尊
    Activity的setContentView(xx)最终调用了AppCompatDelegateImpl里的setContentView(xx):

    AppCompatDelegateImpl.java
        @Override
        public void setContentView(int resId) {
            //创建SubDecor,从名字可以猜测一下是DecorView的子View
            ensureSubDecor();
            //找到R.id.content布局
            ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
            //清空content里的子View
            contentParent.removeAllViews();
            //加载在activity里设置的layout,并添加到content里
            LayoutInflater.from(mContext).inflate(resId, contentParent);
        }
    

    可以看出,我们自定义的layout最后被添加到contentParent里,而contentParent是从mSubDecor里找寻的,因此我们重点来看看ensureSubDecor()方法。
    ensureSubDecor里调用的是createSubDecor()方法来创建subDecor。

    subDecor创建

    AppCompatDelegateImpl.java
        private ViewGroup createSubDecor() {
            //根据Activity的主题设置不同设置window的feature
            TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
            //确保window已经关联
            ensureWindow();
            //获取DecorView,没有则创建
            mWindow.getDecorView();
            ViewGroup subDecor = null;
            //有标题
            if (!mWindowNoTitle) {
                //根据条件给subDecor加载不同的布局文件
                if (mIsFloating) {
                    subDecor = (ViewGroup) inflater.inflate(
                            R.layout.abc_dialog_title_material, null);
                } else if (mHasActionBar) {
                    //加载subDecor布局
                    subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                            .inflate(R.layout.abc_screen_toolbar, null);
    
                    //寻找subDecor子布局
                    mDecorContentParent = (DecorContentParent) subDecor
                            .findViewById(R.id.decor_content_parent);
                    mDecorContentParent.setWindowCallback(getWindowCallback());
                }
            } else {
                //省略
            }
            //标题栏标题
            if (mDecorContentParent == null) {
                mTitleView = (TextView) subDecor.findViewById(R.id.title);
            }
            //寻找subDecor子布局,命名为contentView
            final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                    R.id.action_bar_activity_content);
            //找到window里content布局,实际上找的是DecorView里名为content的布局
            final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
            if (windowContentView != null) {
                //挨个移除windowContentView的子View,并将之添加到contentView里
                while (windowContentView.getChildCount() > 0) {
                    final View child = windowContentView.getChildAt(0);
                    windowContentView.removeViewAt(0);
                    contentView.addView(child);
                }
                //把windowContentView id去掉,之前名为content
                windowContentView.setId(View.NO_ID);
                //将"content"名赋予contentView
                contentView.setId(android.R.id.content);
            }
            //把subDecor添加为Window的contentView,实际上添加为DecorView的子View。该方法后面再具体分析
            mWindow.setContentView(subDecor);
            return subDecor;
        }
    

    该方法较长,省略了一些细枝末节,主体功能是:

    • 根据不同的前置条件,加载不同的布局文件作为subDecor
    • 将subDecor加入到DecorView里,subDecor本身是ViewGroup

    subDecor创建了,那么DecorView啥时候创建呢?createSubDecor()有段代码:

    mWindow.getDecorView()
    

    DecorView创建

    mWindow之前分析过是Activity的window变量,它的实现类是PhoneWindow。

    PhoneWindow.java
        View getDecorView() {
            if (mDecor == null || mForceDecorInstall) {
                installDecor();
            }
            return mDecor;
        }
    
        private void installDecor() {
            //mDecor作为PhoneWindow变量
            if (mDecor == null) {
                //新建DecorView,并关联window
                mDecor = generateDecor(-1);
            }
            if (mContentParent == null) {
                //创建DecorView子布局
                mContentParent = generateLayout(mDecor);
            }
        }
    
        protected DecorView generateDecor(int featureId) {
            Context context;
            if (mUseDecorContext) {
                Context applicationContext = getContext().getApplicationContext();
                if (applicationContext == null) {
                    context = getContext();
                } else {
                    //applicationContext是Application
                    //getContext 是Activity
                    //DecorContext 继承自ContextThemeWrapper
                    context = new DecorContext(applicationContext, getContext());
                    if (mTheme != -1) {
                        context.setTheme(mTheme);
                    }
                }
            } else {
                context = getContext();
            }
            //this 指的是phoneWindow,赋值给mWindow变量
            return new DecorView(context, featureId, this, getAttributes());
        }
    

    从上可知:

    DecorView继承自FrameLayout

    注:DecorContext 有关请移步:Android各种Context的前世今生

    重点来看看

    mContentParent = generateLayout(mDecor);

    PhoneWindow.java
        protected ViewGroup generateLayout(DecorView decor) {
            //获取WindowStyle
            TypedArray a = getWindowStyle();
            //根据style设置各种flags和feature
            //省略...
    
            WindowManager.LayoutParams params = getAttributes();
            //待加载的布局资源id
            int layoutResource;
            int features = getLocalFeatures();
            //根据不同的feature确定不同的布局
            if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
                layoutResource = R.layout.screen_swipe_dismiss;
                setCloseOnSwipeEnabled(true);
            } else if (features) {
                //省略
            } else {
                //默认加载该布局
                layoutResource = R.layout.screen_simple; 
            }
            //实例化上面确定的布局,并将该布局添加到DecorView里
            mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
            //获取名为ID_ANDROID_CONTENT的子View
            //public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
            //实际上就是content
            ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
            return contentParent;
        }
    

    将确定的布局加载,并添加到DecorView里。

        void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
            //实例化布局
            final View root = inflater.inflate(layoutResource, null);
            if (mDecorCaptionView != null) {
                //省略
            } else {
                //添加到DecorView里
                addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            //记录root
            mContentRoot = (ViewGroup) root;
        }
    

    总结来说,mWindow.getDecorView()做了哪些事呢?

    • 创建DecorView,并关联PhoneWindow与DecorView(互相持有)
    • 根据feature确定DecorView子布局,并添加填充满DecorView

    可能看到上面的分析还是一头雾水,没关系先来理一下DecorView和SubDecor关系。

    1、首先创建DecorView,并添加其子View,该子View在DecorView里称为:mContentRoot。当前场景下使用的子View资源id:R.layout.screen_simple;
    2、mContentRoot里有名为"R.id.content"的子View,其在PhoneWindow里称作为:mContentParent
    3、当前场景下使用的subDecor资源id:R.layout.abc_screen_toolbar,实例化该资源文件获得subDecor实例
    4、该subDecor里有子View资源id:R.id.action_bar_activity_content,在AppCompatDelegateImpl里命名为:contentView
    5、DecorView添加subDecor过程:取出DecorView里的mContentParent,并移除里面的子View,并将移除的子View添加到subDecor里的contentView里。
    6、将contentView更名为“R.id.content”
    7、最后通过mWindow.setContentView(subDecor),将subDecor添加到DecorView里

    继续分析mWindow.setContentView(subDecor)

    PhoneWindow.java
        public void setContentView(View view, ViewGroup.LayoutParams params) {
            if (mContentParent == null) {
                //为空,说明DecorView没构造
                installDecor();
            } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                //清空子View
                mContentParent.removeAllViews();
            }
            if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                //省略
            } else {
                //View添加到mContentParent
                mContentParent.addView(view, params);
            }
            //省略
        }
    

    以上就是DecorView的创建过程。
    DecorView创建完成后,再将自定义布局添加到DecorView里名为R.id.content布局里。至此setContentView分析完毕,老规矩用图表示整个流程。

    setContentView图示

    image.png

    DecorView与Window/Activity关系

    Activity:
    我们平时把Activity当做一个页面,直观上看这个页面承载着我们的布局显示以及相应的逻辑处理。Activity有生命周期,在不同的状态下做不同的处理。
    Window
    顾名思义,就是我们所见的窗口,Activity显示的内容实际上是展示在Window上的,对应的是PhoneWindow。
    DecorView
    Window本身是比较抽象的,可以想象成为一个管理的东西,它管理的就是ViewTree,而DecorView是ViewTree的根。我们具体的界面展示是通过View完成的,把设计好的View添加到DecorView里,整个ViewTree就构建起来了,最终添加到Window里。
    因此:

    • Activity管理着Window,Window管理着DecorView,Activity间接管理着DecorView
    • Activity处在“create"状态的时候创建对应的Window,并在setContentView里创建DecorView,最终添加自定义布局到DecorView里。此时,Activity创建完毕,Window创建完毕,DecorView创建完毕,ViewTree构建成功,三者关系已建立。
    • Acitivity处在“resume"状态的时候,通过Window的管理类将DecorView添加到Window里,并提交更新DecorView的请求。当下次屏幕刷新信号到来之时,执行ViewTree三大流程(测量,布局,绘制),最终的效果就是我们的自定义布局显示在屏幕上。
      “注”:指的状态执行时机是处在ActivityThread里的。

    这么看起来还是不太顺,我们所说的三者的联系实际上在代码里来看就是:“不是你持有我就是我引用你”,通过类图来看看三者之间的引用情况:


    image.png

    可以看出,Window作为Activity和DecorView的联结者。
    注:Activity也持有DecorView的引用,只是不对外提供获取的接口。因此一般通过Activity获取Window,再获取DecorView。
    在UI上看来,可以这么理解:


    image.png

    注:这图只是便于理解抽象关系,实际上对于Activity来说并没有尺寸的说法。

    后续

    限于当前篇幅,后续内容重开一篇:
    Android DecorView 一窥全貌(下)

    注:以上关于DecorView、subDecor、标题栏、布局文件和区块尺寸的选择是基于当前的demo的。可能你所使用的主题、设置的属性和本文不同导致布局效果差异,请注意甄别。
    源码基于:Android 10.0

    相关文章

      网友评论

        本文标题:Android DecorView 一窥全貌(上)

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