嗨,我是写博客满脑子骚东西
的哈利迪~今天和大伙聊聊Android中的xml和view的那些事,首先会分析一下xml布局解析inflate
的流程,然后会介绍一些业内的方案,如:
提效
篇:
- JakeWharton:著名的
Butterknife
、 - Android自带:双向绑定的
DataBinding
、省去findViewById的ViewBinding
和kotlin扩展
、
性能优化
篇:
- 掌阅:将xml转view的流程提前到编译期的
x2c
、 - 鸿洋大佬最近研究的:自定义Factory来创建view的思路
ViewOpt
、 - 天猫:把xml压缩成二进制文件,可动态下发、流式解析的
VirtualView
、
本文约5000字,阅读大约13分钟。如个别大图模糊,可前往个人站点阅读。
inflate
java层
源码基于compileSdkVersion 29 和 androidx.appcompat:appcompat:1.1.0
通常,我们在开发布局的时候都是采用xml,这么做的好处一是可拖拽可预览
,二是语法简单清晰
,然后在Activity中setContentView
,即可完成布局的加载,那具体流程是怎么样的呢?主要分为三步,io读取xml文件,parser解析xml结构得到view树,反射创建view。我们从setContentView
开始,
1 | //AppCompatActivity.java |
可见,核心实现交给了LayoutInflater
,跟进inflate
方法,
1 | //LayoutInflater.java |
XmlResourceParser
是一个接口,实现了XmlPullParser
(解析xml的布局结构)和 AttributeSet
(解析xml标签属性)两个接口,我们先往下跟inflate
,
1 | //LayoutInflater.java |
继续跟进rInflateChildren
,
1 | //LayoutInflater.java |
跟进createViewFromTag
,
1 | //LayoutInflater.java |
跟进createView
,
1 | //LayoutInflater.java |
以上就是常规流程,如果有设置工厂,则可以在tryCreateView
中就把view给创建了。利用工厂可以做一些全局处理,比如一键切换皮肤、字体等,
1 | //LayoutInflater.java |
整体流程图如下,
需要注意的是,目前系统的AppCompatActivity
有帮我们设置一个默认工厂,
AppCompatActivity#onCreate ->
delegate.installViewFactory();
AppCompatDelegateImpl#installViewFactory ->
LayoutInflaterCompat.setFactory2(layoutInflater, this);
在AppCompatDelegateImpl
中,
AppCompatDelegateImpl#createView ->
return mAppCompatViewInflater.createView(…);
在AppCompatViewInflater
中可见,我们常见的一些view都被转换成AppCompat的view了,他们的创建不需要走反射逻辑。
1 | //AppCompatViewInflater.java |
native层
那么java层的parser
的具体实例是谁呢?跟进XmlResourceParser parser = res.getLayout(resource)
,最终发现是XmlBlock.Parser
,我们试着跟下parser
的getName
方法,他的实现交给了native层的nativeGetName
,
native源码基于Android 9.0
native函数动态注册,android_util_XmlBlock.cpp:
1 | //android_util_XmlBlock.cpp |
来看到ResXMLParser
的getElementNameID
方法,ResourceTypes.cpp:
1 | //ResourceTypes.cpp |
先看下ResXMLTree_attrExt
是啥,在ResourceTypes.h:
1 | //ResourceTypes.h |
可见,xml被二进制处理时,会把多个相同的字符串压缩成一份存进常量池里,如:
根据位置index字段,就可以知道标签名字是啥了,常量池的处理可以减小xml体积,
文章前边留了个todo1:dtohl是啥,谷歌一下dtohl,发现这些函数被定义在ByteOrder.h里,
1 | //ByteOrder.h |
哈迪能力有限,只能跟到这里了。我们知道运行时解析的xml是经过预处理的二进制文件(apk打包时做的),那我们可以大胆猜测一下,运行时的解析是不是在做一些流式、指针移位之类的读操作?比如,把xml二进制文件进行各种分区,如文件头、标签区、属性区、字符串常量池区,然后解析时则用如readShort、readLong之类的方式进行指针移位,从而读出相应的view标签、view属性,有点类似JVM解析字节码的过程。(能力有限,仅做猜测)
小结
- 预编译
tryInflatePrecompiled
:谷歌正在做的事情,还没开放,敬请期待。 - xml文件的预处理:打包时将xml进行二进制编译,压缩xml体积、提升运行时的解析效率。(猜测:二进制的流式、指针移位操作,解析效率要比原始的xml高)
Butterknife
Butterknife
在编译期通过Apt(注解处理器)处理注解,JavaPoet(辅助生成Java文件的工具)创建类,来省去findViewById、setOnclickListener这些繁琐的操作。哈迪使用时还是在大学的时候,工作后也没接触过了,现在这个项目的作者已经不再维护了,他推荐我们去使用ViewBinding
,不过我们还是简单回顾下吧~
引入依赖:
1 | implementation 'com.jakewharton:butterknife:10.2.1' |
简单使用:
1 | class ButterknifeActivity extends AppCompatActivity { |
跟进bind
方法,
1 | //ButterKnife.java |
跟进findBindingConstructorForClass
,
1 | static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) { |
ButterknifeActivity_ViewBinding
类是由Butterknife
创建的,代码不多,
1 | class ButterknifeActivity_ViewBinding implements Unbinder { |
可见,Butterknife
只有在创建Unbinder
实例的时候用了反射,所以对运行时性能的影响是不大的。Apt处理注解和创建类的常规流程就不分析了哈~
优势:
- 省去findViewById、setOnclickListener这些繁琐的操作
- 反射操作很少,对运行时性能影响不大
缺点:
- apt创建类,增加io耗时,类编译耗时
- 类的增多,意味着包体积增大
DataBinding/ViewBinding/kotlin扩展
DataBinding
DataBinding
可以通过binding对象直接访问到xml布局里的有id控件,而且他还能实现数据和UI的双向绑定
,即数据驱动UI刷新,UI操作修改数据,双向绑定
不是本文重点,本文主要讨论xml和view的事儿~
简单使用:
1 | // app/build.gradle里android{}加上开关 |
xml布局转成data binding layout,也就是在布局外层包一层layout标签,然后多出一个data标签表示数据区,
1 | <layout xmlns:android="http://schemas.android.com/apk/res/android" |
在activity中,通过DataBindingUtil
得到binding对象,
1 | class DBActivity extends AppCompatActivity { |
那DataBinding
是怎么做到的呢?也是通过生成额外的一些类来实现的,感兴趣可以看下哈迪之前写的笔记-DataBinding,我们直接看生成的类app/build/generated/data_binding_base_class_source_out/debug/out/com/holiday/srccodestudy/databinding/ActivityDBBinding.java
,
1 | abstract class ActivityDBBinding extends ViewDataBinding { |
ViewBinding
ViewBinding
省去了双向绑定的逻辑,比DataBinding
更轻量,用法差不多,不过需要Android studio 3.6开始才能使用,
1 | // app/build.gradle里android{}加上开关 |
打开开关后,默认会给所有布局生成java类,不像DataBinding
需要包上一层layout标签。如果个别布局不需要开启ViewBinding
,可以给布局的根标签加上tools:viewBindingIgnore="true"
。
在activity中使用,有点不同,
1 | class VBActivity extends AppCompatActivity { |
ViewBinding
的具体实现暂不关注,直接看他的生成类app/build/generated/data_binding_base_class_source_out/debug/out/com/holiday/srccodestudy/databinding/ActivityVBBinding.java
,路径跟DataBinding
一样的,
1 | final class ActivityVBBinding implements ViewBinding { |
ViewBinding
省去了DataBinding
的双向绑定
功能(不用处理DataBinding
的注解、表达式等),更专注于解决findViewById的问题,所以更轻量,编译更快。
kotlin扩展
如果项目有使用kotlin,还可以使用kotlin的扩展插件来免去findViewById操作。
使用kotlin扩展插件,
1 | // app/build.gradle |
在activity中使用,
1 | class KotlinActivity : AppCompatActivity() { |
使用kotlin扩展插件有个明显的问题,就是控件的“裸奔”问题,比如我在activity中输入tv,就会把其他页面的控件也提示出来,
如果不小心导入了别的页面才有的控件,编译期没问题,运行的时候就才抛异常。也就是说,使用kotlin扩展插件,所有控件都处于不安全的裸奔状态。
使用AS反编译一下KotlinActivity,Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile
,
1 | final class KotlinActivity extends AppCompatActivity { |
至于kotlin如何插入这些代码的,能力有限,哈迪也不知道,有了解的朋友评论区聊起来~
小结
如果不做数据和UI的双向绑定
,只是为了避免findViewById,优先使用更轻量的ViewBinding
,否则使用DataBinding
。DataBinding
和ViewBinding
在避免了findViewById繁琐工作的同时,还确保了空安全
和类型安全
,即不会出现findViewById得到null、view cast exception的问题。当然,这两种方式也是避免不了生成类的编译耗时和包体积增大的问题的,得结合具体场景来使用。至于kotlin扩展,存在控件裸奔问题,不太推荐。
至此,提效
篇就介绍到这里了,下面让我们开始性能优化
篇~
x2c
x2c
是使用Apt+JavaPoet技术,在编译期将xml布局转成view类,免去了运行时解析xml的耗时。
引入依赖:
1 | annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2' |
简单使用:
1 | //给布局文件声明一个注解 |
跟进X2C.inflate
,
1 | //X2C.java |
来看到view创建器X2C0_layout_x2c_test
,
1 | class X2C0_layout_x2c_test implements IViewCreator { |
跟进X2C0_Layout_X2c_Test
可见,xml的标签和属性,都被解析成了java类的相应设置,
1 | class X2C0_Layout_X2c_Test implements IViewCreator { |
优势:
- 将xml解析提前到编译期,免去了运行时解析的耗时和内存
- 只在获取view创建器时用了反射,对运行时性能影响不大
缺点:
- apt创建类,增加io耗时,类编译耗时
- 类的增多,意味着包体积增大
所以,通常只在个别复杂度较高,有性能瓶颈的页面才会使用。
ViewOpt
鸿洋大佬的方案,是从避免反射创建view的角度去做优化的,即使用自定义工厂Factory来创建view,绕开反射逻辑。核心流程就是,先通过merge.xml来收集xml中用到的view集合,然后Apt生成一个类来处理集合,然后干预默认工厂Factory来插入自己的view创建逻辑。
1 | class BaseActivity extends AppCompatActivity { |
更多细节,可前往Android“退一步”的布局加载优化阅读~
延伸:VirtualView
VirtualView
是在天猫重运营的电商业务场景下,产生的一套方案,他可以通过编写xml,然后编译成二进制文件(体积小,解析快),下发到客户端渲染,具备动态能力。感兴趣可以看哈迪之前写的系列文章硬核的Virtualview。
哈迪在inflate
章节中猜测:Android中的xml的二进制解析是不是流式、指针移位的方式来操作?之所以这么想,是因为在VirtualView文件格式与模板编译这篇文章看到了类似操作,所以做出了这个猜测。
总结
不管是提效
篇还是性能优化
篇,我们可以看到,针对不同的业务场景和需求,来选择不同的实现方案。没有完美的技术,只有合不合适~
参考资料
- 简书 - inflate的过程分析
- CSDN - Android 探究 LayoutInflater setFactory
- GitHub - butterknife
- 掘金 - Jetpack笔记-DataBinding
- GitHub - X2C
- 掘金 - Android “退一步”的布局加载优化
- GitHub - Virtualview & 掘金 - 硬核的Virtualview