源碼解析:解析掌閱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);\n", 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\ntranslate from {@link  %s.R.layout.%s}" +
                        "\nautho chengwei \nemail chengwei@zhangyue.com\n", 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,這些都沒有完全支持。
但是,作者的思路,創新,以及對新技術的學習之后使用,還是很值得我們肯定和學習的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM