美文网首页
Android | xml和view的那些事

Android | xml和view的那些事

作者: 哈利迪ei | 来源:发表于2020-07-26 14:44 被阅读0次

    嗨,我是写博客满脑子骚东西的哈利迪~今天和大伙聊聊Android中的xml和view的那些事,首先会分析一下xml布局解析inflate的流程,然后会介绍一些业内的方案,如:

    提效篇:

    • JakeWharton:著名的Butterknife
    • Android自带:双向绑定的DataBinding、省去findViewById的ViewBindingkotlin扩展

    性能优化篇:

    • 掌阅:将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开始,

    //AppCompatActivity.java
    void setContentView(int layoutResID) {
        //交给代理类处理
        getDelegate().setContentView(layoutResID);
    }
    
    //AppCompatDelegateImpl.java
    void setContentView(int resId) {
        //默认指定父布局为content
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        //from通过系统服务得到服务对象
        //inflate解析布局,同时指定contentParent为父布局
        LayoutInflater.from(mContext).inflate(resId, contentParent);
    }
    

    可见,核心实现交给了LayoutInflater,跟进inflate方法,

    //LayoutInflater.java
    View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        //尝试通过预编译得到view,谷歌还在开发中的功能,先忽略
        View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
        if (view != null) {
            return view;
        }
        //获取XmlResourceParser
        XmlResourceParser parser = res.getLayout(resource);
        //开始解析
        return inflate(parser, root, attachToRoot);
    }
    

    XmlResourceParser是一个接口,实现了XmlPullParser(解析xml的布局结构)和 AttributeSet(解析xml标签属性)两个接口,我们先往下跟inflate

    //LayoutInflater.java
    //inflate方法有一段注释提到,解析所用的是经过预处理的xml二进制文件而非原始文件,这点后面分析
    View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        //得到xml里标签的属性
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        View result = root;
        //定位到view树的根节点
        advanceToRootNode(parser);
        final String name = parser.getName();
        if (TAG_MERGE.equals(name)) {//根节点是merge标签
            if (root == null || !attachToRoot) {
                //merge标签必须指定父布局,否则抛异常
                throw new InflateException("<merge /> can be used only with a valid "
                                           + "ViewGroup root and attachToRoot=true");
            }
            //解析
            rInflate(parser, root, inflaterContext, attrs, false);
        } else {
            //得到根视图
            final View temp = createViewFromTag(root, name, inflaterContext, attrs);
            ViewGroup.LayoutParams params = null;
            if (root != null) {
                //用传入的contentParent父布局生成参数给根视图
                params = root.generateLayoutParams(attrs);
                if (!attachToRoot) {
                    temp.setLayoutParams(params);
                }
            }
            //解析
            rInflateChildren(parser, temp, attrs, true);
            if (root != null && attachToRoot) {
                //传入的contentParent作为父布局
                root.addView(temp, params);
            }
            //没有传入父布局,就直接返回根视图
            if (root == null || !attachToRoot) {
                result = temp;
            }
        }
        return result;
    }
    

    继续跟进rInflateChildren

    //LayoutInflater.java
    void rInflateChildren(...){
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }
    
    //遍历、递归(比如LinearLayout里又有一个LinearLayout)
    void rInflate(XmlPullParser parser, View parent, Context context,
                  AttributeSet attrs, boolean finishInflate){
        //view树深度
        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            final String name = parser.getName();
            //根据不同的标签名进行各自的操作
            if (TAG_REQUEST_FOCUS.equals(name)) {//requestFocus
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {//tag
                //...
            } else if (TAG_MERGE.equals(name)) {//merge
                throw new InflateException("<merge /> must be the root element");
            } else {//重点关注
                //创建view
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                //递归
                rInflateChildren(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }
        //...
    }
    

    跟进createViewFromTag

    //LayoutInflater.java
    View createViewFromTag(View parent, String name, Context context, 
                           AttributeSet attrs,boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            //如果是小写的view,就取class属性的值作为名字
            name = attrs.getAttributeValue(null, "class");
        }
        //选择一个工厂来创建view,可以setFactory/setFactory2来自定义工厂,干预view的创建
        View view = tryCreateView(parent, name, context, attrs);
        if (view == null) {//工厂处理不了的view,就手动创建
            if (-1 == name.indexOf('.')) {
                //像<Button/>这样没有包名,则加上前缀android.view.
                //运行时,真正的实例是子类PhoneLayoutInflater,他会先在3个前缀里选一个:
                //android.widget.  android.webkit.  android.app.
                //如果3个前缀都找不到类,才交给父类使用前缀android.view.
                view = onCreateView(context, parent, name, attrs);
            } else {
                //已有包名
                view = createView(context, name, null, attrs);
            }
        }
        return view;
    }
    

    跟进createView

    //LayoutInflater.java
    //通过反射创建view
    View createView(Context viewContext, String name,String prefix, AttributeSet attrs){
        //从缓存中取出构造方法
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        Class<? extends View> clazz = null;
        //加载class
        clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                              mContext.getClassLoader()).asSubclass(View.class);
        //缓存里没有构造方法,则用clazz获取两个参数的构造方法
        if (constructor == null) {
            //Class<?>[] mConstructorSignature = new Class[] {Context.class, AttributeSet.class}
            constructor = clazz.getConstructor(mConstructorSignature);
        }
        //...
        //反射创建view
        View view = constructor.newInstance(args);
        return view;
    }
    

    以上就是常规流程,如果有设置工厂,则可以在tryCreateView中就把view给创建了。利用工厂可以做一些全局处理,比如一键切换皮肤、字体等,

    //LayoutInflater.java
    View tryCreateView(View parent, String name,Context context,AttributeSet attrs) {
        View view;
        //选择一个工厂来创建view,可以setFactory/setFactory2来自定义工厂,干预view的创建
        if (mFactory2 != null) {
            //用工厂创建view
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            //用工厂创建view
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }
        if (view == null && mPrivateFactory != null) {
            //用工厂创建view
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }
        return view;
    }
    

    整体流程图如下,

    image

    需要注意的是,目前系统的AppCompatActivity有帮我们设置一个默认工厂,

    AppCompatActivity#onCreate ->

    ​ delegate.installViewFactory();

    AppCompatDelegateImpl#installViewFactory ->

    ​ LayoutInflaterCompat.setFactory2(layoutInflater, this);

    AppCompatDelegateImpl中,

    AppCompatDelegateImpl#createView ->

    ​ return mAppCompatViewInflater.createView(...);

    AppCompatViewInflater中可见,我们常见的一些view都被转换成AppCompat的view了,他们的创建不需要走反射逻辑。

    //AppCompatViewInflater.java
    View createView(...) {
        switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageView":
                view = createImageView(context, attrs);
                verifyNotNull(view, name);
                break;
                //...
        }
        return view;
    }
    
    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
        //TextView被替换成AppCompatTextView
        return new AppCompatTextView(context, attrs);
    }
    

    native层

    那么java层的parser的具体实例是谁呢?跟进XmlResourceParser parser = res.getLayout(resource),最终发现是XmlBlock.Parser,我们试着跟下parsergetName方法,他的实现交给了native层的nativeGetName

    native源码基于Android 9.0

    native函数动态注册,android_util_XmlBlock.cpp

    //android_util_XmlBlock.cpp
    
    //需要动态注册的native函数数组
    static const JNINativeMethod gXmlBlockMethods[] = {
        { "nativeGetName","(J)I",(void*) android_content_XmlBlock_nativeGetName }
        //...
    }
    
    static jint android_content_XmlBlock_nativeGetName(JNIEnv* env, jobject clazz,
                                                       jlong token){
        ResXMLParser* st = reinterpret_cast<ResXMLParser*>(token);
        if (st == NULL) {
            return -1;
        }
        return static_cast<jint>(st->getElementNameID());
    }
    

    来看到ResXMLParsergetElementNameID方法,ResourceTypes.cpp

    //ResourceTypes.cpp
    int32_t ResXMLParser::getElementNameID() const{
        if (mEventCode == START_TAG) {//标签开始处,如<View>
            //dtohl是啥?todo1
            return dtohl(((const ResXMLTree_attrExt*)mCurExt)->name.index);
        }
        if (mEventCode == END_TAG) {//标签结束处,如</View>
            return dtohl(((const ResXMLTree_endElementExt*)mCurExt)->name.index);
        }
        return -1;
    }
    

    先看下ResXMLTree_attrExt是啥,在ResourceTypes.h

    //ResourceTypes.h
    //是一个结构体
    struct ResXMLTree_attrExt
    {
        //当前标签元素的命名空间
        struct ResStringPool_ref ns;
        //当前标签元素的名称,如"View",但并不是字符串类型,而是一个结构体,往下看
        struct ResStringPool_ref name;
        //...
    };
    
    //结构体,有个int字段,表示在字符串常量池中的索引
    struct ResStringPool_ref
    {
        //从ResStringPool_header(头部标识)之后开始索引,在该表中查找字符串在池子中的位置
        uint32_t index;
    };
    

    可见,xml被二进制处理时,会把多个相同的字符串压缩成一份存进常量池里,如:

    image

    根据位置index字段,就可以知道标签名字是啥了,常量池的处理可以减小xml体积,

    文章前边留了个todo1:dtohl是啥,谷歌一下dtohl,发现这些函数被定义在ByteOrder.h里,

    //ByteOrder.h
    //跟设备架构有关的字节序,对于我们今天使用的ARM CPU,就是小字节序(不太懂)
    #define DEVICE_BYTE_ORDER LITTLE_ENDIAN
    #if BYTE_ORDER == DEVICE_BYTE_ORDER  //不用进行字节转换,传x直接返回x
    #define dtohl(x)    (x)
    #define dtohs(x)    (x)
    #define htodl(x)    (x)
    #define htods(x)    (x)
    #else  //需要进行字节转换
    #define dtohl(x)    (android_swap_long(x))
    #define dtohs(x)    (android_swap_short(x))
    #define htodl(x)    (android_swap_long(x))
    #define htods(x)    (android_swap_short(x))
    #endif
    
    //转换操作,不太懂是拿来做啥的
    static inline uint32_t android_swap_long(uint32_t v)
    {
        return (v<<24) | ((v<<8)&0x00FF0000) | ((v>>8)&0x0000FF00) | (v>>24);
    }
    
    static inline uint16_t android_swap_short(uint16_t v)
    {
        return (v<<8) | (v>>8);
    }
    

    哈迪能力有限,只能跟到这里了。我们知道运行时解析的xml是经过预处理的二进制文件(apk打包时做的),那我们可以大胆猜测一下,运行时的解析是不是在做一些流式、指针移位之类的读操作?比如,把xml二进制文件进行各种分区,如文件头、标签区、属性区、字符串常量池区,然后解析时则用如readShort、readLong之类的方式进行指针移位,从而读出相应的view标签、view属性,有点类似JVM解析字节码的过程。(能力有限,仅做猜测)

    image

    小结

    1. 预编译tryInflatePrecompiled:谷歌正在做的事情,还没开放,敬请期待。
    2. xml文件的预处理:打包时将xml进行二进制编译,压缩xml体积、提升运行时的解析效率。(猜测:二进制的流式、指针移位操作,解析效率要比原始的xml高)

    Butterknife

    Butterknife在编译期通过Apt(注解处理器)处理注解,JavaPoet(辅助生成Java文件的工具)创建类,来省去findViewById、setOnclickListener这些繁琐的操作。哈迪使用时还是在大学的时候,工作后也没接触过了,现在这个项目的作者已经不再维护了,他推荐我们去使用ViewBinding,不过我们还是简单回顾下吧~

    引入依赖:

    implementation 'com.jakewharton:butterknife:10.2.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
    

    简单使用:

    class ButterknifeActivity extends AppCompatActivity {
        @BindView(R.id.tv_name)
        TextView mTextView;
        @OnClick(R.id.tv_name)
        void submit() {
            Toast.makeText(this, "click", Toast.LENGTH_SHORT).show();
        }
    
        void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_butterknife);
            //绑定
            mUnbinder = ButterKnife.bind(this);
            //直接访问
            mTextView.setText("butter knife");
        }
    }
    

    跟进bind方法,

    //ButterKnife.java
    static Unbinder bind(Activity target) {
        //获取DecorView
        View sourceView = target.getWindow().getDecorView();
        return bind(target, sourceView);
    }
    
    static Unbinder bind(Object target, View source) {
        Class<?> targetClass = target.getClass();
        //获取构造方法
        Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
        if (constructor == null) {
            return Unbinder.EMPTY;
        }
        //反射创建Unbinder的实例
        return constructor.newInstance(target, source);
    }
    

    跟进findBindingConstructorForClass

    static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
        //从缓存获取构造方法
        Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
        if (bindingCtor != null || BINDINGS.containsKey(cls)) {
            return bindingCtor;
        }
        String clsName = cls.getName();
        //不支持framework层的类
        if (clsName.startsWith("android.") || clsName.startsWith("java.")
            || clsName.startsWith("androidx.")) {
            return null;
        }
        //加载ButterknifeActivity_ViewBinding类
        Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
        //获取两个参数的构造方法
        bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
        //把构造方法存入缓存
        BINDINGS.put(cls, bindingCtor);
        return bindingCtor;
    }
    

    ButterknifeActivity_ViewBinding类是由Butterknife创建的,代码不多,

    class ButterknifeActivity_ViewBinding implements Unbinder {
        private ButterknifeActivity target;
        private View view7f0700b7;
    
        @UiThread
        public ButterknifeActivity_ViewBinding(final ButterknifeActivity target, View source) {
            this.target = target;
            View view;
            //source是DecorView,这里边就是简单的source.findViewById(id)
            view = Utils.findRequiredView(source, R.id.tv_name, "field 'mTextView' and method 'submit'");
            //强转,并赋值给ButterknifeActivity的mTextView
            //所以mTextView不能是private的,private意味着需要增加反射来实现,影响性能
            target.mTextView = Utils.castView(view, R.id.tv_name, "field 'mTextView'", TextView.class);
            view7f0700b7 = view;
            //设置点击事件
            view.setOnClickListener(new DebouncingOnClickListener() {
                @Override
                public void doClick(View p0) {
                    //调用ButterknifeActivity里的submit方法
                    target.submit();
                }
            });
        }
    
        public void unbind() {
            //解绑时的一些清理逻辑
            this.target = null;
            target.mTextView = null;
            view7f0700b7.setOnClickListener(null);
            view7f0700b7 = null;
        }
    }
    

    可见,Butterknife只有在创建Unbinder实例的时候用了反射,所以对运行时性能的影响是不大的。Apt处理注解和创建类的常规流程就不分析了哈~

    优势:

    1. 省去findViewById、setOnclickListener这些繁琐的操作
    2. 反射操作很少,对运行时性能影响不大

    缺点:

    1. apt创建类,增加io耗时,类编译耗时
    2. 类的增多,意味着包体积增大

    DataBinding/ViewBinding/kotlin扩展

    DataBinding

    DataBinding可以通过binding对象直接访问到xml布局里的有id控件,而且他还能实现数据和UI的双向绑定,即数据驱动UI刷新,UI操作修改数据,双向绑定不是本文重点,本文主要讨论xml和view的事儿~

    简单使用:

    // app/build.gradle里android{}加上开关
    dataBinding {
        enabled = true
    }
    

    xml布局转成data binding layout,也就是在布局外层包一层layout标签,然后多出一个data标签表示数据区,

    <layout xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools">
        <data>
        </data>
    
        <LinearLayout>
    
            <TextView
                      android:id="@+id/tv_name"/>
        </LinearLayout>
    </layout>
    

    在activity中,通过DataBindingUtil得到binding对象,

    class DBActivity extends AppCompatActivity {
    
        void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            //得到binding对象
            ActivityDBBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_d_b);
            //直接访问控件
            binding.tvName.setText("data binding");
        }
    }
    

    DataBinding是怎么做到的呢?也是通过生成额外的一些类来实现的,感兴趣可以看下哈迪之前写的笔记-DataBinding,我们直接看生成的类app/build/generated/data_binding_base_class_source_out/debug/out/com/holiday/srccodestudy/databinding/ActivityDBBinding.java

    abstract class ActivityDBBinding extends ViewDataBinding {
        //public的TextView,可以直接访问
        public final TextView tvName;
    
        protected ActivityDBBinding(Object _bindingComponent, View _root, 
                                    int _localFieldCount,TextView tvName) {
            super(_bindingComponent, _root, _localFieldCount);
            this.tvName = tvName;
        }
    
        //省略一些inflate、bind方法
    }
    

    ViewBinding

    ViewBinding省去了双向绑定的逻辑,比DataBinding更轻量,用法差不多,不过需要Android studio 3.6开始才能使用,

    // app/build.gradle里android{}加上开关
    viewBinding {
        enabled = true
    }
    

    打开开关后,默认会给所有布局生成java类,不像DataBinding需要包上一层layout标签。如果个别布局不需要开启ViewBinding,可以给布局的根标签加上tools:viewBindingIgnore="true"

    在activity中使用,有点不同,

    class VBActivity extends AppCompatActivity {
    
        void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            //先inflate
            ActivityVBBinding binding = ActivityVBBinding.inflate(LayoutInflater.from(this));
            //getRoot获取rootView
            setContentView(binding.getRoot());
            //直接访问控件
            binding.tvVb.setText("view binding");
        }
    }
    

    ViewBinding的具体实现暂不关注,直接看他的生成类app/build/generated/data_binding_base_class_source_out/debug/out/com/holiday/srccodestudy/databinding/ActivityVBBinding.java,路径跟DataBinding一样的,

    final class ActivityVBBinding implements ViewBinding {
        private final LinearLayout rootView;
        //public的TextView,可以直接访问
        public final TextView tvVb;
    
        private ActivityVBBinding(LinearLayout rootView, TextView tvVb) {
            this.rootView = rootView;
            this.tvVb = tvVb;
        }
    
        //比DataBinding多出一个getRoot方法
        public LinearLayout getRoot() {
            return rootView;
        }
    
        //省略一些inflate、bind方法
    }
    

    ViewBinding省去了DataBinding双向绑定功能(不用处理DataBinding的注解、表达式等),更专注于解决findViewById的问题,所以更轻量,编译更快。

    kotlin扩展

    如果项目有使用kotlin,还可以使用kotlin的扩展插件来免去findViewById操作。

    使用kotlin扩展插件,

    // app/build.gradle
    apply plugin: 'kotlin-android-extensions'
    

    在activity中使用,

    class KotlinActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_kotlin)
            //直接访问控件
            tv_kotlin.text = "kotlin 扩展插件"
        }
    }
    

    使用kotlin扩展插件有个明显的问题,就是控件的“裸奔”问题,比如我在activity中输入tv,就会把其他页面的控件也提示出来,

    image

    如果不小心导入了别的页面才有的控件,编译期没问题,运行的时候就才抛异常。也就是说,使用kotlin扩展插件,所有控件都处于不安全的裸奔状态。

    使用AS反编译一下KotlinActivity,Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile

    final class KotlinActivity extends AppCompatActivity {
        private HashMap _$_findViewCache;//控件缓存
    
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            this.setContentView(1300004);
            //查找TextView
            TextView var10000 = (TextView)this._$_findCachedViewById(id.tv_kotlin);
            //运行时,合法性检测,如果导了别的页面的控件进来,会抛xxx must not be null异常
            Intrinsics.checkExpressionValueIsNotNull(var10000, "tv_kotlin");
            //使用控件
            var10000.setText((CharSequence)"kotlin 扩展插件");
        }
    
        public View _$_findCachedViewById(int var1) {
            if (this._$_findViewCache == null) {
                this._$_findViewCache = new HashMap();
            }
            //在缓存中找控件
            View var2 = (View)this._$_findViewCache.get(var1);
            if (var2 == null) {
                //第一次找不到,走findViewById(如果导了别的页面的控件进来,就只能返回null了)
                var2 = this.findViewById(var1);
                this._$_findViewCache.put(var1, var2);
            }
            return var2;
        }
    }
    

    至于kotlin如何插入这些代码的,能力有限,哈迪也不知道,有了解的朋友评论区聊起来~

    小结

    如果不做数据和UI的双向绑定,只是为了避免findViewById,优先使用更轻量的ViewBinding,否则使用DataBindingDataBindingViewBinding在避免了findViewById繁琐工作的同时,还确保了空安全类型安全,即不会出现findViewById得到null、view cast exception的问题。当然,这两种方式也是避免不了生成类的编译耗时和包体积增大的问题的,得结合具体场景来使用。至于kotlin扩展,存在控件裸奔问题,不太推荐。

    至此,提效篇就介绍到这里了,下面让我们开始性能优化篇~

    x2c

    x2c是使用Apt+JavaPoet技术,在编译期将xml布局转成view类,免去了运行时解析xml的耗时。

    引入依赖:

    annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'
    implementation 'com.zhangyue.we:x2c-lib:1.0.6'
    

    简单使用:

    //给布局文件声明一个注解
    @Xml(layouts = "layout_x2c_test")
    class X2CActivity extends AppCompatActivity {
    
        void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_x2c);
            //通过X2C来获取view
            View view = X2C.inflate(this, R.layout.layout_x2c_test, null);
        }
    }
    

    跟进X2C.inflate

    //X2C.java
    
    static View inflate(Context context, int layoutId, ViewGroup parent) {
        return inflate(context, layoutId, parent, true);
    }
    
    //加载xml文件,检测如果有对应的java类,使用java类,否则使用LayoutInflater
    static View inflate(Context context, int layoutId, ViewGroup parent, boolean attach) {
        //获取xml布局对应的java类
        View view = getView(context, layoutId);
        if (view != null) {
            if (parent != null) {
                parent.addView(view);
            }
            return view;
        } else {
            //没有java类,走LayoutInflater解析逻辑
            return LayoutInflater.from(context).inflate(layoutId, parent, attach);
        }
    }
    
    static View getView(Context context, int layoutId) {
        //获取缓存好的view创建器
        IViewCreator creator = sSparseArray.get(layoutId);
        if (creator == null) {
            //没有,则生成一个
            int group = generateGroupId(layoutId);
            String layoutName = context.getResources().getResourceName(layoutId);
            layoutName = layoutName.substring(layoutName.lastIndexOf("/") + 1);
            String clzName = "com.zhangyue.we.x2c.X2C" + group + "_" + layoutName;
            //加载Apt生成的X2C0_layout_x2c_test这个类,反射获得view创建器
            creator = (IViewCreator) context.getClassLoader().loadClass(clzName).newInstance();
            //缓存起来
            sSparseArray.put(layoutId, creator);
        }
        //使用view创建器来创建view
        return creator.createView(context);
    }
    

    来看到view创建器X2C0_layout_x2c_test

    class X2C0_layout_x2c_test implements IViewCreator {
    
        View createView(Context context) {
            return new com.zhangyue.we.x2c.layouts.X2C0_Layout_X2c_Test().createView(context);
        }
    }
    

    跟进X2C0_Layout_X2c_Test可见,xml的标签和属性,都被解析成了java类的相应设置,

    class X2C0_Layout_X2c_Test implements IViewCreator {
    
        View createView(Context ctx) {
            Resources res = ctx.getResources();
            LinearLayout linearLayout0 = new LinearLayout(ctx);
            linearLayout0.setGravity(Gravity.CENTER_HORIZONTAL);
            linearLayout0.setOrientation(LinearLayout.VERTICAL);
            //...
            TextView textView2 = new TextView(ctx);
            LinearLayout.LayoutParams layoutParam2 = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
            textView2.setId(R.id.tv);
            textView2.setText("文案内容");
            textView2.setLayoutParams(layoutParam2);
            linearLayout0.addView(textView2);
            return linearLayout0;
        }
    }
    

    优势:

    1. 将xml解析提前到编译期,免去了运行时解析的耗时和内存
    2. 只在获取view创建器时用了反射,对运行时性能影响不大

    缺点:

    1. apt创建类,增加io耗时,类编译耗时
    2. 类的增多,意味着包体积增大

    所以,通常只在个别复杂度较高,有性能瓶颈的页面才会使用。

    ViewOpt

    鸿洋大佬的方案,是从避免反射创建view的角度去做优化的,即使用自定义工厂Factory来创建view,绕开反射逻辑。核心流程就是,先通过merge.xml来收集xml中用到的view集合,然后Apt生成一个类来处理集合,然后干预默认工厂Factory来插入自己的view创建逻辑。

    image
    class BaseActivity extends AppCompatActivity {
    
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            //插入自己的逻辑,将view的创建交给代理类
            View view = ViewOpt.createView(name, context, attrs);
            if (view != null) {
                return view;
            }
            //走默认工厂
            return super.onCreateView(parent, name, context, attrs);
        }
    }
    

    更多细节,可前往Android“退一步”的布局加载优化阅读~

    延伸:VirtualView

    VirtualView是在天猫重运营的电商业务场景下,产生的一套方案,他可以通过编写xml,然后编译成二进制文件(体积小,解析快),下发到客户端渲染,具备动态能力。感兴趣可以看哈迪之前写的系列文章硬核的Virtualview

    哈迪在inflate章节中猜测:Android中的xml的二进制解析是不是流式、指针移位的方式来操作?之所以这么想,是因为在VirtualView文件格式与模板编译这篇文章看到了类似操作,所以做出了这个猜测。

    总结

    不管是提效篇还是性能优化篇,我们可以看到,针对不同的业务场景和需求,来选择不同的实现方案。没有完美的技术,只有合不合适~

    参考资料


    image

    相关文章

      网友评论

          本文标题:Android | xml和view的那些事

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