Android 編譯時注解解析 —— 自己動手實現(xiàn)一個 Router

前言

類似于 ButterKnife 、 Dagger 、 ARouter,它們可以通過幾個注解,就可以實現(xiàn)以往需要很大功夫?qū)懙拇a,還可以非常好的解除依賴,來提高解耦度,提升代碼的擴展空間。
其實它們也是通過一些注解在編譯時生成一部分代碼,來提高開發(fā)效率或者實現(xiàn)一些特別的功能。

這幾天在準(zhǔn)備春招的過程中對這一塊的內(nèi)容產(chǎn)生了較大的興趣,于是也自己動手寫了一個超級簡易版的 Router,功能暫且就只實現(xiàn)跳轉(zhuǎn) Activity。

代碼demo

需要知道的是

編譯時注解

需要對 @Retention 有一個了解。

元注解 @Retention

類型 作用
SOURCE 只在 java 源文件中存在,編譯器編譯后就被丟棄
CLASS 在 Java 源文件和編譯后生成的字節(jié)碼中存在,在虛擬機運行時被丟棄
RUNTIME 運行時也存在,可以通過反射讀取注解

這里的 CLASS 就是編譯時注解,我們可以通過這樣的注解在編譯時獲取到自己想要的一些信息,并依據(jù)它們做一些操作,來達到自己的目的。

注解其它相關(guān)這里就不提了。

APT

APT(annotation processing tool) 是一種處理編譯時注解,在編譯階段解析注解并且生成 java 代碼的技術(shù),以此來減少開發(fā)時需要寫的代碼,我們這里用的就是 APT 技術(shù)。

AbstractProcessor

AbstractProcessor 是一個抽象類,是 Java 中專門用來處理注解的類。

AbstractProcessor 中有兩個需要重寫的方法

    //初始化方法,提供處理時的環(huán)境
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }
    
    //處理注解過程
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

SPI

如果對虛擬機的有了解朋友可能會對它有印象,因為可以追溯到第二次雙親委派模型破壞。

它是一種動態(tài)替換機制,可以動態(tài)加載一些繼承某個接口的實體類。

AutoService

Android build 不會生成 META-INF,而 SPI 需要根據(jù)聲明在 META-INF 中的類名來執(zhí)行類文件。

這里可以使用 Google 的 AutoService 來創(chuàng)建 META-INF,并將被注解的文件聲明到 META-INF 中。

JavaPoet

大名鼎鼎的 square 公司的 JavaPoet, 可以有效的幫忙生成代碼。

開始動手

自定義注解

創(chuàng)建一個寫自定義注解的library,注意,一定要選擇 Java Library

image.png

創(chuàng)建一個自定義注解

//指定被標(biāo)注類型為類或者接口
@Target(ElementType.TYPE)
//指定為編譯時注解
@Retention(RetentionPolicy.CLASS)
public @interface RouterUrl {
    //用來做為跳轉(zhuǎn)路徑
    String url();
}

使用注解

先在 app 的 gradle 中引入此包

    implementation project(':lib_annotation')

接下來為 Activity 標(biāo)注,因為需要跳轉(zhuǎn),這里我們寫兩個 Activty。

//當(dāng)前類
@RouterUrl(url = "main")
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

//跳轉(zhuǎn)目標(biāo)
@RouterUrl(url = "second")
public class SecondActivity extends AppCompatActivity {
    private TextView textView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.tv_text);
        textView.setText("跳轉(zhuǎn)成功~");
    }
}

自定義注解非常簡答,接下來看 APT 技術(shù)的核心,如何在編譯時解析一個注解。

解析注解

創(chuàng)建一個用來解析注解的 library,同樣也需要 Java 的 library,不然將無法使用 AbstractProcessor。

image.png

在它的 gradle 中引用注解包和需要的第三方庫

    implementation 'com.squareup:javapoet:1.8.0'
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation project(':lib_annotation')

創(chuàng)建一個 RouterCompiler 類

//通過 AutoService 將 Processor 聲明到 META-INF 中
@AutoService(Processor.class)
//指定解析的注解
@SupportedAnnotationTypes({"com.example.jzycc.lib_annotation.RouterUrl"})
public class RouterCompiler extends AbstractProcessor {
    private Filer filer;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //獲取 RouterUrl 的集合
        Set< ? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(RouterUrl.class);
        
        //這里是 JavaPoet 的使用,作用是構(gòu)建一個類,類名為 WhatRouterTable
        TypeSpec.Builder typeSpec = TypeSpec.classBuilder("WhatRouterTable")
                .addModifiers(Modifier.PUBLIC);

        for (Element element : elements){
            //獲取注解中的 url 信息
            String url = element.getAnnotation(RouterUrl.class).url();
            //將其專成存有類信息的 TypeElement
            TypeElement typeElement = (TypeElement)element;
            //獲取目標(biāo)類的全名
            String className = typeElement.getQualifiedName().toString();
            
            //構(gòu)造一個字段,以 url 為名,類名為值,如 public final static url = "classname";
            FieldSpec fieldSpec = FieldSpec.builder(String.class,url)
                    .addModifiers(Modifier.PUBLIC,Modifier.FINAL,Modifier.STATIC)
                    .initializer("$S",className)
                    .build();
            //將字段添加到 typeSpec 中
            typeSpec.addField(fieldSpec);
        }
        try{
            //指定生成 Java 文件的 包名 (這個包現(xiàn)在還沒建立,接下來它做為提供給開發(fā)者使用的 api)
            String packageFullName = "com.example.jzycc.lib_whatrouter";
            //構(gòu)造 Java 文件
            JavaFile javaFile = JavaFile.builder(packageFullName, typeSpec.build()).build();
            //寫入到 filer,生成對應(yīng)的 class 文件
            javaFile.writeTo(filer);

        }catch (IOException e){
            e.printStackTrace();
        }

        return true;
    }
}

以上就是處理注解的邏輯,我們通過解析注解獲取它的url值以及被此注解表明的類名,并將這些值做為字段添加到我們解析時生成的 Java 文件中。

如何調(diào)試 APT

直接調(diào)試是無法調(diào)試 APT 代碼的,首先找到項目中的 gradle.properties 文件,也可以在 .gradle 文件中全局配置。

加入如下代碼

org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

如圖位置我們選擇 Edit Configurations

image.png

點擊下面圖中的 + 號,并選擇 Remote

image.png

隨便起個名字,點擊調(diào)試按鈕,如果調(diào)試成功則會提示如下信息

image.png
Connected to the target VM, address: 'localhost:5005', transport: 'socket'

為 APT 代碼加入斷點,我們這時候 rebuild 程序就可以開始開始調(diào)試了。

接下來去看下如何使用解析注解得到的信息。

Router 的 API

同樣,我們?yōu)樗⒁粋€ module ,但是這次我們建立的是 Android Library,因為我們在這里需要使用 Android 的一些類。

image.png

在這里,我們先運行一下代碼,看一下注解是否會生成我們想要的 Java 文件。

按兩下 shift,搜索我們創(chuàng)建的類名 'WhatRouterTable'

可以發(fā)現(xiàn)我們很成功的生成了我們想要的信息,最重要的一步已經(jīng)完成了,接下來我們來完成如何利用這個文件。

package com.example.jzycc.lib_whatrouter;

import java.lang.String;

public class WhatRouterTable {
  public static final String main = "com.example.jzycc.whatrouter.MainActivity";

  public static final String second = "com.example.jzycc.whatrouter.SecondActivity";
}

創(chuàng)建 WhatRouter,做為 Api 的提供類。

package com.example.jzycc.lib_whatrouter;

import android.app.Activity;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import java.lang.reflect.Method;

public class WhatRouter {

    private Class clazz;
    private Context context;
    private Class goalClazz;

    private static WhatRouter instance;
    //這里用單例模式來創(chuàng)建此類
    public static WhatRouter getInstance(){
        if (instance == null){
            synchronized (WhatRouter.class){
                if (instance == null){
                    instance = new WhatRouter();
                }
            }
        }

        return instance;
    }

    private WhatRouter(){
        try{
            clazz = Class.forName("com.example.jzycc.lib_whatrouter.WhatRouterTable");
        }catch (Exception e){
            Log.e("WhatRouterError", "WhatRouter: ",e );
        }
    }
    //傳入 context
    public WhatRouter with(Context context){
        this.context = context;
        return this;
    }
    
    //傳入 url
    public WhatRouter url(String url){
        try{
            goalClazz = Class.forName(clazz.getField(url).get("").toString());

        }catch (Exception e){
            Log.e("WhatRouterError", "url: ", e);
        }
        return this;
    }
    
    //啟動 Activity
    public void startActiviity(){
        try{
            Intent intent = new Intent(context, goalClazz);
            context.startActivity(intent);
        }catch (Exception e){
            Log.e("WhatRouterError", "startActiviity: ",e );
        }
    }
}

非常的簡單,我們通過反射去獲取我們在解析注解時生成的類,再根據(jù)類中的url字段獲取目標(biāo)類的類名,依據(jù)此類名獲取目標(biāo)類用來啟動 Activity。

使用一下看看是否成功

修改 MainActivity

@RouterUrl(url = "main")
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //這里我們啟動 SecondActivity
        WhatRouter.getInstance().with(this).url("second").startActiviity();
    }
}

運行APP

image.png

成功的跳轉(zhuǎn)到了 SecondActivity。

總結(jié)

看完之后發(fā)現(xiàn)實現(xiàn)一個簡易 Router 是非常簡單的了。

當(dāng)然這只是一個學(xué)習(xí) APT 技術(shù)的 demo, 如果要投入到實際場景當(dāng)中去那是完全不行的,使用技術(shù)并不難,難的是如何更好的滿足業(yè)務(wù)場景,以及更加深入到其中去學(xué)習(xí)它的原理。

學(xué)習(xí) APT 相關(guān)的知識,我這僅僅是一個開始。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容