Android 11源码分析:从Activity的setContent方法看渲染流程(1)Activity,Window,View的关系

1,301 阅读11分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

之前的学习记录都是在自己的本地,借着掘金的这次8月活动的机会,也挑战一下自己。各位看官点点赞,小弟先谢过。

从Activity的setContent方法看渲染流程(1)Activity,Window,View的关系

从Activity的setContent方法看渲染流程(2)初识Window

从Activity的setContent方法看渲染流程(3)再看Window

从Activity的setContent方法看渲染流程(番外篇)AppCompatActivity的setContent

前言

在不熟悉Fragment之前,可以说我是看到一个页面就写一个Activity,不会MVVP之前,什么代码都丢在Activity里,所以Activity我用的熟哇。直到有一天,一个刚学安卓的学妹问我

学妹:我写的XML是怎么显示到Activity的?

我:我们setContentView传给Activity就好了。 学妹:然后呢?

我:后面就比较复杂了,你刚学安卓先不要深究细节,以后慢慢再告诉你。

学妹就乖乖的不问了。

我这么做有问题么?没有问题。对于初学者确实不建议深究细节,而是应该把精力放着怎么掌握更多轮子的用法。

那我会知道答案吗?我不知道,这有问题么?有问题!!

好歹也工作几年了,平时自己也多多少少了解过Activity,Window,View的关系,面试的时候也问到过,只不过把百度到的几句结论说一下也就糊弄过去了,但是忽悠别人容易,想骗自己就很难。于是我决定从头至尾梳理一边setContentView到底做了什么。

在以前看别人的文章设计到一些流程都会以不要深入代码内部,免得不能自拔一笔带过导致很多流程看的迷迷糊糊。所以这一次,我决定尽我所能,弄懂每一块代码,直到Native层。

我们给Activity写的XML到底去哪了

友情提示:本文图片较多。

前置环境

AS创建一个项目,在他默认给的MainActivity的XML里,我们简单的加一个TextView,代码如下图

1629264269(1).png 最外层是默认的约束布局,加上idcl_my_xml,然后自己的TextView加上idtv_hello。 我用Theme主题是Theme.AppCompat.Light.DarkActionBar,MainActivity继承的是Activity

OK运行项目,开始旅程

可视化观察层级

运行后效果如下图

6cab90e261c2ae3071a211bdadf32ac.jpg

我们使用Android Studio的视图检测工具,进行查看。

1018ecbaf104e4602d1e958adac89cb.png 在右边可以看到每个层级的View,中间是可视化的视图层级,右边是每个View的具体信息,只需要看id就好,id对应的就是这个View的id。

706c8306367c484cfdd72bac8f5752b.png

以我们设置给Activity进去XML为对底层计算,一共有4个层级。从外至内分别是DecorView->LinearLayout->一个FrameLayouyt->我们的约束布局。接下来一层层单独看看,能不能找到相关线索:

第一层(顶层):DecorView

1629281216(1).png

顶层是一个叫DecorView的视图。没有id信息

第二层:LinearLayout

1629281401(1).png

DecorView下只有一个视图。也就是一个LinearLayout同样没有id信息。

第三层:FrameLayout+View

d064a71618024f49a11f8bf09628a16.png

70451f0ab295e4e364a65534624d659.png

第三层有2块布局,上面是一个id为statusBarBackground的View,看id和实际样式是状态栏了没跑了。 状态栏下面是一个id叫content的FrameLayout。

第四层:setContent的XML

1629282052(1).png

第四层就是我们自己自己写的id叫my_xml的布局了,他被添加到了上层的一个id为content的FrameLayout中。

下面还有一层我们自己写TextView布局,就没有继续分析的必要了,因为实际开发过程中还可以有无数层再嵌套。

层级结论

通过AS的可视化分析后,我们有一个很直观的结论:

我们设置给Activity的xml布局,最后是添加到了一个id为content的FrameLayout中。而这个FrameLayout同级的布局是一个id为statusBarBackground的View,也就是我们熟悉的状态栏。他们共同在一个垂直布局的LinearLayout中,这个LinearLayout又在顶层视图DecorView中。

疑问

DecorView是我们能看到的最顶层的视图,但是我们是给Activity设置内容,这个DecorViewActivity有什么关系?

AS的布局可视化工具还是很好用,能很直观的给到我们结论。但是我们是程序员,女朋友会骗人,但是代码不会,我们只相信代码。所以,我们带着这个观点,去代码中一一找到依据。

代码论证

我们的目的是找到setContent把我们的写的XML怎么添加到Activity,所以我们只需要一直盯着我们的XML去了哪里了即可,最终肯定能理清状况。先从setContentView方法开始看

android.app.Activity

   public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(@LayoutRes int layoutResID);
        initWindowDecorActionBar();
    }

第二行看方法名应该是初始化状态栏壮相关,我们不管,盯着我们的layoutResID,所以看第一行代码。

getWindow方法返回的是一个Window类型的变量mWindow,对他进行赋值的地方,在之前讲Activity启动流程提过的Activity的attach方法中

什么是mWindow

public Window getWindow() {
        return mWindow;
    }
    
 final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
            attachBaseContext(context);

            mFragments.attachHost(null /*parent*/);
            关键代码
            mWindow = new PhoneWindow(this, window, activityConfigCallback);
            mWindow.setWindowControllerCallback(mWindowControllerCallback);
            將Activity传给Window
            mWindow.setCallback(this);
            mWindow.setOnWindowDismissedCallback(this);
            mWindow.getLayoutInflater().setPrivateFactory(this);
            ......
            将系统 WindowManager 传给 PhoneWindow mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);      
            ......
        }

可以看到mWindow实际上是PhoneWindow类型,因为Window是一个抽象类,他的唯一实现类是PhoneWindow

mWindow创建实例后,将当前Activity当回调设置了进去,主要用于一些事件的回调处理。

setWindowManager主要是将系统 WindowManager 传给 PhoneWindow ,最终,在 PhoneWindow 中持有了一个 WindowManagerImpl 的引用。 之后的文章会对对Window做一个比较详细解释,系列的第一篇文章先弄明白大体框架。

也许你经常看到说Window的唯一实现类PhoneWindow但是并不理解,其实在Window的定义中已经写的很明白,如下图

1629289966(1).png

PhoneWindow的setContent

所以我们的布局现在来到了PhoneWindow中

com.android.internal.policy.PhoneWindow
    ......
   ViewGroup mContentParent;
   ......
    @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) {
            关键代码1(初始化 DecorView 和 mContentParent)
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            关键代码2(把我们的布局丢给这个mContentParent)
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

先看第一处关键代码。我们还不知道这个mContentParent到底是这个啥,只知道是ViewGroup类型,而且在一开始可视化的的时候也有个id为content的帧布局,这两者会不会有什么联系呢?通过点击代码我看到mContentParent的创建是在installDecor方法里,而mContentParent ==null成立,也会执行installDecor,所以installDecor方法是必然要执行喽,而且可以确定一件事,初始化了这个叫mContentParent的东西。

installDecor

com.android.internal.policy.PhoneWindow
    ......
    // This is the top-level view of the window, containing the window decor.
    翻译为:这是窗口的顶层视图,包含了窗口装饰
    private DecorView mDecor;
    ......
    
    private void installDecor() {
            mForceDecorInstall = false;
            if (mDecor == null) {
                创建顶层视图DecorView
                mDecor = generateDecor(-1);
                mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
                mDecor.setIsRootNamespace(true);
                if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                    mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
                }
            } else {
                把DecorView和PhoneWindow进行绑定
                mDecor.setWindow(this);
            }
              if (mContentParent == null) {
                 创建mContentParent
                mContentParent = generateLayout(mDecor);
                ......
            }
        }

installDecor方法一开始就判断了mDecor这个东西,通过类变量的我们可以知道,mDecor是DecorView类型,上面的注释说这是窗口的顶层视图。在结合一开始可视化看到的东西。所以我们证明了第一层的结论:

DecorView类型的变量mDecor就是我们的顶层视图。而这个DecorView和PoneWindow进行了绑定,PhoneWindow在A抽创建Activity时执行的attch方法中又和Activity进行了绑定。

我们进行视图检测的时候,看不到Activity的原因也得到了解释

Activity并不是作为我们可以看到的视图存在,他负责的是某些操作和回调的控制,他有一个PhoneWindow,而我们能看到的最顶层视图DecorView就是依附在PhoneWindow上

generateDecor(创建顶层视图DecorView)

看看这个顶级视图DecorView是如何创建的吧

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, this);
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        context = getContext();
    }
    return new DecorView(context, featureId, this, getAttributes());

好吧,这里只是创建了一个DecorView,继承的是FrameLayout,也没看到之前可视化分析的时候的视图,按理说里面应该有一层线性布局的。先放一放,继续往下看能不能找到线索吧。

1629292386(1).png

创建了DecorView后调用mDecor.setWindow(this)将其与当前PhoneWindow进行了绑定

com.android.internal.policy.DecorView

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {

   ......
        void setWindow(PhoneWindow phoneWindow) {
            //当前DecorView与PhoneWindow绑定
            mWindow = phoneWindow;
            Context context = getContext();
            if (context instanceof DecorContext) {
                DecorContext decorContext = (DecorContext) context;
                decorContext.setPhoneWindow(mWindow);
            }
            if (mPendingWindowBackground != null) {
                Drawable background = mPendingWindowBackground;
                mPendingWindowBackground = null;
                setWindowBackground(background);
       }
    }
   ......
}

继续往下看,别把我们进到这个方法是为了看mContentParent这个东西是啥的事给忘了。

友情提示:前方高能,大量代码即将到达战场 mContentParent = generateLayout(mDecor);

generateLayout(创建我们XML的上层content)
  protected ViewGroup generateLayout(DecorView decor) {
     // Apply data from current theme.

        TypedArray a = getWindowStyle();
        ......
     if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
            requestFeature(FEATURE_ACTION_BAR_OVERLAY);
        }
        此处省略大量类似代码都是读取主题对Window设置一样不同样式,比如啥透明状态栏这些都是在这处理的
      ......
       // Inflate the window decor. DecorView需要用到的XML文件
       int layoutResource;
         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 = R.layout.screen_title_icons;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
            // System.out.println("Title Icons!");
        } else if{......}
        省略大量代码,都是根据系统版本已经主题样式去匹配最终DecorView需要用哪个XML文件。
       
      .......
       mDecor.startChanging();
       将匹配到的XML样式设置给mDecor(顶层视图DecorView)
       mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
       找到id为content的帧布局
       ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
       //必须存在id为content的ViewGroup,否则直接抛异常
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
       mDecor.finishChanging();
      返回我们需要的mContent
      return contentParent;
  }

如果你也是点generateLayout和一起看的。。一进去估计会被吓到,这个方法太长了。这里分享一个我看这种代码的方法:

我知道我进这个方法是找mContent是怎么创建的,所以直接去看他return了什么。然后找找返回目标的过程中能看明白啥就当是我们白捡的。

整理一下我看到这个方法做了哪些事:

  1. 读取 WindowStyle对Window设置一样不同样式
  • 这里主要是处理Window的样式,比如透明状态栏之类的
  1. 根据系统版本已经主题样式去匹配最终DecorView需要用哪个XML文件
  • 给顶层视图DecorView匹配到的XML其实就是我们在视图检测中看到的第二层视图
  • 这一步不同条件会为顶层视图DecorView设置不同的布局,我点了几个不同的XML进去看看,发现各不相同,但是都有同一个布局那就是id为content的布局且类型就是FrameLayout。 结合之前的视图工具我们可以肯定这个就是我们找的第三层的布局。
  • 通过contentParent == null就直接抛异常我们也可以确定id为content的的容器必须存在
  • 第二层也并未一定是垂直的线性布局,取决于最后设置给视图DecorView的XML具体实现。
  1. 在当前PhoneWindow中找到我们需要的ViewGroup(实则为FrameLayout)类型的contentParent。返回出去赋值给mContentParent
  • 因为DecorView已经与当前PhoneWindow绑定了所以这里findViewById其实就是从DecorView中查找View
  • 这里的id常量ID_ANDROID_CONTENT点击去可以看到定义为com.android.internal.R.id.content 所以mContentParent就是们的第三层id为content的帧布局。

再回到setContentView

再回到PhoneWindow的setContentView方法中,忘了代码的可以再回头看看。

接之前的逻辑,第一处关键代码installDecor创建初始化了DecorView 和 mContentParent。 第二处mLayoutInflater.inflate(layoutResID, mContentParent);就太简单了,把我们写的XML设置到了mContentParent容器里,而mContentParent容器就是就是id为content的FrameLayout。

梳理层次

至此,我们已经在代码中找到了4个层次的布局关系,再加上一开始可视化工具的观察,我想读者心里应该很清楚Activity,Window,View的关系了。我也装模作样的再贴个图吧

1629295759(1).png

总结

  1. 我们先是写了一个只有一个TextView的app,运行到设备后通过AS的视图检测工具查看了视图层次关系。毕竟眼见为实。
  2. 接着我们通过Activity的setContent方法开始带着我们的猜测进行论证,知道了mWindow的创建实际,以及PhoneWindow是mWindow的真实类型。也是抽象类Window在安卓系统里唯一的视线类
  3. 我们在PhoneWindow找到了setContent方法的真实视线,知道了DecorView是怎么来的,与PhoneWindow的关系,找到了id为content的布局在哪里。同时也找到了我们写想XML文件最后是被放到了id为content的布局里。所以也知道为啥叫方法名叫setContentView而不叫setView了。

预知后事如何

其实虽然我们知道Activity在attach方法里PhoneWindow但是他们是如何具体创建关系的我们还没说。同时我们也只是分析到了把写的XML最终设置给了DecorView,但是Activity还并不知道。包括View是如何绘制到屏幕上的。相应的点击事件是如何反馈到Activity上的等等。。。且听下回分解。 希望看我这篇文章的小伙伴以后面试被问到Activity,Window,View的关系时,别还是只会一句:把Window看成窗口,View就是贴纸,Activity就是控制者这种回答了。。虽然错也每次。但是有点太。。。那啥了。