• 源码解析:解析掌阅X2C 框架


    前言

    掌阅出品了X2C 框架,听说可以加快性能。喜欢研究源码的我,肯定要来看下是怎么回事。
    作为一个开发,应该不屑于只会使用开源框架。

    OK,来尝试下。

    项目地址:

    https://github.com/TomasYu/X2C
    

    原理分析:

    X2C 是把Xml 文件,翻译成Java文件,减少系统利用LayoutInflate 去解析xml 的过程。
    有两个技术要点:

    1. 什么时候解析xml?
    2. 怎么生成Java 文件?

    对于什么时候解析XML

    关键在于下面这行:

            annotationProcessor project(':x2c-apt')
            implementation project(':x2c-lib')
    

    这里指定了annotationProcessor ,也就是注解编译处理器。不了解的同学可以百度下java APT 技术。
    javac 编译的时候,会调用指定的这个处理器,并把注解都传给你。这时候你就可以做一些事情了。
    比如:解析xml。 对应的X2C 的代码如下:

    @SupportedSourceVersion(SourceVersion.RELEASE_7)
    @SupportedAnnotationTypes("com.zhangyue.we.x2c.ano.Xml")
    public class XmlProcessor extends AbstractProcessor {
    
        private int mGroupId = 0;
        private LayoutManager mLayoutMgr;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            Log.init(processingEnvironment.getMessager());
            mLayoutMgr = LayoutManager.instance();
            mLayoutMgr.setFiler(processingEnvironment.getFiler());
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Xml.class);
            TreeSet<String> layouts = new TreeSet<>();
            for (Element element : elements) {
                Xml xml = element.getAnnotation(Xml.class);
                String[] names = xml.layouts();
                for (String name : names) {
                    layouts.add(name.substring(name.lastIndexOf(".") + 1));
                }
            }
    
            for (String name : layouts) {
                if (mGroupId == 0 && mLayoutMgr.getLayoutId(name) != null) {
                    mGroupId = (mLayoutMgr.getLayoutId(name) >> 24);
                }
                Log.i("xinyu:"+ mGroupId);
                mLayoutMgr.setGroupId(mGroupId);
                mLayoutMgr.translate(name);
            }
    
            mLayoutMgr.printTranslate();
            return false;
        }
    
    
    }
    
    有一个小问题,他怎么知道我的XML文件在哪里?

    查看源码之后,可以发现有一个方法:

        private HashMap<String, ArrayList<File>> scanLayouts(File root) {
            return new FileFilter(root)
                    .include("layout")
                    .include("layout-land")
                    .include("layout-v28")
                    .include("layout-v27")
                    .include("layout-v26")
                    .include("layout-v25")
                    .include("layout-v24")
                    .include("layout-v23")
                    .include("layout-v22")
                    .include("layout-v21")
                    .include("layout-v20")
                    .include("layout-v19")
                    .include("layout-v18")
                    .include("layout-v17")
                    .include("layout-v16")
                    .include("layout-v15")
                    .include("layout-v14")
                    .exclude("build")
                    .exclude("java")
                    .exclude("libs")
                    .exclude("mipmap")
                    .exclude("values")
                    .exclude("drawable")
                    .exclude("anim")
                    .exclude("color")
                    .exclude("menu")
                    .exclude("raw")
                    .exclude("xml")
                    .filter();
        }
    

    这个方法会去扫描你项目的res/layout 等一系列文件。

    找到文件之后,怎么解析呢?

    具体的解析代码在:com.zhangyue.we.view.View#translate(java.lang.StringBuilder, java.lang.String, java.lang.String) 这个方法。

        @Override
        public boolean translate(StringBuilder stringBuilder, String key, String value) {
            switch (key) {
                case "android:textSize":
                    return setTextSize(stringBuilder, value);
    
       private boolean setTextSize(StringBuilder stringBuilder, String value) {
            String unit;
            String dim;
            if (value.startsWith("@")) {
                unit = "TypedValue.COMPLEX_UNIT_PX";
                dim = String.format("(int)res.getDimension(R.dimen.%s)", value.substring(value.indexOf("/") + 1));
            } else {
                if (value.endsWith("dp") || value.endsWith("dip")) {
                    unit = "TypedValue.COMPLEX_UNIT_DIP";
                    dim = value.substring(0, value.indexOf("d"));
                } else if (value.endsWith("sp")) {
                    unit = "TypedValue.COMPLEX_UNIT_SP";
                    dim = value.substring(0, value.indexOf("s"));
                } else {
                    unit = "TypedValue.COMPLEX_UNIT_PX";
                    dim = value.substring(0, value.indexOf("p"));
                }
            }
            stringBuilder.append(String.format("%s.setTextSize(%s,%s);
    ", getObjName(), unit, dim));
            mImports.add("android.util.TypedValue");
            return true;
        }
    

    其实就是拼接字符串。字符串里面就是Java 代码。

    解析完,写入文件:

    这里用到了javapoet 技术,不知道的可以百度下,是一个java 库,用它可以生成java 源代码。

    public class LayoutWriter {
        private Filer mFiler;
        private String mName;
        private String mMethodSpec;
        private String mPkgName;
        private String mLayoutCategory;
        private String mLayoutName;
        private TreeSet<String> mImports;
    
        public LayoutWriter(String methodSpec, Filer filer, String javaName
                , String pkgName
                , String layoutSort
                , String layoutName
                , TreeSet<String> imports) {
            this.mMethodSpec = methodSpec;
            this.mFiler = filer;
            this.mName = javaName;
            this.mPkgName = pkgName;
            this.mLayoutCategory = layoutSort;
            this.mLayoutName = layoutName;
            this.mImports = imports;
        }
    
        public String write() {
    
            MethodSpec methodSpec = MethodSpec.methodBuilder("createView")
                    .addParameter(ClassName.get("android.content", "Context"), "ctx")
                    .addStatement(mMethodSpec)
                    .returns(ClassName.get("android.view", "View"))
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .build();
    
            TypeSpec typeSpec = TypeSpec.classBuilder(mName)
                    .addMethod(methodSpec)
                    .addSuperinterface(ClassName.get("com.zhangyue.we.x2c", "IViewCreator"))
                    .addModifiers(Modifier.PUBLIC)
                    .addJavadoc(String.format("WARN!!! dont edit this file
    translate from {@link  %s.R.layout.%s}" +
                            "
    autho chengwei 
    email chengwei@zhangyue.com
    ", mPkgName, mLayoutName))
                    .build();
    
            String pkgName = "com.zhangyue.we.x2c.layouts";
            if (mLayoutCategory != null && mLayoutCategory.length() > 0) {
                pkgName += ("." + mLayoutCategory);
            }
            JavaFile javaFile = JavaFile.builder(pkgName, typeSpec)
                    .addImports(mImports)
                    .build();
            try {
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return pkgName + "." + mName;
        }
    }
    

    MethodSpec 表示一个方法,addParameter 表示增加一个方法参数。javaFile.writeTo(mFiler); 就会把创建的Java 类写入文件,具体使用大家自己百度学习下吧。

    那程序为什么用的是生成的java 文件?而不是xml?

    我们写代码的时候,写的

            X2C.setContentView(this, R.layout.activity_main_inter);
    
    

    就会执行下面的代码,x2c 的getView 会去拿生成的Java 文件,然后创建View.

        public static void setContentView(Activity activity, int layoutId) {
            if (activity == null) {
                throw new IllegalArgumentException("Activity must not be null");
            }
            View view = getView(activity, layoutId);
            if (view != null) {
                activity.setContentView(view);
            } else {
                activity.setContentView(layoutId);
            }
        }
    
    
        public static View getView(Context context, int layoutId) {
            IViewCreator creator = sSparseArray.get(layoutId);
            if (creator == null) {
                try {
                    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;
                    creator = (IViewCreator) context.getClassLoader().loadClass(clzName).newInstance();
                } catch (Exception e) {
                    e.printStackTrace();
                }
    
                //如果creator为空,放一个默认进去,防止每次都调用反射方法耗时
                if (creator == null) {
                    creator = new DefaultCreator();
                }
                sSparseArray.put(layoutId, creator);
            }
            return creator.createView(context);
        }
    

    OK。到这里整个流程就走通了。

    但是X2C 有BUG,有用户反馈:

    SeekBar的MaxHeight和MinHeight属性,用X2C翻译成Java代码为:seekBar.setMaxHeight()和seekBar.setMinHeigh(),在seekBar源码中也没有这两个方法的. 编译报错。

    这个BUG原作者可能没有仔细看,我给解决了。主要是View 属性翻译的时候,方法写错了方法名字。大家可以看下这个 https://github.com/TomasYu/X2C Git log提交 就知道了。

    总结:
    个人觉得,X2C ,思路很不错,而且作者对开源项目都熟知,了解市场上常用的框架。
    但是,X2C 的局限性太大了,很多View 的属性,作者都没有写进去。比如:SeekBar的一些属性,
    如:progress 这个属性你设置了默认是20的话,发现没有效果。没错,X2C 没有处理。它就处理了常见的
    几个属性,但是并不能满足很多情况,很多常用控件的属性都没有做处理。个人还是对这个缺陷比较在意的。
    android 的View 很多,TextView,EditText,这些都没有完全支持。
    但是,作者的思路,创新,以及对新技术的学习之后使用,还是很值得我们肯定和学习的。

  • 相关阅读:
    replace函数的使用(替换单个和全局)
    数据库记录删除方式
    UDP接收百万级数据的解决方案
    早睡早起+微信朋友圈控制在10人以内…
    java Date型时间比较大小
    教会你时间管理的奥秘,提升工作效率。
    大多数人都输给了这个字——等
    二分法查找和普通查找
    王者荣耀里拿个王者有啥了不起,有胆就来挑战一下ApsaraCache源码
    给予Java初学者的学习路线建议
  • 原文地址:https://www.cnblogs.com/caoxinyu/p/10568500.html
Copyright © 2020-2023  润新知