Android进阶 - 源码中的视图转换

作者: 梦想编织者灬小楠 | 来源:发表于2017-08-29 15:05 被阅读683次
    android_7.1.png

    摘要:

    最近,笔者在研究Activity布局加载时,发现自己XML文件里的部分View在实际运行时被转换成了其他View,举个简单的例子,如下图所示:

    hierarchy_viewer.png

    笔者使用 Hierarchy Viewer 工具分析视图时发现:原activity_main.xml里的TextView被转换成了AppCompatTextView

    笔者以此为问题起点,引出本篇文章的主要内容:

    1. 源码是怎样实现视图转换的?
    2. 这种视图转换有什么实际应用场景?

    注: 本文源码分析使用的Android版本为 API 25 (Android 7.1)

    正文:

    一、源码是怎样实现视图转化的?

    为帮助读者理解,笔者先简要概括一下源码实现视图转换的原理,再去详细分析源码。

    在Android源码中有一个用作视图加工处理的工厂,我们可以称之为视图工厂。这个视图工厂和Android系统提供的“Inflater”做了绑定,我们从XML文件中加载的每一个View都会被这个视图工厂处理。

    源码分析开始:

    首先从Activity的onCreate()方法的第一行代码super.onCreate()方法开始分析,如下图所示:

    main_activity.png

    首先,AppCompatActivity(MainActivity的父类)的onCreate()的方法会被先调用,该方法的具体实现如下图所示:

    appcompat_activity.png

    对应视图转换功能,这里只分析getDelegate()delegate.installViewFactory()方法。

    1. getDelegate()

    介绍这个方法的原因在于这个方法使用很频繁,我们后续会经常用到。

    getDelegate()用于获取AppCompatDelegate的实例,当没有实例时会先去创建,如下图所示:

    get_delegate.png

    注:AppCompatDelegate是一个抽象类,它有多个版本的实现类,用于兼容各个版本执行代理操作。

    AppCompatDelegate.create()方法会根据手机或模拟器的Android版本创建不同的实例,创建过程如下图所示:

    appcompat_delegate.png

    可以看到笔者sdk 25版本创建的出的实例是AppCompatDelegateImplN对象

    补充:
    这些不同版本的实现类之间是有继承关系的,用来保证高版本兼容低版本的各种方法。

    delegate.png

    2. delegate.installViewFactory()

    从方法名可以看出这个方法的作用:安装视图工厂

    installViewFactory()方法的实现如下图所示:

    view_factory.png
    • 可以看到这是 AppCompatDelegateImplV9 类中的一个方法。
    • 可以看到抽象类LayoutInflater的具体实现类是 PhoneLayoutInflate
    • LayoutInflaterCompat.setFactory()是整个方法的 核心

    接下来,我们看一下LayoutInflaterCompat.setFactory()方法(如下图所示):

    view_factory3.png

    稍微解释一下,这个静态方法实际上就是把我们传入的的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)方法,如下图所示:

    layout_inflater_factory2_2.png

    最后,跟进上图1029行代码AppCompatViewInlater类的createView()方法,如下图所示:

    layout_inflater_factory3.png

    TextView是怎样被转成AppCompatTextView的?看到这里基本上就真相大白了。

    而且,不止TextView,其他系统控件也做了兼容转换

    最后,再回到LayoutInflater.setFactory()这个核心方法说一个没有提及的问题,如下图所示:

    view_factory_core.png

    问题:我们通过setFactory(layoutInflater, this)方法只是把inflaterfactory做了个绑定,那onCreateView()这个方法是在哪?何时?被谁调用的呢?

    其实,这个问题对于有经验的老司机来讲,大方向是能确定的:“一定是inflater在inflate视图的时候掉用了这个方法”。

    在说明这个问题前,需要再说一下setFactory()的一个绑定细节:

    setFactory(inflater, factory)在绑定视图工厂的过程中,会把这个“factory”赋值给inflater的mFactory属性和mFactory2属性,如下图(324行代码)所示:

    inflater_result.png

    剩下的就是“inflate的流程”了,整个流程如下图所示:

    inflate.png

    最后,可以看到inflater是通过mFactory2属性调用了“视图工厂”的onCreateView()方法。

    至此,整个流程就缕通了。

    二、这种视图转换有什么实际应用场景?

    讲使用场景前,先回顾下installViewFactory()这个方法,如下图所示:

    view_factory.png

    可以看到LayoutInflaterCompat.setFactory()这个方法在执行前进行了一个 if 判断,如果layoutInflater之前没有设置过factory才会执行绑定操作,如果绑定过只会简单的打印个Log日志。这意味着我们可以在installViewFactory()方法调用之前安装自己的视图工厂

    场景一:

    假如出现这样一种需求,某Android程序员自定义了一个功能很强的TextView - SuperTextView,想要进行全局替换(把所有Activity里每个用到TextView的地方全部替换成SuperTextView),用正常程序员的思维一般是跑到每个XML文件里一个一个查找替换。看完源码的视图转换机制我们可以给出另一种做法:

    为Inflater安装自定义视图工厂,如下图所示:

    super_text_view.png

    视图转换效果如下:

    view_change.png

    补充下笔者代码的两个注意点:

    1. 笔者代码中使用的new SuperTextView(context, attrs)两个参数的构造方法,其中第二个参数attrs(属性集)是从XML文件里解析出来的,这意味着“转换后的View”要继承自XML中“被转换的View”(要保证属性兼容)
    2. 笔者这种代码写法最低兼容至API 11(Android 3.1)

    场景二:

    统一的字体替换需求,如果产品给了我们一种新的字体格式,需要我们进行全局替换。

    方案一:可以用自定义TextView实现,使用场景一的方法进行替换。

    方案二:继续使用原生控件,额外设置属性,代码和效果图如下:

    change_ttf.png

    注:

    比较细心的朋友会发现我们的标题栏字体“My Application”字体没有发生改变,不是说这种设置是全局的吗?

    答:笔者有个地方一直没有点透,文章从开始到现在一直在围绕一个点展开,那就是“inflater”“factory”,也就是只有在XML解析的时候才会用到inflaterfactory如果在代码中new一个View是不会走inflate流程的,也不会有视图加工的过程。而Toolbar的标题是在代码中new出来的,如下图所示:

    toolbar_source.png

    最后,笔者插入一点关于inflater的剧情,有兴趣的朋友可以看下:

    inflater剧情:

    我们常常使用LayoutInflater.from(Context context)获取inflater的实例,但实际上这个inflater是通过获取系统服务的方式获取的(如下图所示:)

    inflater.png

    稍微研究过getSystemService()源码的朋友可能都会认为,通过获取系统服务的方式获取的实例对象一定是单例。但笔者在这里强调的一下inflater不是,因为我们通常情况下获取到的inflater是“系统服务生成的inflater”的克隆体

    简单看一下原因:

    我们调用的LayoutInflater.from(Context context)方法最终会走到ContextThemeWrap类的getSystemService()方法中,如下图所示:

    inflater2.png
    当我们第一次掉用这个方法,因为当前mInflater还没有实例引用,会继续调用LayoutInflater.from(Context context)方法并传入另一个Context对象继续获取inflater的实例,之后调用cloneInContext(Context context)进行克隆

    继续Debug跟进上图167行代码LayoutInflater.from(getBaseContext())方法,如下图所示:

    inflater3.png
    • 可以看到我们传入的getBaseContext()是一个 ContextImpl 类的实例。
    • 可以看到获取和最后返回的LayoutInflater实例是 PhoneLayoutInflater@4553

    接下来,会调用cloneInContext(Context context)这个克隆方法,如下图所示:

    inflater4.png

    原来所谓的克隆,就是根据传入的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~

    相关文章

      网友评论

      本文标题:Android进阶 - 源码中的视图转换

      本文链接:https://www.haomeiwen.com/subject/zeilhxtx.html