1. 程式人生 > >Android開發之手把手教你寫ButterKnife框架(三)

Android開發之手把手教你寫ButterKnife框架(三)

系列文章目錄導讀:

一、概述

然後在Processor裡生成自己的程式碼,把要輸出的類,通過StringBuilder拼接字串,然後輸出。

    try { // write the file
        JavaFileObject source = processingEnv.getFiler().createSourceFile("com.chiclaim.processor.generated.GeneratedClass");
        Writer writer = source.openWriter();
        writer.write(builder.toString
()); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); }

輸出簡單的類這種方法還是挺好的,簡單明瞭,如果要輸出複雜點的java檔案,這個就不是很方便了,接下來介紹一個square公司開源的框架javapoet來幫助我們構建java檔案。

二、 javapoet簡單使用

通過MethodSpec類來構建java方法,如:

MethodSpec main = MethodSpec.methodBuilder
("main") //方法名 .addModifiers(Modifier.PUBLIC, Modifier.STATIC) //方法修飾符 .returns(void.class) //方法返回型別 .addParameter(String[].class, "args") //方法引數 .addStatement("$T.out.println($S)"
, System.class, "Hello, JavaPoet!")//方法體語句 .build();

通過TypeSpec來構建java類的修飾符

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")  //類名
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)         //類的修飾符
    .addMethod(main)                                       新增類方法(MethodSpec main)
    .build();

通過JavaFile輸出類

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();
javaFile.writeTo(System.out);

通過CodeBlock來構建方法體

上面通過MethodSpec.addaddParameter(String[].class, “args”)方法構建方法體,對於比較複雜的可以通過CodeBlock來構建:

codeBlock.addStatement("$L.inject(this, this)", mViewInjectorName);

javapoet就先介紹到這裡。更多具體的使用可以檢視官方文件或者其他資料。

三、通過javapoet生成Bind類

在ButterKnifeProcessor process方法中獲取基本資訊

我們要想達到在生成的類中初始化activity Views,那麼肯定需要如下類似下面的程式碼(虛擬碼):

public class MainActivity_Binding {

    public MainActivity_Binding(MainActivity target,View view) {
        target.text1 = (TextView)view.findViewById(id);
        target.text2 = (TextView)view.findViewById(id);
        target.text3 = (TextView)view.findViewById(id);
        target.text4 = (TextView)view.findViewById(id);
    }
}

據此,我們需要在ButterKnifeProcessor process方法裡獲取三個基本資訊:
1、註解所在的類,用於生成類名,比如MainActivity使用了註解,那麼生成的類就是 MainActivity_ViewBinding

2、註解的值,用於findViewById,如:

    @BindView(R.id.title)
    TextView title;

那麼我們要獲取的值就是R.id.title

3、註解所在欄位的型別,用於強轉。如:

    @BindView(R.id.title)
    TextView title;

那麼我們要獲取的型別就是TextView

通過下面的方法可以獲取上面的資訊
【element.getEnclosingElement()】 //註解所在的類
【element.getAnnotation(BindView.class).value()】 //註解上的值, 用於findViewById
【element.asType()】 //註解欄位的型別,用於強轉

通過上一篇部落格知道,我們是在process方法裡生成程式碼的:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    //TODO do something
    return true;
}

對RoundEnvironment裡的資訊進行分組處理

所有關於類上的註解資訊,全部在 RoundEnvironment roundEnv裡,而且可能的多個類用到了註解, 所以我們要對RoundEnvironment的資訊進行分組處理。

我通過Map來儲存分組的資訊,
Map

//roundEnv裡的資訊進行分組
private void parseRoundEnvironment(RoundEnvironment roundEnv) {
        // 儲存分組資訊
        Map<TypeElement, BindClass> map = new LinkedHashMap<>();
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            //註解的值
            int annotationValue = element.getAnnotation(BindView.class).value();
            //如果不存在建立BindClass,要建立的程式碼都存在BindClass裡
            BindClass bindClass = map.get(enclosingElement);
            if (bindClass == null) {
                bindClass = BindClass.createBindClass(enclosingElement);
                map.put(enclosingElement, bindClass);
            }
            String name = element.getSimpleName().toString();
            TypeName type = TypeName.get(element.asType());
            //ViewBinding用於儲存每個註解的相關資訊(比如註解所在欄位的名稱、註解所在欄位的型別、註解上的值,)
            ViewBinding viewBinding = ViewBinding.createViewBind(name, type, annotationValue);
            //因為一個類上可能多處用了註解,所以用一個集合儲存
            bindClass.addAnnotationField(viewBinding);
        }

        //迭代分組後的資訊,主義生成對應的類
        for (Map.Entry<TypeElement, BindClass> entry : map.entrySet()) {
            printValue("==========" + entry.getValue().getBindingClassName());
            try {
                entry.getValue().preJavaFile().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

ViewBinding用於儲存每個註解的相關資訊,程式碼也很簡單:

class ViewBinding {

    private final String name;
    private final TypeName type;
    private final int value;

    private ViewBinding(String name, TypeName type, int value) {
        this.name = name;
        this.type = type;
        this.value = value;
    }

    static ViewBinding createViewBind(String name, TypeName type, int value) {
        return new ViewBinding(name, type, value);
    }
}

BindClass用於儲存需要生成的程式碼,裡面封裝了javapoet相關處理,所有具有生成程式碼的功能.
先來看看建立BindClass構造方法:

    private BindClass(TypeElement enclosingElement) {
        //asType 表示註解所在欄位是什麼型別(eg. Button TextView)
        TypeName targetType = TypeName.get(enclosingElement.asType());
        if (targetType instanceof ParameterizedTypeName) {
            targetType = ((ParameterizedTypeName) targetType).rawType;
        }
        //註解所在類名(包括包名)
        String packageName = enclosingElement.getQualifiedName().toString();
        packageName = packageName.substring(0, packageName.lastIndexOf("."));
        String className = enclosingElement.getSimpleName().toString();
        //我們要生成的類的類名
        ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
        boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
        //註解所在類,在生成的類中,用於呼叫findViewById
        this.targetTypeName = targetType;
        this.bindingClassName = bindingClassName;
        //生成的類是否是final
        this.isFinal = isFinal;
        //用於儲存多個註解的資訊
        fields = new ArrayList<>();
    }

添加註解資訊實體

    void addAnnotationField(ViewBinding viewBinding) {
        fields.add(viewBinding);
    }

生成類的修飾符,方法:

    private TypeSpec createTypeSpec() {
        TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
                .addModifiers(PUBLIC);
        if (isFinal) {
            result.addModifiers(FINAL);
        }

        result.addMethod(createConstructor(targetTypeName));


        return result.build();
    }

建立構造方法,在構造方法裡生成初始化View的程式碼:

    private MethodSpec createConstructor(TypeName targetType) {
        MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
                .addModifiers(PUBLIC);
        //構造方法有兩個引數,target和source,在本例子中,Target就是activity,source就是activity的DecorView
        constructor.addParameter(targetType, "target", FINAL);
        constructor.addParameter(VIEW, "source");
        //可能有多個View需要初始化,也就是說activity中多個欄位用到了註解
        for (ViewBinding bindings : fields) {
            //生成方法裡的語句,也就是方法體
            addViewBinding(constructor, bindings);
        }

        return constructor.build();
    }

下面看看如何為activity中每個用到註解的View在構造方法中生成初始化程式碼:

    private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
        //通過CodeBlock生成語句,因為生成的語句比較複雜。
        CodeBlock.Builder builder = CodeBlock.builder()
                .add("target.$L = ", binding.getName());
        //判斷是否需要強制型別轉換,如果目標View本來就是View,那就不需要強轉了
        boolean requiresCast = requiresCast(binding.getType());
        if (!requiresCast) {
            builder.add("source.findViewById($L)", binding.getValue());
        } else {
            //我們使用ProcessorUtils重點工具方法findViewByCast進行強轉 $T就是一個佔位符,UTILS就是ClassName包含了UTILS的包名和類名
            //用ProcessorUtils替換成$T CodeBlock還支援很多佔位符,需要了解更多可以去看看文件.
            builder.add("$T.findViewByCast", UTILS);
            //ProcessorUtils.findViewByCast需要的引數source就是DecorView
            builder.add("(source, $L", binding.getValue());
            //ProcessorUtils.findViewByCast需要的引數$T.class,就是目標View需要強轉的型別
            builder.add(", $T.class", binding.getRawType());
            builder.add(")");
        }
        result.addStatement("$L", builder.build());

    }

下面就是強轉用到的工具類:

public class ProcessorUtils {


    public static <T> T findViewByCast(View source, @IdRes int id, Class<T> cls) {
        View view = source.findViewById(id);
        return castView(view, id, cls);
    }

    private static <T> T castView(View view, @IdRes int id, Class<T> cls) {
        try {
            return cls.cast(view);
        } catch (ClassCastException e) {
            //提示使用者型別轉換異常
            throw new IllegalStateException(view.getClass().getName() + "不能強轉成" + cls.getName());
        }
    }

}

注意, 如果你需要除錯public boolean process(Set

// Generated code from My Butter Knife. Do not modify!!!
package com.chiclaim.sample;

import android.view.View;
import android.widget.TextView;
import com.chiclaim.butterknife.ProcessorUtils;

public class MainActivity_ViewBinding {
  public MainActivity_ViewBinding(final MainActivity target, View source) {
    target.textView = ProcessorUtils.findViewByCast(source, 2131427414, TextView.class);
    target.view = ProcessorUtils.findViewByCast(source, 2131427415, TextView.class);
  }
}

接下來就簡單了,在MainActivity中呼叫MainActivity_ViewBinding的構造方法就可以了。因為我們生成的類是有規律的,包名就是使用者的包名,類名是使用者類名加ViewBinding。然後通過反射呼叫下就可以了:

public class MyButterKnife {
    public static void bind(Activity activity) {
        //獲取activity的decorView
        View view = activity.getWindow().getDecorView();
        String qualifiedName = activity.getClass().getName();

        //找到該activity對應的Bind類
        String generateClass = qualifiedName + "_ViewBinding";
        try {
            //然後呼叫Bind類的構造方法,從而完成activity裡view的初始化
            Class.forName(generateClass)
                    .getConstructor(activity.getClass(), View.class).newInstance(activity, view);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

所以只需要在跟butterknife一樣在activity的onCreate宣告週期方法裡呼叫bind方法即可,如下所示:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.text_view)
    TextView textView;

    @BindView(R.id.view)
    TextView view;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //完成初始化操作
        MyButterKnife.bind(this);

        Toast.makeText(this, textView + "--textView", Toast.LENGTH_LONG).show();
        Log.d("MainActivity", textView + "," + view);

        textView.setText("initialed by my butter knife");
    }
}

四、總結

1> butterknife 是一個執行時依賴祝框架,簡化android的大量模板程式碼,使用apt來生成程式碼
2> 像很多框架都是跟butterKnife的機制太不多的,比如下面幾款流行的框架:
greendao 流行的sqlite框架
dagger2 依賴注入框架
PermissionsDispatcher 處理Android6.0許可權的框架
所以利用這個技術,也可以整個自己的框架。