Android中不得不谈的setContentView

本文深入探讨了Android中布局加载的过程,解析了setContentView方法的工作原理,揭示了布局层级的形成原因,以及DecorView和mContentParent的角色。

本文原创,转载请注明出处。
欢迎关注我的 简书 ,关注我的专题 Android Class 我会长期坚持为大家收录简书上高质量的Android相关博文。

写在前面:

几个月之前在做项目的布局优化时,使用 Hierarchy Viewer 查看项目的层级结构,然后发现顶层的布局并不是在XML中我写的根布局,而是嵌套了多层 Layout ,简单查阅了一些资料之后明白这是系统为我们加上的。把这个知识点写在了印象笔记中的 TODO list(里面还有好多知识想研究,一直在拖延T.T),搁置了好久最近重新拿出来好好研究了一下,争取做到温故知新,融会贯通嘛。

也许有的同学没看过 Hierarchy Viewer 下项目的界面布局,没关系,我现在带大家了解下。
新建一个 module ,打开 sdk tool 文件夹下的 Hierarchy Viewer ,布局结构展示如下:

MainActivity界面层级

先别着急找放大镜,想想我们新建项目的默认布局,按理说根布局应该是 RelativeLayout ,并且子 View 是一个 TextView 写着 “Hello World”才对啊~ 多出来的这些布局层级是什么

既然陌生又看不懂,那就先从我们熟悉的入手,找一下我们自己写的布局:

RelativeLayout

原来 RelativeLayout 和它的子 View TextView 在这里,看一下左下角的位置标识,红框部分指明 RelativeLayout 是 Toolbar 以下的部分。

再想想,我们是通过什么方法将这个布局填充到 Activity 上的呢?

没错是 setContentView

那就在 setContentView 中寻找蛛丝马迹吧

因为在 Android Studio 中 MainActivity 默认继承于v7包下的 AppCompatActivity ,目的是为了提供控件的向下兼容或者新控件,AppCompatActivity 也是层层继承于 Activity ,所以我们直接去看 ActivitysetContentView

    /**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
     *
     * @param layoutResID Resource ID to be inflated.
     *
     * @see #setContentView(android.view.View)
     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
     */
    public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

getWindow() 拿到了 Activity 的成员变量 mWindow ,进而调用了 setContentView() 方法,mWindow 是 Window 类,继续跟进,看看 Window 类是什么

Window类

注释中的描述翻译过来就是,Window 是 视觉和行为表现的顶层抽象基类,它的实例会当作顶层视图添加进 WindowManager , 它有一个唯一的实现类是 PhoneWindow
本文我们不会去剖析 WindowManager 有哪些作用和行为,我默默地把它加入了我的 TODO list 中,拖延到什么时候就不一定了哈T.T。

为了防止你忘了我们在做什么和我们即将做什么,先来一个中场回顾
首先我们查看布局时发现有很多“超出我们预料和理解范畴”的布局出现,跟进 setContentView() 方法,发现 Acitvity 中是 Window 调用了 setContentView() ,而抽象基类 Window 有一个唯一的实现类 PhoneWindow。不多说,来看看实现类 PhoneWindow 中的 setContentView() 方法。

    @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) {
            //初始化 DectorView 和 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 {
            //首次 setContentView 走到这里
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

当我们没有调用 setContentView() 时,mContentParent (是ViewGroup) 是 null ,所以有两行代码值得我们关注 installDecor()mLayoutInflater.inflate(layoutResID, mContentParent)
首先 mContentParent 作为第二个参数传入了 inflate 方法中, 也就是说 我的布局中的 RelativeLayout 被层层解析之后的 View 视图树 作为了 mContentParent 的子 View 插入。

现在不知道 mContentParent 是什么没关系,继续跟进 installDecor() 方法。

随着API level的升高,源码发生了很多有关 Feature 、 Style 和 Wiget 的细微变化,还是蛮有意思的
这里我还想说一句,相信在 Android 设计之初 PhoneWindow 这个类就存在了,显然现在的这个命名有些问题,毕竟目前的设备不仅仅是 phone 了,也许改成 DeviceWindow 会比较合适

    private void installDecor() {
        if (mDecor == null) {
            // new 一个 DecorView
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        }
        if (mContentParent == null) {
            //初始化 mContentParent 
            mContentParent = generateLayout(mDecor);
            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();
            // 找到一个带ActionBar属性的布局容器 decorContentParent 
            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
                mDecorContentParent = decorContentParent;
                mDecorContentParent.setWindowCallback(getCallback());             
                //配置UI设置
                mDecorContentParent.setUiOptions(mUiOptions);
            }
         } else {
             if (mContentParent instanceof FrameLayout) {
                  ((FrameLayout)mContentParent).setForeground(null);
                }
         }                   
    }

省略了与分析无关的代码,其中很多是对 feature 和 style 属性的一些判断和设置,首先 installDecor() 方法从字面意思看,很有可能是初始化加载 DecorView 的,首先看看 PhoneWindow 中两个成员变量 mDecormContentParent 分别是什么:

mDector 和 mContentParent

描述的信息可以概括为 mDector 是 窗体的顶级视图,mContentParent 是放置窗体内容的容器,也就是我们 setContentView() 时,所加入的 View 视图树。

当二者为 null 时,有两行代码值得关注,分别为 mDecor = generateDecor()mContentParent = generateLayout(mDecor)

不过在此之前,先来看看这行寻找 decorContentParent 布局的代码

final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

decor_content_parent 看起来很眼熟的样子,点击它进入布局来看看:

screen_toolbar.xml

为什么说 decor_content_parent 眼熟呢?打开布局查看器来看看

这里写图片描述

Hierarchy Viewer 中可以看到 ActionBarOverlayLayout 的布局文件的 id 正是 decor_content_parent 不光如此 布局文件中的每个 View 节点的名称和 id 都与 Hierarchy Viewer 视图中的一一对应。再看其中的 FrameLayout 的 id 为 content , 我们自然而然的猜测它就是我们根布局 RelativeLayout 的父布局,心里一下有了底,继续研究~

跟进 generateDecor() 方法:

    protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }

这个没什么可多说的,就是为我们的窗体 new 了 一个 DecorView 。

再来看 generateLayout(mDecor)

    protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        // 获得窗体的 style 样式
        TypedArray a = getWindowStyle();

        // 省略大量无关代码

        // Inflate the window decor.
        int layoutResource;
        int features = getLocalFeatures();

        //填充带有 style 和 feature 属性的 layoutResource (是一个layout id)

        View in = mLayoutInflater.inflate(layoutResource, null);

        // 插入的顶层布局 DecorView 中

        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;

        // 找到我们XML文件的父布局 contentParent 

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
        // 省略无关代码
        mDecor.finishChanging();
        // 返回 contentParent 并赋值给成员变量 mContentParent
        return contentParent;
    }

这个方法的代码有300多行,剔除了很多无关代码,我们分模块来看:

    View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;

首先 layoutResource 是系统的 xml 布局文件的 id,里面有我们设置窗体的 features 和 style 属性,然后通过 decor.addView 添加进 mDector 视图。这里也是我们要在 setContentView() 之前执行requestWindowFeature() 才可以的原因

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        // Remaining setup -- of background and title -- that only applies
        // to top-level windows.

        mDecor.finishChanging();

        return contentParent;

关键点来了, ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
通过 findViewById 找到系统修饰布局文件中 id 为:
这里写图片描述

这个 id 是不是非常眼熟,与我们上文的猜测不谋而合,这就是我们一直在寻找的作为 activity_main 的父布局的 FrameLayout

我们在布局文件查看器中再找一下:

容器 FrameLayout id = content

return contentParent 这一步就返回了我们的成员变量 mContentParent

到现在为止其实整个知识点主干的逻辑已经走完了,为大家花了一张简单的思维导图

调用逻辑

并不复杂,线性逻辑调用还是蛮清晰的。

不过相信你也许会问,上文你仅仅提到了两个布局呀,一个顶层的 DecorView 和 我们布局文件的父布局 FrameLayout ,而查看布局层级时,为什么有这么多其他这么多额外的布局呢?

因为随着 Android API level 的不断变化,组件也在随之增多,比如 ActionBar Toolbar 等等,这些组件相关的布局是否加载与你的 feature 设置设备的特性相关联,而且版本不同,布局文件的层级结构也在不断变化着丰富着,我这个是 API22 的源码,我做了一些对比,有许多代码细节是不一样的,比如在这里的 feature 就新增了 Toolbar ,但是大体上的逻辑框架肯定不会变
比如我们目前的 MainActivity 的视图主要有两大分支,一条设置 Toolbar 的相关配置,一条就是我们的 RelativeLayout 了。

写在后面:
写这篇博客的原因一是我自己要研究梳理总结这个知识点,二是想让大家明白,Android 版本之间的迭代很快,一年前的博客阐述的观点到今天可能就再不适用了,但是 PhoneWindow 管理布局视图的这套逻辑框架,却一直没怎么改变。通过阅读源码,可以学习 Google 工程师们良好的代码风格,汲取他们搭建框架的思想,让我们自己写的代码也能如此健壮。

PS: PhoneWindow 什么时候能改个名字啊!

中国乡村创新创业指,由浙江大学卡特农业与农村经济发展研究院与企业研究团队携手打造,是一项全面评估中国乡村地区创新与创业活力的权威指标体系。该指体系由“乡村创新”与“乡村创业”两大核心一级指标构成,深度剖析了乡村发展的内在动力与外在表现。 在“乡村创新”维度下,进一步细化为技术创新、品牌创新、绿色创新、数字创新四个二级指标,全面覆盖了乡村在科技应用、品牌建设、环境保护及数字化转型等方面的创新能力。而“乡村创业”则聚焦于农业及相关产业创业、农民合作社创业、家庭农场创业三大板块,展现了乡村经济多元化发展的创业生态。 这七大二级指标进一步拆解为21个三级指标,确保了评价的全面性和精准性。指自2014年起至2021年连续发布,不仅展现了近年来中国乡村创新创业的演进轨迹,还通过横纵向对比,为研究人员、投资者及社会各界提供了参考依据,助力乡村振兴战略深入实施。 引用规范:阮建青、杨奇明、叶武威、张雨薇,2023,《中国乡村创新创业指报告(2023)》,浙大卡特-企研乡村产业研究团队工作论文 一、据介绍 据名称:中国省、市、县级乡村创新创业指 据年份:2014-2021年 样本量:1万7+条 据格式:面板据 二、指标说明 共计10个指标:省份代码、省份名称、地市代码、地市名称、县市代码、县市名称、年份、乡村创业指、乡村创新指、总指 三、据文件 中国省、市、县级乡村创新创业指(2014-2021年).xlsx
内容概要:本文围绕分布式传感器网络中的LEACH聚类算法展开深入研究,重点评估其在能量消耗方面的性能表现,并通过Matlab代码实现完整的仿真系统。文中系统阐述了LEACH协议的核心工作机制,包括基于概率的簇头选举机制、据融合与多跳传输策略,详细分析了其在延长网络生命周期、降低能耗方面的优势与存在的局限性。研究构建了标准化的无线传感器网络模型,通过仿真实验对比不同节点分布、初始能量配置和轮询机制下的能量耗尽过程,定量评估网络存活时间、死亡节点增长曲线和剩余能量分布等关键指标,进而提出针对性的优化方向以提升算法的能量效率与系统稳定性。此外,文档还关联了智能优化算法、WSN覆盖优化、路由优化等多项关键技术,体现了LEACH协议在物联网、环境监测和智能感知等领域的广泛应用价值。; 适合人群:具备一定Matlab编程基础和无线通信基础知识,从事无线传感器网络、物联网工程、通信系统设计及相关方向研究的科研人员、高校研究生及工程技术开发者。; 使用场景及目标:①用于教学与科研中对LEACH协议运行机制的理解与仿真实践;②为优化无线传感器网络的能量管理策略和拓扑控制提供理论依据与实验平台;③作为进一步研究低功耗路由协议、簇头优化算法及大规模WSN部署的基础工具; 阅读建议:建议读者结合提供的Matlab代码进行实际运行与参调优,深入理解每一轮迭代中簇头选择、据聚合和能量更新的实现细节,同时可参考文中提及的Leach协议优化方向拓展研究思路,提升系统级仿真与算法创新能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值