摘要:
最近,笔者在研究Activity布局加载时,发现自己XML文件里的部分View在实际运行时被转换成了其他View,举个简单的例子,如下图所示:
hierarchy_viewer.png笔者使用 Hierarchy Viewer 工具分析视图时发现:原activity_main.xml里的TextView被转换成了AppCompatTextView。
笔者以此为问题起点,引出本篇文章的主要内容:
- 源码是怎样实现视图转换的?
- 这种视图转换有什么实际应用场景?
注: 本文源码分析使用的Android版本为 API 25 (Android 7.1)
正文:
一、源码是怎样实现视图转化的?
为帮助读者理解,笔者先简要概括一下源码实现视图转换的原理,再去详细分析源码。
在Android源码中有一个用作视图加工处理的工厂,我们可以称之为视图工厂。这个视图工厂和Android系统提供的“Inflater”做了绑定,我们从XML文件中加载的每一个View都会被这个视图工厂处理。
源码分析开始:
首先从Activity的onCreate()
方法的第一行代码super.onCreate()
方法开始分析,如下图所示:
首先,AppCompatActivity(MainActivity的父类)的onCreate()
的方法会被先调用,该方法的具体实现如下图所示:
对应视图转换功能,这里只分析getDelegate()
和delegate.installViewFactory()
方法。
1. getDelegate()
介绍这个方法的原因在于这个方法使用很频繁,我们后续会经常用到。
getDelegate()
用于获取AppCompatDelegate的实例,当没有实例时会先去创建,如下图所示:
注:AppCompatDelegate是一个抽象类,它有多个版本的实现类,用于兼容各个版本执行代理操作。
AppCompatDelegate.create()
方法会根据手机或模拟器的Android版本创建不同的实例,创建过程如下图所示:
可以看到笔者sdk 25版本创建的出的实例是AppCompatDelegateImplN对象。
补充:
delegate.png
这些不同版本的实现类之间是有继承关系的,用来保证高版本兼容低版本的各种方法。
2. delegate.installViewFactory()
从方法名可以看出这个方法的作用:安装视图工厂。
installViewFactory()
方法的实现如下图所示:
- 可以看到这是 AppCompatDelegateImplV9 类中的一个方法。
- 可以看到抽象类LayoutInflater的具体实现类是 PhoneLayoutInflate。
-
LayoutInflaterCompat.setFactory()
是整个方法的 核心。
接下来,我们看一下LayoutInflaterCompat.setFactory()
方法(如下图所示):
稍微解释一下,这个静态方法实际上就是把我们传入的的inflater对象和factory对象做了一个绑定,以后通过这个inflater加载进来的视图都会被factory加工处理。
那么这个LayoutInflaterFactory是什么呢?(参看下图)
layout_inflater_factory.png- 原来LayoutInflaterFactory是一个接口,里面有个抽象方法onCreateView(),从注释可以知道这个方法是用来解析XML文件的标签名重新创建View的。
接下来,我们回到AppCompatDelegateImplV9类,看一下LayoutInflaterFactory的具体实现:
layout_inflater_factory2.png我们找到了LayoutInflaterFactory类抽象方法onCreateView()
的具体实现(上图红色圈选)。
之后,继续跟进上图1087行代码createView(parent, name, context, attrs)
方法,如下图所示:
最后,跟进上图1029行代码AppCompatViewInlater类的createView()
方法,如下图所示:
TextView是怎样被转成AppCompatTextView的?看到这里基本上就真相大白了。
而且,不止TextView,其他系统控件也做了兼容转换。
最后,再回到LayoutInflater.setFactory()
这个核心方法说一个没有提及的问题,如下图所示:
问题:我们通过setFactory(layoutInflater, this)
方法只是把inflater和factory做了个绑定,那onCreateView()
这个方法是在哪?何时?被谁调用的呢?
其实,这个问题对于有经验的老司机来讲,大方向是能确定的:“一定是inflater在inflate视图的时候掉用了这个方法”。
在说明这个问题前,需要再说一下setFactory()
的一个绑定细节:
setFactory(inflater, factory)
在绑定视图工厂的过程中,会把这个“factory”赋值给inflater的mFactory属性和mFactory2属性,如下图(324行代码)所示:
剩下的就是“inflate的流程”了,整个流程如下图所示:
inflate.png最后,可以看到inflater是通过mFactory2属性调用了“视图工厂”的onCreateView()
方法。
至此,整个流程就缕通了。
二、这种视图转换有什么实际应用场景?
讲使用场景前,先回顾下installViewFactory()
这个方法,如下图所示:
可以看到LayoutInflaterCompat.setFactory()
这个方法在执行前进行了一个 if 判断,如果layoutInflater之前没有设置过factory才会执行绑定操作,如果绑定过只会简单的打印个Log日志。这意味着我们可以在installViewFactory()
方法调用之前安装自己的视图工厂。
场景一:
假如出现这样一种需求,某Android程序员自定义了一个功能很强的TextView - SuperTextView,想要进行全局替换(把所有Activity里每个用到TextView的地方全部替换成SuperTextView),用正常程序员的思维一般是跑到每个XML文件里一个一个查找替换。看完源码的视图转换机制我们可以给出另一种做法:
为Inflater安装自定义视图工厂,如下图所示:
super_text_view.png视图转换效果如下:
view_change.png补充下笔者代码的两个注意点:
- 笔者代码中使用的
new SuperTextView(context, attrs)
两个参数的构造方法,其中第二个参数attrs(属性集)是从XML文件里解析出来的,这意味着“转换后的View”要继承自XML中“被转换的View”(要保证属性兼容)- 笔者这种代码写法最低兼容至API 11(Android 3.1)。
场景二:
统一的字体替换需求,如果产品给了我们一种新的字体格式,需要我们进行全局替换。
方案一:可以用自定义TextView实现,使用场景一的方法进行替换。
方案二:继续使用原生控件,额外设置属性,代码和效果图如下:
change_ttf.png注:
比较细心的朋友会发现我们的标题栏字体“My Application”字体没有发生改变,不是说这种设置是全局的吗?
答:笔者有个地方一直没有点透,文章从开始到现在一直在围绕一个点展开,那就是“inflater”和“factory”,也就是只有在XML解析的时候才会用到inflater和factory,如果在代码中new一个View是不会走inflate流程的,也不会有视图加工的过程。而Toolbar的标题是在代码中new出来的,如下图所示:
toolbar_source.png
最后,笔者插入一点关于inflater的剧情,有兴趣的朋友可以看下:
inflater剧情:
我们常常使用
inflater.pngLayoutInflater.from(Context context)
获取inflater的实例,但实际上这个inflater是通过获取系统服务的方式获取的(如下图所示:)稍微研究过
getSystemService()
源码的朋友可能都会认为,通过获取系统服务的方式获取的实例对象一定是单例。但笔者在这里强调的一下inflater不是,因为我们通常情况下获取到的inflater是“系统服务生成的inflater”的克隆体。简单看一下原因:
我们调用的
inflater2.pngLayoutInflater.from(Context context)
方法最终会走到ContextThemeWrap类的getSystemService()
方法中,如下图所示:
当我们第一次掉用这个方法,因为当前mInflater还没有实例引用,会继续调用LayoutInflater.from(Context context)
方法并传入另一个Context对象继续获取inflater的实例,之后调用cloneInContext(Context context)
进行克隆。继续Debug跟进上图167行代码
inflater3.pngLayoutInflater.from(getBaseContext())
方法,如下图所示:
- 可以看到我们传入的getBaseContext()是一个 ContextImpl 类的实例。
- 可以看到获取和最后返回的LayoutInflater实例是 PhoneLayoutInflater@4553。
接下来,会调用
inflater4.pngcloneInContext(Context context)
这个克隆方法,如下图所示:原来所谓的克隆,就是根据传入的context参数又new了一个PhoneLayoutInflater对象。
接下来,我们看下最终生成的inflater实例,如下图所示:
inflater5.png
- 可以看到mInflater最后引用的实例对象是PhoneLayoutInflater@4564,而前一步通过服务获取的实例对象是PhoneLayoutInflater@4553,这显然不是同一个对象。
- 还可以注意到最后生成的PhoneLayoutInflater@4564对象的mFactory属性值为null,也就是说此时的inflater还没有绑定LayoutInflaterFactory接口对象。
最后,笔者用一个简单的实验图做一个总结:
inflater6.png在我们的应用程序中一般系统会生成两个inflater的实例:一个是通过ContextImpl从服务中获取的实例对象;另一个是我们通过"克隆方法"new出来的实例对象。我们"克隆"出来的inflater对象安装了视图工厂(Factory),原始服务中的却没有。
题外话:
分析完视图转换的过程后,发现整个过程是其实是和inflater息息相关的,也就是说受影响的是XML的解析过程,我们在代码里创建视图(比如new TextView)时,是不会有影响的(最后还是TextView)。
这让我想起一件事,和我一起工作的同事喜欢纯代码写布局,不喜欢用XML(应该是IOS写习惯了ㄟ( ▔, ▔ )ㄏ),当时感觉这同事太非主流了,就说道了他两句。
他是这样怼我的“写XML最后不是还会转成代码吗?用代码写布局和XML写布局有什么区别吗?”
当时的我:“......”
不说了,马上开怼,怼回来~O(∩_∩)O~
网友评论