Android APT(編譯時代碼生成)最佳實踐

越來越多第三方庫使用apt技術串结,如DBflowDagger2饶套、ButterKnifeActivityRouter其馏、AptPreferences凤跑。在編譯時根據Annotation生成了相關的代碼,非常高大上但是也非常簡單的技術叛复,可以給開發(fā)帶來了很大的便利仔引。

Annotation

如果想學習APT,那么就必須先了解Annotation的基礎褐奥,這里就不展開了

APT

APT(Annotation Processing Tool)是一種處理注釋的工具,它對源代碼文件進行檢測找出其中的Annotation咖耘,使用Annotation進行額外的處理。
Annotation處理器在處理Annotation時可以根據源文件中的Annotation生成額外的源文件和其它的文件(文件具體內容由Annotation處理器的編寫者決定),APT還會編譯生成的源文件和原來的源文件撬码,將它們一起生成class文件儿倒。

創(chuàng)建Annotation Module

首先,我們需要新建一個名稱為annotation的Java Library,主要放置一些項目中需要使用到的Annotation和關聯(lián)代碼夫否。這里簡單自定義了一個注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS) 
public @interface Test {   } 
配置build.gradle,主要是規(guī)定jdk版本
apply plugin: 'java'
sourceCompatibility = 1.7 
targetCompatibility = 1.7 
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

創(chuàng)建Compiler Module

創(chuàng)建一個名為compiler的Java Library凰慈,這個類將會寫代碼生成的相關代碼汞幢。核心就是在這里。

配置build.gradle
apply plugin: 'java'
sourceCompatibility = 1.7 
targetCompatibility = 1.7 
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.7.0'
    compile project(':annotation')
}
  1. 定義編譯的jdk版本為1.7微谓,這個很重要森篷,不寫會報錯。
  2. AutoService 主要的作用是注解 processor 類豺型,并對其生成 META-INF 的配置信息仲智。
  3. JavaPoet 這個庫的主要作用就是幫助我們通過類調用的形式來生成代碼。
  4. 依賴上面創(chuàng)建的annotation Module姻氨。

定義Processor類

生成代碼相關的邏輯就放在這里钓辆。

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(Test.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}
生成第一個類

我們接下來要生成下面這個HelloWorld的代碼:

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

修改上述TestProcessor的process方法

@Override    
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   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 helloWorld = TypeSpec.classBuilder("HelloWorld")
           .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
           .addMethod(main)
           .build();
   JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
           .build();
   try {
       javaFile.writeTo(processingEnv.getFiler());
   } catch (IOException e) {
       e.printStackTrace();
   }
   return false;
}

在app中使用

配置項目根目錄的build.gradle
dependencies {
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
配置app的build.gradle
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
//...
dependencies {
    //..
    compile project(':annotation')
    apt project(':compiler')
}
編譯使用

在隨意一個類添加@Test注解

@Test
public class MainActivity extends AppCompatActivity {

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
   }
}

點擊Android Studio的ReBuild Project,可以在在app的 build/generated/source/apt目錄下哼绑,即可看到生成的代碼岩馍。

基于注解的View注入:DIActivity

到目前我們還沒有使用注解碉咆,上面的@Test也沒有實際用上抖韩,下面我們做一些更加實際的代碼生成。實現(xiàn)基于注解的View疫铜,代替項目中的findByView茂浮。這里僅僅是學習怎么用APT,如果真的想用DI框架壳咕,推薦使用ButterKnife席揽,功能全面。

  1. 第一步谓厘,在annotation module創(chuàng)建@DIActivity幌羞、@DIView注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface DIActivity {
    
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DIView {
    int value() default 0;
}
  1. 創(chuàng)建DIProcessor方法
@AutoService(Processor.class)
public class DIProcessor extends AbstractProcessor {

    private Elements elementUtils;

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 規(guī)定需要處理的注解
        return Collections.singleton(DIActivity.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("DIProcessor");
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(DIActivity.class);
        for (Element element : elements) {
            // 判斷是否Class
            TypeElement typeElement = (TypeElement) element;
            List<? extends Element> members = elementUtils.getAllMembers(typeElement);
            MethodSpec.Builder bindViewMethodSpecBuilder = MethodSpec.methodBuilder("bindView")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(TypeName.VOID)
                    .addParameter(ClassName.get(typeElement.asType()), "activity");
            for (Element item : members) {
                DIView diView = item.getAnnotation(DIView.class);
                if (diView == null){
                    continue;
                }
                bindViewMethodSpecBuilder.addStatement(String.format("activity.%s = (%s) activity.findViewById(%s)",item.getSimpleName(),ClassName.get(item.asType()).toString(),diView.value()));
            }
            TypeSpec typeSpec = TypeSpec.classBuilder("DI" + element.getSimpleName())
                    .superclass(TypeName.get(typeElement.asType()))
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(bindViewMethodSpecBuilder.build())
                    .build();
            JavaFile javaFile = JavaFile.builder(getPackageName(typeElement), typeSpec).build();

            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return true;
    }

    private String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_7;
    }
}
  1. 使用DIActivity
@DIActivity
public class MainActivity extends Activity {
    @DIView(R.id.text)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DIMainActivity.bindView(this);
        textView.setText("Hello World!");
    }
}

實際上就是通過apt生成以下代碼

public final class DIMainActivity extends MainActivity {
 public static void bindView(MainActivity activity) {
   activity.textView = (android.widget.TextView) activity.findViewById(R.id.text);
 }
}

常用方法

常用Element子類
  1. TypeElement:類
  2. ExecutableElement:成員方法
  3. VariableElement:成員變量
通過包名和類名獲取TypeName

TypeName targetClassName = ClassName.get("PackageName", "ClassName");

通過Element獲取TypeName

TypeName type = TypeName.get(element.asType());

獲取TypeElement的包名

String packageName = processingEnv.getElementUtils().getPackageOf(type).getQualifiedName().toString();

獲取TypeElement的所有成員變量和成員方法

List<? extends Element> members = processingEnv.getElementUtils().getAllMembers(typeElement);

總結

推薦閱讀dagger2读规、dbflow罩旋、ButterKnife等基于apt的開源項目代碼罩缴。JavaPoet 也有很多例子可以學習。

Example代碼

https://github.com/taoweiji/DemoAPT

我們的開源項目推薦:

Android快速持久化框架:AptPreferences

AptPreferences是基于面向對象設計的快速持久化框架聂宾,目的是為了簡化SharePreferences的使用,減少代碼的編寫诊笤∠敌常可以非常快速地保存基本類型和對象讨跟。AptPreferences是基于APT技術實現(xiàn)纪他,在編譯期間實現(xiàn)代碼的生成鄙煤,根據不同的用戶區(qū)分持久化信息。

https://github.com/joyrun/AptPreferences

ActivityRouter路由框架:通過注解實現(xiàn)URL打開Activity

基于apt技術茶袒,通過注解方式來實現(xiàn)URL打開Activity功能馆类,并支持在WebView和外部瀏覽器使用,支持多級Activity跳轉弹谁,支持Bundle乾巧、Uri參數注入并轉換參數類型。

https://github.com/joyrun/ActivityRouter

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末预愤,一起剝皮案震驚了整個濱河市沟于,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌植康,老刑警劉巖旷太,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異销睁,居然都是意外死亡供璧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門冻记,熙熙樓的掌柜王于貴愁眉苦臉地迎上來睡毒,“玉大人,你說我怎么就攤上這事冗栗⊙莨耍” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵隅居,是天一觀的道長钠至。 經常有香客問我,道長胎源,這世上最難降的妖魔是什么棉钧? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮涕蚤,結果婚禮上宪卿,老公的妹妹穿的比我還像新娘。我一直安慰自己赞季,他們只是感情好愧捕,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著申钩,像睡著了一般次绘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天邮偎,我揣著相機與錄音管跺,去河邊找鬼。 笑死禾进,一個胖子當著我的面吹牛豁跑,可吹牛的內容都是我干的。 我是一名探鬼主播泻云,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼艇拍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了宠纯?” 一聲冷哼從身側響起卸夕,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎婆瓜,沒想到半個月后快集,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡廉白,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年个初,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猴蹂。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡院溺,死狀恐怖,靈堂內的尸體忽然破棺而出晕讲,到底是詐尸還是另有隱情覆获,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布瓢省,位于F島的核電站,受9級特大地震影響痊班,放射性物質發(fā)生泄漏勤婚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一涤伐、第九天 我趴在偏房一處隱蔽的房頂上張望馒胆。 院中可真熱鬧,春花似錦凝果、人聲如沸祝迂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽型雳。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間纠俭,已是汗流浹背沿量。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留冤荆,地道東北人朴则。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像钓简,于是被迫代替她去往敵國和親乌妒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內容