引言
- 这是 Android 10 源码分析系列的第 3 篇
- 分支:android-10.0.0_r14
- 全文阅读大概 15 分钟
通过这篇文章你将学习到以下内容,文末会给出相应的答案
- LayoutInflater的inflate 方法的三个参数都代表什么意思?
- 系统对 merge、include 是如何处理的
- merge 标签为什么可以起到优化布局的效果?
- XML 中的 View 是如何被实例化的?
- 为什么复杂布局会产生卡顿?在 Android 10 上做了那些优化?
- BlinkLayout 是什么?
前面两篇文章 0xA01 Android 10 源码分析:APK 是如何生成的 和 0xA02 Android 10 源码分析:APK 的安装流程 分析了 APK 大概可以分为代码和资源两部分,那么 APK 的加载也是分为代码和资源两部分,代码的加载涉及了进程的创建、启动、调度,本文主要来分析一下资源的加载,如果没有看过 APK 是如何生成的 和 APK 的安装流程 可以点击下方连接前往:
- Android 资源
Android 资源大概分为两个部分:assets 和 res
assets 资源
assets 资源放在 assets 目录下,它里面保存一些原始的文件,可以以任何方式来进行组织,这些文件最终会原封不动的被打包进 APK 文件中,通过AssetManager 来获取 asset 资源,代码如下
1 | ini复制代码AssetManager assetManager = context.getAssets(); |
res资源
res 资源放在主工程的 res 目录下,这类资源一般都会在编译阶段生成一个资源 ID 供我们使用,res 目录包括 animator、anim、 color、drawable、layout、menu、raw、values、XML等,通过 getResource() 去获取 Resources 对象
1 | ini复制代码Resources res = getContext().getResources(); |
APK 的生成过程中,会生成资源索引表 resources.arsc 文件和 R.java 文件,前者资源索引表 resources.arsc 记录了所有的应用程序资源目录的信息,包括每一个资源名称、类型、值、ID以及所配置的维度信息,后者定义了各个资源 ID 常量,运行时通过 Resources 和 AssetManger 共同完成资源的加载,如果资源是个文件,Resouces 先根据资源 ID 查找出文件名,AssetManger 再根据文件名查找出具体的资源,关于 resources.arsc,可以查看 0xA01 ASOP应用框架:APK 是如何生成的
- 资源的加载和解析到 View 的生成
下面代码一定不会很陌生,在 Activity 常见的几行代码
1 | kotlin复制代码override fun onCreate(savedInstanceState: Bundle?) { |
一起来分析一下调用 setContentView 方法之后做了什么事情,接下来查看一下 Activity 中的 setContentView 方法
frameworks/base/core/java/android/app/Activity.java
1 | less复制代码public void setContentView(@LayoutRes int layoutResID) { |
调用 getWindow 方法返回的是 mWindow,mWindow 是 Windowd 对象,实际上是调用它的唯一实现类 PhoneWindow.setContentView 方法
2.1 Activity -> PhoneWindow
PhoneWindow 是 Window 的唯一实现类,它的结构如下:
当调用 Activity.setContentView 方法实际上调用的是 PhoneWindow.setContentView 方法
frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
1 | scss复制代码public void setContentView(int layoutResID) { |
- 先判断 mContentParent 是否为空,如果为空则调用 installDecor 方法,生成 mDecor,并将它赋值给 mContentParent
- 根据 FEATURE_CONTENT_TRANSITIONS 标记来判断是否加载过转场动画
- 如果设置了 FEATURE_CONTENT_TRANSITIONS 则添加 Scene 来过度启动,否则调用 mLayoutInflater.inflate(layoutResID, mContentParent),解析资源文件,创建 View, 并添加到 mContentParent 视图中
2.2 PhoneWindow -> LayoutInflater
当调用 PhoneWindow.setContentView 方法,之后调用 LayoutInflater.inflate 方法,来解析 XML 资源文件
frameworks/base/core/java/android/view/LayoutInflater.java
1 | less复制代码public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { |
inflate 它有多个重载方法,最后调用的是 inflate(resource, root, root != null) 方法
frameworks/base/core/java/android/view/LayoutInflater.java
1 | less复制代码public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { |
这个方法主要做了三件事:
- 根据 XML 预编译生成 compiled_view.dex, 然后通过反射来生成对应的 View
- 获取 XmlResourceParser
- 解析 View
注意:在目前的 release 版本中不支持使用 tryInflatePrecompiled 方法源码如下:
1 | java复制代码private void initPrecompiledViews() { |
- tryInflatePrecompiled 方法是 Android 10 新增的方法,这是一个在编译器运行的一个优化,因为布局文件越复杂 XmlPullParser 解析 XML 越耗时, tryInflatePrecompiled 方法根据 XML 预编译生成compiled_view.dex, 然后通过反射来生成对应的 View,从而减少 XmlPullParser 解析 XML 的时间,然后根据 attachToRoot 参数来判断是添加到根布局中,还是设置 LayoutParams 参数返回给调用者
- 用一个全局变量 mUseCompiledView 来控制是否启用 tryInflatePrecompiled 方法,根据源码分析,mUseCompiledView 始终为 false
了解了 tryInflatePrecompiled 方法之后,在来查看一下 inflate 方法中的三个参数都什么意思
- resource:要解析的 XML 布局文件 ID
- root:表示根布局
- attachToRoot:是否要添加到父布局 root 中
resource 其实很好理解就是资源 ID,而 root 和 attachToRoot 分别代表什么意思:
- 当 attachToRoot == true 且 root != null 时,新解析出来的 View 会被 add 到 root 中去,然后将 root 作为结果返回
- 当 attachToRoot == false 且 root != null 时,新解析的 View 会直接作为结果返回,而且 root 会为新解析的 View 生成 LayoutParams 并设置到该 View 中去
- 当 attachToRoot == false 且 root == null 时,新解析的 View 会直接作为结果返回
根据源码知道调用 tryInflatePrecompiled 方法返回的 view 为空,继续往下执行调用 Resources 的 getLayout 方法获取资源解析器 XmlResourceParser
2.3 LayoutInflater -> Resources
上面说到 XmlResourceParser 是通过调用 Resources 的 getLayout 方法获取的,getLayout 方法又去调用了 Resources 的loadXmlResourceParser 方法
frameworks/base/core/java/android/content/res/Resources.java
1 | less复制代码public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException { |
TypedValue 是动态的数据容器,主要用来存储 Resource 的资源,获取 XML 资源保存到 TypedValue,之后调用 ResourcesImpl 的 loadXmlResourceParser 方法加载对应的解析器
2.4 Resources -> ResourcesImpl
ResourcesImpl 实现了 Resource 的访问,它包含了 AssetManager 和所有的缓存,通过 Resource 的 getValue 方法获取 XML 资源保存到 TypedValue,之后就会调用 ResourcesImpl 的 loadXmlResourceParser 方法对该布局资源进行解析
frameworks/base/core/java/android/content/res/ResourcesImpl.java
1 | less复制代码XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie, |
首先从缓存中查找 XML 资源之后调用 newParser 方法,如果缓存中没有,则调用 AssetManger 的 openXmlBlockAsset 方法创建一个 XmlBlock,并将它放到缓存中,XmlBlock 是已编译的 XML 文件的一个包装类
frameworks/base/core/java/android/content/res/AssetManager.java
1 | java复制代码XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName) throws IOException { |
最终调用 native 方法 nativeOpenXmlAsset 去打开指定的 XML 文件,加载对应的资源,来查看一下 navtive 方法 NativeOpenXmlAsset
frameworks/base/core/jni/android_util_AssetManager.cpp
1 | ini复制代码// java方法对应的native方法 |
- C++ 层的 NativeOpenXmlAsset 方法会创建 ResXMLTree 对象,返回的是 ResXMLTree 在 C++ 层的地址
- Java 层 nativeOpenXmlAsse t方法的返回值 xmlBlock 是 C++ 层的 ResXMLTree 对象的地址,然后将 xmlBlock 封装进 XmlBlock 中返回给调用者
当 xmlBlock 创建之后,会调用 newParser 方法,构建一个 XmlResourceParser 对象,返回给调用者
2.5 ResourcesImpl -> XmlBlock
XmlBlock 是已编译的 XML 文件的一个包装类,XmlResourceParser 负责对 XML 的标签进行遍历解析的,它的真正的实现是 XmlBlock 的内部类 XmlBlock.Parser,而真正完成 XML 的遍历操作的函数都是由 XmlBlock 来实现的,为了提升效率都是通过 JNI 调用 native 的函数来做的,接下来查看一下 newParser 方法
frameworks/base/core/java/android/content/res/XmlBlock.java
1 | java复制代码public XmlResourceParser newParser(@AnyRes int resId) { |
这个方法做两件事
- mNative 是 C++ 层的 ResXMLTree 对象的地址,调用 native 方法 nativeCreateParseState,在 C++ 层构建一个 ResXMLParser 对象,返回 ResXMLParser 对象在 C++ 层的地址
- Java 层拿到 ResXMLParser 在 C++ 层地址,构建 Parser,封装 ResXMLParser,返回给调用者
接下来查看一下 native 方法 nativeCreateParseState
frameworks/base/core/jni/android_util_XmlBlock.cpp
1 | scss复制代码// java方法对应的native方法 |
- token 对应 Java 层 mNative,是 C++ 层的 ResXMLTree 对象的地址
- 调用 C++ 层 android_content_XmlBlock_nativeCreateParseState 方法,根据 token找到 ResXMLTree 对象
- 在 C++ 层构建一个 ResXMLParser 对象,返给 Java 层对应 ResXMLParser 对象在 C++ 层的地址
- Java 层拿到 ResXMLParser 在 C++ 层地址,封装到 Parser 中
2.6 再次回到 LayoutInflater
经过一系列的跳转,最后调用 XmlBlock.newParser 方法获取资源解析器 XmlResourceParser,之后回到 LayoutInflater 调用处 inflate 方法,然后调用 rInflate 方法解析 View
frameworks/base/core/java/android/view/LayoutInflater.java
1 | java复制代码public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { |
- 解析 merge 标签,使用 merge 标签必须有父布局,且依赖于父布局加载
- rInflate 方法会将 merge 标签下面的所有 View 添加到根布局中
- 如果不是 merge 标签,调用 createViewFromTag 解析布局视图,返回 temp, 这里的 temp 其实是我们 XML 里的 Top View
- 调用 rInflateChildren 方法,传递参数 temp,在 rInflateChildren方 法里内部,会调用 rInflate 方法, 解析当前 View 下面的所有子 View
通过分析源码知道了attachToRoot 和 root的参数代表什么意思,这里总结一下:*\
- 当 attachToRoot == true 且 root != null 时,新解析出来的 View 会被 add 到 root 中去,然后将 root 作为结果返回
- 当 attachToRoot == false 且 root != null 时,新解析的 View 会直接作为结果返回,而且 root 会为新解析的View生成 LayoutParams并设置到该 View 中去
- 当 attachToRoot == false 且 root == null 时,新解析的 View 会直接作为结果返回
无论是不是 merge 标签,最后都会调用 rInflate 方法进行 View 树的解析,他们的区别在于,如果是 merge 标签传递的参数 finishInflate 是 false,如果不是 merge 标签传递的参数 finishInflate 是 true
frameworks/base/core/java/android/view/LayoutInflater.java
1 | scss复制代码void rInflate(XmlPullParser parser, View parent, Context context, |
整个 View 树的解析过程如下:
- 获取 View 树的深度
- 逐个 View 解析
- 解析 android:focusable=”true”, 获取 View 的焦点
- 解析 android:tag 标签
- 解析 include 标签,并且 include 标签不能作为根布局
- 解析 merge 标签,并且 merge 标签必须作为根布局
- 根据元素名解析,生成对应的 View
- rInflateChildren 方法内部调用的 rInflate 方法,深度优先遍历解析所有的子 View
- 添加解析的 View
注意:通过分析源码, 以下几点需要特别注意
- include 标签不能作为根元素,需要放在 ViewGroup中
- merge 标签必须为根元素,使用 merge 标签必须有父布局,且依赖于父布局加载
- 当 XmlResourseParser 对 XML 的遍历,随着布局越复杂,层级嵌套越多,所花费的时间也越长,所以对布局的优化,可以使用 meger 标签减少层级的嵌套
在解析过程中调用 createViewFromTag 方法,根据元素名解析,生成对应的 View,接下来查看一下 createViewFromTag 方法
frameworks/base/core/java/android/view/LayoutInflater.java
1 | ini复制代码private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) { |
- 解析 View 标签,如果设置了 theme, 构建一个 ContextThemeWrapper
- 调用 tryCreateView 方法,如果 name 是 blink,则创建 BlinkLayout,如果设置 factory,根据 factory 进行解析,这是系统留给我们的 Hook 入口,我们可以人为的干涉系统创建 View,添加更多的功能
- 如果 tryCreateView 方法返回的 View 为空,则分别调用 onCreateView 方法和 createView 方法,onCreateView 方法解析内置 View,createView 方法解析自定义 View
在解析过程中,会先调用 tryCreateView 方法,来看一下 tryCreateView 方法内部做了什么
frameworks/base/core/java/android/view/LayoutInflater.java
1 | less复制代码public final View tryCreateView(@Nullable View parent, @NonNull String name, |
- 如果 name 是 blink,则创建 BlinkLayout,返给调用者
- 如果设置 factory,根据 factory 进行解析, 这是系统留给我们的 Hook 入口,我们可以人为的干涉系统创建 View,添加更多的功能,例如夜间模式,将 View 返给调用者
根据刚才的分析,会先调用 tryCreateView 方法,如果这个方法返回的 View 为空,然后会调用 onCreateView 方法对内置 View 进行解析,createView 方法对自定义 View 进行解析
onCreateView 方法与 createView 方法的有什么区别
- onCreateView 方法:会给内置的 View 前面加一个前缀,例如: android.widget,最终会调用 createView 方法
- createView 方法: 根据完整的类的路径名利用反射机制构建 View 对象
来看一下这两个方法的实现,LayoutInflater 是一个抽象类,我们实际使用的是 PhoneLayoutInflater,它的结构如下
PhoneLayoutInflater 重写了 LayoutInflater 的 onCreatView 方法,这个方法就是给内置的 View 前面加一个前缀
frameworks/base/core/java/com/android/internal/policy/PhoneLayoutInflater.java
1 | java复制代码private static final String[] sClassPrefixList = { |
onCreateView 方法会给内置的 View 前面加一个前缀,之后调用 createView 方法,真正的 View 构建还是在 LayoutInflater 的 createView 方法里完成的,createView 方法根据完整的类的路径名利用反射机制构建 View 对象
frameworks/base/core/java/android/view/LayoutInflater.java
1 | less复制代码public final View createView(@NonNull Context viewContext, @NonNull String name, |
- 先从缓存中寻找构造函数,如果存在直接使用
- 如果没有找到根据完整的类的路径名利用反射机制构建 View 对象
到了这里关于 APK 的布局 XML 资源文件的查找和解析 -> View 的生成流程到这里就结束了
总结
那我们就来依次来回答上面提出的几个问题
LayoutInflater 的 inflate 的三个参数都代表什么意思?
- resource:要解析的 XML 布局文件 ID
- root:表示根布局
- attachToRoot:是否要添加到父布局 root 中
resource 其实很好理解就是资源 ID,而 root 和 attachToRoot 分别代表什么意思:
- 当 attachToRoot == true 且 root != null 时,新解析出来的 View 会被 add 到 root 中去,然后将 root 作为结果返回
- 当 attachToRoot == false 且 root != null 时,新解析的 View 会直接作为结果返回,而且 root 会为新解析的 View 生成 LayoutParams 并设置到该 View 中去
- 当 attachToRoot == false 且 root == null 时,新解析的 View 会直接作为结果返回
系统对 merge、include 是如何处理的
- 使用 merge 标签必须有父布局,且依赖于父布局加载
- merge 并不是一个 ViewGroup,也不是一个 View,它相当于声明了一些视图,等待被添加,解析过程中遇到 merge 标签会将 merge 标签下面的所有子 view 添加到根布局中
- merge 标签在 XML 中必须是根元素
- 相反的 include 不能作为根元素,需要放在一个 ViewGroup 中
- 使用 include 标签必须指定有效的 layout 属性
- 使用 include 标签不写宽高是没有关系的,会去解析被 include 的 layout
merge 标签为什么可以起到优化布局的效果?
解析过程中遇到 merge 标签,会调用 rInflate 方法,部分代码如下
1 | ini复制代码// 根据元素名解析,生成对应的View |
解析 merge 标签下面的所有子 View,然后添加到根布局中
View 是如何被实例化的?
View 分为系统 View 和自定义 View, 通过调用 onCreateView 与createView 方法进行不同的处理
- onCreateView 方法:会给内置的 View 前面加一个前缀,例如:android.widget,最终会调用 createView 方法
- createView 方法:根据完整的类的路径名利用反射机制构建 View 对象
为什么复杂布局会产生卡顿?在 Android 10 上做了那些优化?
- XmlResourseParser 对 XML 的遍历,随着布局越复杂,层级嵌套越多,所花费的时间也越长
- 调用 onCreateView 与 createView 方法是通过反射创建 View 对象导致的耗时
- 在 Android 10上,新增 tryInflatePrecompiled 方法是为了减少 XmlPullParser 解析 XML 的时间,但是用一个全局变量 mUseCompiledView 来控制是否启用 tryInflatePrecompiled 方法,根据源码分析,mUseCompiledView 始终为 false,所以 tryInflatePrecompiled 方法目前在 release 版本中不可使用
BlinkLayout 是什么?
BlinkLayout 继承 FrameLayout,是一种会闪烁的布局,被包裹的内容会一直闪烁,根据源码注释 Let’s party like it’s 1995!,BlinkLayout 是为了庆祝 1995 年的复活节, 有兴趣可以看看 reddit 上的讨论,来查看一下它的源码是如何实现的
1 | java复制代码private static class BlinkLayout extends FrameLayout { |
通过源码分析可以看出,BlinkLayout 通过 Handler 每隔 500ms 发送消息,在 handleMessage 中循环调用 invalidate 方法,通过调用 invalidate 方法,来触发 dispatchDraw 方法,做到一闪一闪的效果
参考
结语
致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,文章中有什么没有写明白的地方,或者有什么更好的建议欢迎留言,欢迎一起来学习,在技术的道路上一起前进。
计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。
算法
由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。
- 数据结构: 数组、栈、队列、字符串、链表、树……
- 算法: 查找算法、搜索算法、位运算、排序、数学、……
每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。
Android 10 源码系列
正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。
- 0xA01 Android 10 源码分析:APK 是如何生成的
- 0xA02 Android 10 源码分析:APK 的安装流程
- 0xA03 Android 10 源码分析:APK 加载流程之资源加载
- 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
- 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
- 更多……
工具系列
- 为数不多的人知道的 AndroidStudio 快捷键(一)
- 为数不多的人知道的 AndroidStudio 快捷键(二)
- 关于 adb 命令你所需要知道的
- 如何高效获取视频截图
- 10分钟入门 Shell 脚本编程
- 如何在项目中封装 Kotlin + Android Databinding
逆向系列
本文转载自: 掘金