嘗試手寫一個註解框架

藏地情人發表於2017-11-30

引言

前兩天在掘金上看到一篇文章: 一個小需求引發的思考。 需求是根據多個EditText是否有輸入的值來確定Button是否可點選。很常見的一個需求吧,但是要怎麼做到簡單優雅呢?文章裡也有講到封裝的過程,這裡我就不細講了。最後作者封裝成了用註解就可以做到。但是他全部用的反射,反射技術對APP效能影響還是很大的,作者最後也提到想使butterknife使用的技術。用大家都知道著名的註解框架 butterknife是使用了動態生成java程式碼的技術,這對效能影響非常小,我就在想我是不是也可以試試呢,正好學習一下butterknife原理以及用到的技術。於是說幹就幹,就有了下面的嘗試!(我也是在查閱了許多資料後學習中摸索的,如果有寫的不對的地方請大神指正。另外第一次寫部落格,排版什麼的可能不是很美觀。哈哈哈,想把自己的學習心得分享出來。慢慢進步吧!)


分析

首先要知道butterknife使用了什麼技術,那就得閱讀原始碼,網上搜一搜文章一大堆哈。這就不廢話了哈哈哈。其實最重要的技術點就兩個:

  • 怎麼解析處理註解
  • 怎麼動態生成java程式碼檔案

閱讀原始碼得知前者使用了 AndroidAnnotations框架,後者則使用了Javapoet框架。這裡簡單介紹一下吧。

準備工作

AndroidAnnotations

AndroidAnnotations是一個javax的註解解析技術。我們可以通過繼承javax.annotation.processing.AbstractProcessor這個類來定義一個自己的註解處理類。(由於Android已經不支援javax程式設計了,所以需要在一個java lib 中來寫)。

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


    /**
     * 每一個註解處理器類都必須有一個空的建構函式。然而,這裡有一個特殊的init()方法,它會被註解處理工具呼叫,
     * 並輸入ProcessingEnviroment引數。ProcessingEnviroment提供很多有用的工具類Elements,Types和Filer。
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    /**
     * 這相當於每個處理器的主函式main()。 在這裡寫掃描、評估和處理註解的程式碼,以及生成Java檔案。
     * 輸入引數RoundEnviroment,可以讓查詢出包含特定註解的被註解元素。
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

    /**
     * 這個註解處理器是註冊給哪個註解的。注意,它的返回值是一個字串的集合,
     * 包含本處理器想要處理的註解型別的合法全稱。換句話說,在這裡定義你的註解處理器註冊到哪些註解上。
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    /**
     * 返回支援的java版本
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }
}
複製程式碼

上面的程式碼中通過註釋應該瞭解每一個方法的作用了。我們處理的主要方法就是process()方法了。 另外這個類上面還有一個註解@AutoService(Processor.class) 它是幹嘛的呢?是這樣的:在以往的定義註解解析器時,需要在解析器類定義過程中,做以下操作: 在解析類名前定義: @SupportedAnnotationTypes("com.bosssoft.cloin.ViewInjectProcesser") @SupportedSourceVersion(SourceVersion.RELEASE_7)

同時在java的同級目錄新建resources目錄,新建META-INF/services/javax.annotation.processing.Processor檔案,檔案中填寫你自定義的Processor全類名,這是向JVM宣告解析器。

當然幸好我們現在使用的是AndroidStudio,可以用auto-service來替代以上操作。只要在註解類前面加上@AutoService(Processor.class)就可以替代以上操作。它是由谷歌開發的,在gradle中加上:

 compile 'com.google.auto.service:auto-service:1.0-rc2'
複製程式碼

有興趣的小夥伴可以自行網上搜尋瞭解更多內容....

Javapoet

Javapoet是squareup公司提供的能夠自動生成java程式碼的庫。

講一下javapoet裡面常用的幾個類: MethodSpec代表一個建構函式或方法宣告。 TypeSpec 代表一個類,介面,或者列舉宣告。 FieldSpec 代表一個成員變數,一個欄位宣告。 JavaFile包含一個頂級類的Java檔案

舉個例子:

private void generateHelloworld() throws IOException{
        //構建main方法
        MethodSpec main = MethodSpec.methodBuilder("main") //main代表方法名
                    .addModifiers(Modifier.PUBLIC,Modifier.STATIC)//Modifier 修飾的關鍵字
                .addParameter(String[].class, "args") //新增string[]型別的名為args的引數
                    .addStatement("$T.out.println($S)", System.class,"Hello World")//新增程式碼,這裡其實就是新增了System,out.println("Hello World");
                .build();
                //構建HelloWorld類
                TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")//HelloWorld是類名
                .addModifiers(Modifier.FINAL,Modifier.PUBLIC)
                .addMethod(main)  //在類中新增方法
                .build();
                //生成java檔案
        JavaFile javaFile = JavaFile.builder("com.example.helloworld", typeSpec)
                .build();
        javaFile.writeTo(System.out);
    }
複製程式碼

執行一下

嘗試手寫一個註解框架

看到這應該知道java程式碼是怎麼生成的了吧。現在需要用到的技術點都大致瞭解了。準備工作做好了,現在進入正題吧。

擼程式碼

專案結構

嘗試手寫一個註解框架

annotation:(java lib) 提供註解。 annotation-compiler:(java lib)註解處理。 annotation-api:(Android Lib) 是我們外部用到 api。 app:是呼叫api進行測試的。

APP模組

public class MainActivity extends AppCompatActivity {

    @WatchEdit(editIds = {R.id.ed_1, R.id.ed_2, R.id.ed_3})
    Button button1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button1 = (Button) findViewById(R.id.bbb11);
        ViewAnnoUtil.watch(this);
        button1.setEnabled(false);
    }
}
複製程式碼

看看我們使用了什麼: 一個註解標記:@WatchEdit 還有一句程式碼: ViewAnnoUtil.watch(this);

這兩句話到底是怎麼生效的呢?這就到下一個模組了。

annotation模組

先來看這個註解標記

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface WatchEdit {

    /**
     * 被觀察的輸入框的id
     *
     * @return
     */
    int[] editIds();
}
複製程式碼

它位於annotation模組中,為了觀察多個EditText,定義一個註解。引數是 要觀察的EditText的id。

api模組

再來看ViewAnnoUtl.watch(this)幹了啥:

//最終對外使用的工具類
public class ViewAnnoUtil {
    private static ActivityWatcher actWatcher = new ActivityWatcher();

    private static Map<String, Injector> WATCH_MAP = new HashMap<>();

    public static void watch(Activity activity) {
        watch(activity, activity, actWatcher);
    }

    private static void watch(Object host, Object source, Watcher watcher) {
        String className = host.getClass().getName();
        try {
            Injector injector = WATCH_MAP.get(className);
            if (injector == null) {
                Class<?> finderClass = Class.forName(className + "$$Injector");
                injector = (Injector) finderClass.newInstance();
                WATCH_MAP.put(className, injector);
            }
            injector.inject(host, source, watcher);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

看到這個類中的程式碼,我們知道,watch(this)實際上呼叫了watch(Object host, Object source, Watcher watcher)方法。

其中hostsource引數都是傳的activity,watcher引數則是傳的類裡例項化的ActivityWatcher物件例項。做了一些由於使用這裡使用了一些反射,所以通過使用記憶體快取來進行優化。最後則呼叫了injector.inject()方法,那我們看看這些都是什麼東西。

//觀察類的介面
public interface Watcher {
    /**
     * 查詢view的方法
     *
     * @param obj view的來源,哪個activity或者fragment
     * @param id  要查詢的view的id
     * @return 查詢到的view
     */
    EditText findView(Object obj, int id) throws ClassCastException;

    /**
     * 進行觀察
     *
     * @param editText 被觀察的edit
     * @param obser    觀察的view
     */
    void watchEdit(EditText editText, View obser);
}
複製程式碼
//提供一個預設的通過Activity實現的Watcher
public class ActivityWatcher implements Watcher, TextWatcher {
    private HashMap<View, ArrayList<EditText>> map = new HashMap<>();


    @Override
    public EditText findView(Object obj, int id) throws ClassCastException {
        return (EditText) ((Activity) obj).findViewById(id);
    }

    @Override
    public void watchEdit(EditText editText, final View obser) {
        if (map.get(obser) == null) {
            ArrayList<EditText> itemEditList = new ArrayList<>();
            itemEditList.add(editText);
            map.put(obser,itemEditList);
        } else {
           map.get(obser).add(editText);
        }
        editText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {

            }

            @Override
            public void afterTextChanged(Editable s) {
                if (checkEnable(map.get(obser)))
                    obser.setEnabled(true);
                else obser.setEnabled(false);
            }
        });


    }

    private boolean checkEnable(ArrayList<EditText> editList) {
        for (EditText text : editList) {
            if (TextUtils.isEmpty(text.getText().toString()))
                return false;
        }
        return true;
    }
複製程式碼

這很好理解了吧,也就是說具體的讓Button監聽到EditText輸入變化的程式碼在這裡。 再來看injector:

//繫結的介面類
public interface Injector<T> {
    /**
     * @param host    目標
     * @param source  來源
     * @param watcher 提供具體使用的方法 查詢edit,新增監聽
     */
    void inject(T host, Object source, Watcher watcher);
}
複製程式碼

可以發現其實它是一個介面,規定了目標從哪裡來,由誰來執行這個監聽操作(Wathcer) 那麼問題來了,光是介面怎麼能夠實現功能呢?肯定得有一個介面的實現類才行吧。 彆著急,我們看這一段程式碼:

 String className = host.getClass().getName();
 Injector injector = WATCH_MAP.get(className);
            if (injector == null) {
                Class<?> finderClass = Class.forName(className + "$$Injector");
                injector = (Injector) finderClass.newInstance();
                WATCH_MAP.put(className, injector);
            }
複製程式碼

可以發現其實我們用反射載入了一個類,類名是 host的類名+ "$$Injector" 是不是很熟悉?使用butterknife的小夥伴肯定遇到過 MainActivity&&ViewBinder這類似的類名吧。沒錯就是它。他就是我們 Injector的實現類,完成了具體的實現。只是它是由我們前面提到的 javapoet動態生成的。再來看看這個順序:

ViewAnnoUtil.watch() ----> injector.inject()並傳入了目標的Activity,和我們寫好的ActivityWacther。
通過動態生成的injector實現類來協調。
複製程式碼

現在我們來看看怎麼生成這個實現類。

compiler模組

annotation-compiler中包含註解處理器,java檔案生成等

常量類

//常量工具類
public class TypeUtil {
    public static final ClassName WATCHER = ClassName.get("com.colin.annotation_api", "Watcher");

    public static final ClassName INJECTOR = ClassName.get("com.colin.annotation_api", "Injector");
}
複製程式碼

註解處理類

@AutoService(Processor.class)
public class WatchEditProcessor extends AbstractProcessor {
    //具體程式碼我放後面
}
複製程式碼

前面介紹過怎麼定義註解處理器,我們來看看這個類裡該幹什麼

先來簡單的,定義我們支援的註解,我們這隻支援@WatchEdit這個註解。(可以有多個)

 @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();

        types.add(WatchEdit.class.getCanonicalName());
        return types;
    }
複製程式碼

我們支援的java版本是最高版本

@Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
複製程式碼

定義幾個成員變數:

    //檔案工具類
    private Filer mFiler; 
    
    //處理元素的工具類
    private Elements mElementUtils;
    
    //log工具類
    private Messager mMessager; 
    
    //使用了註解的類的包裝類的集合
    private Map<String, WatchEditAnnotatedClass> mAnnotatedClassMap = new HashMap<>(); 
複製程式碼

然後在init方法中進行了初始化

@Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mElementUtils = processingEnvironment.getElementUtils();
        mMessager = processingEnvironment.getMessager();
        mFiler = processingEnvironment.getFiler();
    }
複製程式碼

最後看最重要的方法 process()

   @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        mAnnotatedClassMap.clear();
        try {
            processWatchEdit(roundEnvironment);
        } catch (IllegalArgumentException e) {
            error(e.getMessage());
            return true;
        }

        try {

            for (WatchEditAnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
                info("generating file for %s", annotatedClass.getFullClassName());
                annotatedClass.generateWatcher().writeTo(mFiler);
            }
        } catch (Exception e) {
            e.printStackTrace();
            error("Generate file failed,reason:%s", e.getMessage());
        }
        return true;
    }
複製程式碼

返回true表示已經處理過。

先看這句processWatchEdit(roundEnvironment);程式碼幹了什麼:

private void processWatchEdit(RoundEnvironment roundEnv) {
        //遍歷處理 使用了 @WatchEdit 註解的類
        //一個element代表一個元素(可以是類,成員變數等等)
        for (Element element : roundEnv.getElementsAnnotatedWith(WatchEdit.class)) {
            WatchEditAnnotatedClass annotatedClass = getAnnotatedClass(element);
            //通過 roundEnv工具構建一個成員變數
            WatchEditField field = new WatchEditField(element);
            //新增使用了@WatchEdit註解的成員變數
            annotatedClass.addField(field);
        }
    }


    private WatchEditAnnotatedClass getAnnotatedClass(Element element) {
        //得到一個 類元素
        TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
        //拿到類全名
        String fullClassName = encloseElement.getQualifiedName().toString();
        //先從快取中取
        WatchEditAnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
        if (annotatedClass == null) {
            //沒有就構建一個
            annotatedClass = new WatchEditAnnotatedClass(encloseElement, mElementUtils);
            //放入快取
            mAnnotatedClassMap.put(fullClassName, annotatedClass);
        }
        return annotatedClass;
    }
複製程式碼

這裡又用到了兩個類: WatchEditField:被@WatchEdit註解標記的成員變數的包裝類。 WatchEditAnnotatedClass:使用了@WatchEdit註解的類。 拿上面的例子來說MainActivity就是WatchEditAnnotatedClassButton button1這個button1就是WatchEditField 這兩個類裡面具體有什麼待會看,現在看下一段程式碼:

 for (WatchEditAnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
                info("generating file for %s", annotatedClass.getFullClassName());
                annotatedClass.generateWatcher().writeTo(mFiler);
            }
複製程式碼

這裡迴圈我們的包裝類,並呼叫generateWatcher()方法,並寫入到前面提到的檔案工具中。 看這個方法名就知道,這裡就是生成java程式碼的核心方法了。至此流程終於連上了。。。不容易啊 =_=

梳理一下:

嘗試手寫一個註解框架

流程搞明白了,接下來看看,我們費了大力氣生成的java檔案怎麼生成的,也就是generateWatcher()裡做了啥,來看程式碼:

    public JavaFile generateWatcher() {
        String packageName = getPackageName(mClassElement);
        String className = getClassName(mClassElement, packageName);
        //獲取到當前使用了註解標記的類名(MainActivity)
        ClassName bindClassName = ClassName.get(packageName, className);
        //構建出重寫的inject方法
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(TypeUtil.WATCHER, "watcher");

        //新增程式碼
        for (WatchEditField field : mFiled) {
            //獲得每個button要監聽的EditText的id
            int[] ids = field.getResIds();
            if (ids != null) {
                //為每個EditText新增監聽
                for (int i = 0; i < ids.length; i++) {
                    //新增監聽
                    methodBuilder.addStatement("watcher.watchEdit(watcher.findView(source,$L),$N)",
                            ids[i], "host." + field.getFieldName());
//                    methodBuilder.addStatement("watcher.watchEdit(watcher.findView(source,$L),$N)",
//                            ids[i], field);
                }
            }
        }

        //構建類 MainActivity$$Injector
        TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJECTOR, TypeName.get(mClassElement.asType())))
                .addMethod(methodBuilder.build()) //新增剛剛生成的injector方法
                .build();
        //生成一個java檔案
        return JavaFile.builder(packageName, finderClass).build();
    }
複製程式碼

這裡就用到了我們前面提到的javapoet庫了。通過註釋應該很好理解這段程式碼的意思了。 關於javapoet有興趣的小夥伴可以自行搜尋瞭解更多內容。

好了,至此一切都結束了!!!至於WatchEditField的程式碼貼下面了。 測試了一波功能是正常執行了。。不會貼動圖。。。然後接下來就要考慮的是接觸繫結,釋放資源等等優化了。先到這吧。

結束

平時用起來很方便的東西瞭解一下原理才發現還是很複雜的。一個小小的需求,仔細研究一下也會學習到很多知識,學會了新的知識就可以應用到更多的方面去,有學習才有進步。加油。另外Demo我會放到Github上去。裡面也會慢慢更新出更多的東西 GitHub

被@WatchEdit註解標記的成員變數包裝類,如一個 button

public class WatchEditField {
    private VariableElement mFieldElement;

    private int[] mEditIds;

    public WatchEditField(Element element) throws IllegalArgumentException {
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException(String.format("Only field can be annotated with @%s",
                    WatchEdit.class.getSimpleName()));
        }
        mFieldElement = (VariableElement) element;

        WatchEdit bindView = mFieldElement.getAnnotation(WatchEdit.class);
        if (bindView != null) {
            mEditIds = bindView.editIds();
            if (mEditIds == null && mEditIds.length <= 0) {
                throw new IllegalArgumentException(String.format("editIds() in %s for field % is not valid",
                        WatchEdit.class.getSimpleName(), mFieldElement.getSimpleName()));
            }
        }
    }

 c
    public Name getFieldName() {
        return mFieldElement.getSimpleName();
    }


    public int[] getResIds() {
        return mEditIds;
    }

    public TypeMirror getFieldType() {
        return mFieldElement.asType();
    }

    public Object getConstantValue(){
        return mFieldElement.getConstantValue();
    }
}
複製程式碼

相關文章