1. 程式人生 > >ButterKnife原始碼分析和手寫

ButterKnife原始碼分析和手寫

大學剛出來實習那會,自己寫了一個執行時的 ViewById 和 OnClick 註解,用來解決 findViewById 和 setOnClickListener 。其實也是參考 xUtils 的原始碼,後來加了很多的功能擴充套件,如網路檢測等等。關鍵是那時 xUtils 並沒有外掛,自己學著寫了外掛。這個在外包公司能省不少事,技術總監開會說誰誰怎麼怎麼樣,大家要像他學習啊。工資一下漲了不少,裝 B 的技能瞬間提升了一個檔次。隔年回學校拿了畢業證,公司就關門了,這是幹垮的第一家。

後來 ButterKnife 火了,相信很多人都用過,說什麼沒有用反射,而且自帶外掛也不要寫程式碼。我第一反應是蒙的,很好奇不用反射也行?這麼厲害。今天我就帶大家來了解一下 ButterKnife 的原始碼,自己動手去寫,相信這樣會有更深層次的理解,自己手寫不代表我們重複去造輪子。也不是沒事幹,而是知識是用來喚醒智慧的,我們可以把這些知識用到其他地方,比如我們繞過微信支付的侷限性,自動讀取生成配置程式碼等等。從原始碼中汲取營養,這個還是蠻重要。

1. ButterKnife 原理分析

分析原始碼之前我們首先要會使用,這裡就不做詳細介紹,如果不太會的哥們可以去看看別人的文章。網上也有很多分析原始碼的文章,都可以看看,我其實也是看了很多這方面的文章。找到 github 地址,都有教我們怎麼使用。當我們點選執行的時候,仔細找找我們能找到這麼一些程式碼:

// Generated code from Butter Knife. Do not modify!
package com.darren.architect_day05;

public class MainActivity_ViewBinding implements Unbinder
{
private MainActivity target; @UiThread public MainActivity_ViewBinding(MainActivity target) { this(target, target.getWindow().getDecorView()); } @UiThread public MainActivity_ViewBinding(MainActivity target, View source) { this.target = target; target.textView1 = Utils.findRequiredViewAsType(source, R.id.tv1, "field 'textView1'"
, TextView.class); target.textView2 = Utils.findRequiredViewAsType(source, R.id.tv2, "field 'textView2'", TextView.class); } @Override @CallSuper public void unbind() { MainActivity target = this.target; if (target == null) throw new IllegalStateException("Bindings already cleared."); this.target = null; target.textView1 = null; target.textView2 = null; } }

看到這裡我想很多人都應該懂了,其實當我們每次點選執行的時候, 都會去掃描執行時的註解,然後自動生成這麼一個類,是自動生成的不是我們自己寫的。規則大概就像上面這樣,xxx_ViewBinding 作為類名,實現 Unbinder 在 xxx_ViewBinding 的建構函式裡面去 findViewById 或者 setOnclickListener。我們只要在 MainActivity 中去例項化一個 MainActivity_ViewBinding 物件,那麼我們 MainActivity 裡面的所有屬性就都會被賦值了。這個目前來看完全沒有涉及到任何反射,那麼我們只需要去了解怎麼生成程式碼豈不就行了。我們按照這個思路,自己動手來寫一個 ButterKnife 。

2. apt 生成程式碼

怎麼自動生成程式碼,這個其實在 thinking in java 這本書中有提到,這個屬於 mirror 板塊的知識。實在不行嘞,我們可以參考 ButterKnife 的寫法。我們新建一個 Java 工程,注意一定要是一個 Java 工程,否則待會有可能找不到類。

新建 ButterKnifeProcessor 繼承 AbstractProcessor

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {

    /**
     * 用來指定支援的 AnnotationTypes
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }

    /**
     * 參考了 ButterKnife 的寫法
     *
     * @return
     */
    private Set<Class<? extends Annotation>> getSupportedAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        annotations.add(BindView.class);
        return annotations;
    }

    /**
     * 用來指定支援的 SourceVersion
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 除錯列印
        System.out.println("------------------------------------>");
        System.out.println("------------------------------------>");
        return false;
    }
}

上面就這麼簡單,點選執行apk,如果你能看到下面這個列印,代表配置這方面是沒有問題的,如果沒有看到任何列印,說明沒起作用,要先找找問題:

如果我們在程式碼的任何地方有註解,都會來到 process 這個方法裡面,我們只要在這個方法裡面生成程式碼就可以了,至於註解反射泛型的基礎知識這個之前講過,這裡就不做詳細的介紹。

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 除錯列印
        // System.out.println("------------------------------------>");
        // System.out.println("------------------------------------>");

        Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);

        // 解析 Element
        Map<Element, List<Element>> analysisElementMap = new LinkedHashMap<>();
        for (Element bindViewElement : bindViewElements) {
            Element enclosingElement = bindViewElement.getEnclosingElement();

            List<Element> elements = analysisElementMap.get(enclosingElement);
            if (elements == null) {
                elements = new ArrayList<>();
                analysisElementMap.put(enclosingElement, elements);
            }

            elements.add(bindViewElement);
        }

        // 生成 java 類
        for (Map.Entry<Element, List<Element>> entry : analysisElementMap.entrySet()) {
            Element enclosingElement = entry.getKey();
            List<Element> elements = entry.getValue();

            String classNameStr = enclosingElement.getSimpleName().toString();

            ClassName unbinderClassName = ClassName.get("com.butterknife", "Unbinder");
            // 組裝類:  xxx_ViewBinding implements Unbinder
            TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(classNameStr + "_ViewBinding")
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addSuperinterface(unbinderClassName);

            ClassName callSuperClassName = ClassName.get("android.support.annotation", "CallSuper");
            // 組裝unbind 方法
            MethodSpec.Builder unbindMethodBuilder = MethodSpec.methodBuilder("unbind")
                    .addAnnotation(Override.class)
                    .addAnnotation(callSuperClassName)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(TypeName.VOID);

            ClassName uiThreadClassName = ClassName.get("android.support.annotation", "UiThread");
            ClassName parameterClassName = ClassName.bestGuess(classNameStr);
            // 組裝建構函式: public xxx_ViewBinding(xxx target)
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addAnnotation(uiThreadClassName)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(parameterClassName, "target");

            // 新增 target.textView1 = Utils.findViewById(target,R.id.tv1);
            for (Element bindViewElement : elements) {
                String filedName = bindViewElement.getSimpleName().toString();
                ClassName utilClassName = ClassName.get("com.butterknife", "Utils");
                int resId = bindViewElement.getAnnotation(BindView.class).value();
                constructorBuilder.addStatement("target.$L = $T.findViewById(target,$L)",
                        filedName, utilClassName, resId);
            }

            typeSpecBuilder.addMethod(constructorBuilder.build());
            typeSpecBuilder.addMethod(unbindMethodBuilder.build());

            try {
                // 寫入生成 java 類
                String packageName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
                JavaFile.builder(packageName, typeSpecBuilder.build())
                        .addFileComment("ButterKnife自動生成")
                        .build().writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("翻車了");
            }
        }

        return false;
    }

最後我們看下生成的類檔案,下一篇將利用編譯時註解的知識點,生成程式碼,解決掉我們開發過程中的一些棘手問題。

視訊講解地址:週六晚八點