重走JAVA之路(一):覆盤ButterKnife-編譯時註解

前言

其實一直想寫這麼個系列,雖然Android開發大部分是基於Java語言的,可是平常開發中基本涉及的都比較簡單,當遇到一些疑難雜症的時候,很難去找到根因,本系列就針對一些日常開發比較少涉及的JAVA點,好比、註解、代理、併發等等,但願能幫到一些朋友從新鞏固下基礎知識。java

今天咱們主要來講道說道註解中另外一種實現方式,編譯時註解(CLASS),不一樣於運行時註解(RUNTIME),你們比較熟悉的須要在代碼運行時,反射拿到註解的參數值,而後再把值綁定回去,這樣反射畢竟消耗性能。著名的ButterKnife就是用的編譯時註解,利用APT在編譯時生成文件,再去賦值,就不會有性能消耗問題啦~git

1.編譯時註解

由於編譯時註解須要用到AbstractProcessor這個類,而這個是在JDK裏面的,因此咱們須要Android Stuio中新建一個Java lib github

image.png
建好以後,咱們新建一個註解類

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface InjectView {
    int value();
}
複製代碼

而後咱們在新建一個java類,集成AbstractProcessor緩存

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
@AutoService(Processor.class)
public class BuildProcess extends AbstractProcessor {
    private Filer mFiler;
    private Messager mMessager;
    @Override
    public synchronized void init(ProcessingEnvironment mProcessingEnvironment) {
         super.init(mProcessingEnvironment);

        //初始化咱們須要的基礎工具
        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(InjectView.class.getCanonicalName());
        return annotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersio() {
       return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> mSet, RoundEnvironment mRoundEnvironment) {
        return false;
    }
}
複製代碼

咱們主要關注這4個方法bash

  • init方法做爲一個初始化的入口,作一些初始化工具類的操做
  • getSupportedAnnotationTypes(),這個方法要求咱們返回一個Set集合,從方法名咱們應該也能看出來是一組支持註解類型的集合元素,這裏咱們把註解類加進去,後續若是要添加註解,這裏須要手動再添加修改
  • getSupportedSourceVersio(),返回支持的版本號,通常返回上次支持的版本號便可
  • process(),大頭來了,這個方法是整個類的核心,也是實現編譯時註解最關鍵的地方,咱們這裏從獲取到註解參數的值供咱們調用,借鑑一波他人圖來展現一下
    image.png

@AutoService(Processor.class)也是google出品的一個開源庫,省了咱們須要寫配置文件META-INF的工做。添加依賴併發

implementation 'com.google.auto.service:auto-service:1.0-rc3'
複製代碼

首先看看最終須要生成的類,是否是似曾相識的樣子!ide

image.png

接下來就是實現了,主要的代碼固然就在process方法裏面了函數

@Override
    public boolean process(Set<? extends TypeElement> mSet, RoundEnvironment mRoundEnvironment) {
        Map<TypeElement, List<InjectViewField>> parseHashMap = getParseHashMap(mRoundEnvironment);
        for (Map.Entry<TypeElement, List<InjectViewField>> entry : parseHashMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            List<InjectViewField> list = entry.getValue();
            if (list == null || list.size() == 0) {
                continue;
            }


            // 獲取包名
            String packageName = mElementUtils.getPackageOf(typeElement).getQualifiedName().toString();
            // 根據舊Java類名建立新的Java文件
            String className = typeElement.getQualifiedName().toString().substring(packageName.length() + 1);
            String newClassName = className + "_ViewBinding";

            //生成方法
            MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.bestGuess(className), "target");
            for (InjectViewField injectViewField : list) {
                String packageNameString = injectViewField.getFieldType().toString();
                ClassName viewClass = ClassName.bestGuess(packageNameString);
                methodBuilder.addStatement
                        ("target.$L=($T)target.findViewById($L)", injectViewField.getFieldName()
                                , viewClass, injectViewField.getId());
            }


            TypeSpec typeBuilder = TypeSpec.classBuilder(newClassName)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(methodBuilder.build())
                    .build();


            JavaFile javaFile = JavaFile.builder(packageName, typeBuilder)
                    .addFileComment("Generated code from Butter Knife. Do not modify!")
                    .build();
            try {
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return true;
    }
複製代碼
private Map<TypeElement,List<InjectViewField>> getParseHashMap(RoundEnvironment mRoundEnvironment) {
        Map<TypeElement,List<InjectViewField>> map = new HashMap<>();
        List<InjectViewField> injectViewFields = new ArrayList<>();
        for (Element element :mRoundEnvironment.getElementsAnnotatedWith(InjectView.class)) {
            InjectViewField injectViewField = new InjectViewField(element);
            injectViewFields.add(injectViewField);
            map.put((TypeElement) element.getEnclosingElement(),injectViewFields);
        }
        return map;
    }
複製代碼

InjectViewField做爲一個簡單的實體類封裝一些屬性工具

public class InjectViewField {
    private int id;
    private TypeElement mTypeElement;
    private String fieldName;
    private TypeMirror fieldType;
    public InjectViewField(Element element) {
        //獲取成員變量名稱mTextView
        fieldName = element.getSimpleName().toString();
        //獲取成員變量類型 TextView
        fieldType = element.asType();
        //獲取成員變量具體value R.id.textView
        id = element.getAnnotation(InjectView.class).value();
       //獲取完整類名
        mTypeElement = (TypeElement) element.getEnclosingElement();
    }

    public int getId() {
        return id;
    }

    public TypeElement getTypeElement() {
        return mTypeElement;
    }

    public String getFieldName() {
        return fieldName;
    }

    public TypeMirror getFieldType() {
        return fieldType;
    }
}
複製代碼

這裏生成文件用到了javapoet,這樣就不用本身手動去拼接字符串,具體API使用方法,能夠進去瞅瞅,也是出自square之手,很是簡便 而後再動動咱們的小手,build一下,就能獲得咱們想要的文件了~,而後咱們看看怎麼使用性能

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    @InjectView(R.id.textView)
    TextView mTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectManager.init(this);
        mTextView.setText("i have changed");
    }
}
複製代碼

很簡單吧!,看着和butterknife很相似,實際原理差很少,固然butterknife裏面涉及的仍是蠻複雜的,但基本是通的,有興趣的能夠去看看源碼,butterknife,而後咱們的InjectManager

public class InjectManager  {
    private static final String TAG = "InjectManager";
    public static void init(Activity mActivity){
        //一、獲取全限定類名
        String name = mActivity.getClass().getName();
        try {
            //二、 根據全限定類名獲取經過註解解釋器生成的Java類
            Class<?> clazz = mActivity.getClass().getClassLoader().loadClass(name + "_ViewBinding");
            //三、 經過反射獲取構造方法並建立實例完成依賴注入
            clazz.getConstructor(mActivity.getClass()).newInstance(mActivity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

有朋友要說了,你這個不也是反射嘛?emmm...確實是經過反射生成對應的文件對象,可是起碼每一個文件只涉及到了一次反射,而不是像運行時註解,每一個註解都須要反射拿到,因此 不要太苛刻了~~忽略忽略。。

大概基本就是這麼多,再來總結一波!

  • 新建java lib庫,自定義類,繼承AbstractProcessor,最少複寫3個函數(init不須要的能夠不復寫)
  • 在process方法裏面獲取對應註解的變量、方法等等,存到Map裏,後續能夠作必定的緩存,減小消耗,使用javapoet,生成java文件
  • 經過反射拿到對應類的對象,執行賦值代碼

好了,基本大概如此了,固然也有不少須要優化的地方,緩存,設計結構等等,有興趣的朋友能夠去嘗試嘗試,也是一個好的鍛鍊機會

有疑問的能夠留言討論,感謝觀看 謝謝~~

相關文章
相關標籤/搜索