Android DecorView 一窥全貌(上)

5,814 阅读7分钟

前言

系列文章:

Android DecorView 一窥全貌(上)
Android DecorView 一窥全貌(下)

我们都知道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来说并没有尺寸的说法。

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