關於Apt註解實踐與總結【包含20篇部落格】

楊充發表於2019-02-24

YCApt關於apt方案實踐與總結

目錄介紹

  • 00.註解系列部落格彙總
  • 01.什麼是apt
  • 02.annotationProcessor和apt區別
  • 03.專案目錄結構
  • 04.該案例作用
  • 05.使用說明
  • 06.編譯期註解生成程式碼[點選事件案例]
  • 07.執行期註解案例[setContentView案例]
  • 08.使用註解替代列舉
  • 09.使用註解搭建路由[綜合案例]
    • 9.1 搭建路由條件
    • 9.2 通過註解去實現路由跳轉
    • 9.3 自定義路由Processor編譯器
    • 9.4 利用apt生成路由對映檔案
    • 9.5 路由框架的設計
    • 9.6 路由引數的傳遞和接收
    • 9.7 為何需要依賴注入
    • 9.8 Activity屬性注入
    • 9.9 路由開源庫的使用

關於apt實踐與總結開源庫地址

github.com/yangchong21…

00.註解系列部落格彙總

0.1 註解基礎系列部落格

  • 01.Annotation註解詳細介紹
    1.Annotation庫的簡單介紹
    2.@Nullable和@NonNull
    3.資源型別註釋
    4.型別定義註釋
    5.執行緒註釋
    6.RGB顏色紙註釋
    7.值範圍註釋
    8.許可權註釋
    9.重寫函式註釋
    10.返回值註釋
    11.@Keep註釋
    12.@SuppressWarnings註解
    13.其他
    複製程式碼
  • 02.Dagger2深入分析,待更新
  • 03.註解詳細介紹
    • 什麼是註解,註解分類有哪些?自定義註解分類?執行註解案例展示分析,以一個最簡單的案例理解註解……使用註解替代列舉,使用註解限定型別
  • 04.APT技術詳解
    • 什麼是apt?理解註解處理器的作用和用途……android-apt被替代?annotationProcessor和apt區別? 什麼是jack編譯方式?
  • 06.自定義annotation註解
    • @Retention的作用?@Target(ElementType.TYPE)的解釋,@Inherited註解可以被繼承嗎?Annotation裡面的方法為何不能是private?
  • 07.註解之相容kotlin
    • 後期更新
  • 08.註解之處理器類Processor
    • 處理器類Processor介紹,重要方法,Element的作用,修飾方法的註解和ExecutableElement,瞭解修飾屬性、類成員的註解和VariableElement……
  • 10.註解遇到問題和解決方案
    • 無法引入javax包下的類庫,成功執行一次,修改程式碼後再執行就報錯
  • 11.註解代替列舉
    • 在做記憶體優化時,推薦使用註解代替列舉,因為列舉佔用的記憶體更高,如何說明列舉佔用記憶體高呢?這是為什麼呢?
  • 12.註解練習案例開原始碼
    • 註解學習小案例,比較系統性學習註解並且應用實踐。簡單應用了執行期註解,通過註解實現了setContentView功能;簡單應用了編譯器註解,通過註解實現了防暴力點選的功能,同時支援設定時間間隔;使用註解替代列舉;使用註解一步步搭建簡單路由案例。結合相應的部落格,在來一些小案例,從此應該對註解有更加深入的理解……

0.2 註解系列部落格問題答疑

  • 13.0.0.1 什麼是註解?系統內建的標準註解有哪些?SuppressWarnings用過沒?Android中提供了哪些與執行緒相關的註解?
  • 13.0.0.2 什麼是apt?apt的難點和優勢?什麼是註解處理器?抽象處理器中四個方法有何作用?annotationProcessor和apt區別?
  • 13.0.0.3 註解是怎麼分類的?自定義註解又是怎麼分類的?執行期註解原理是什麼?實際註解案例有哪些?
  • 13.0.0.4 在自定義註解中,Annotation裡面的方法為何不能是private?Annotation裡面的方法引數有哪些?
  • 13.0.0.5 @Inherited是什麼意思?註解是不可以繼承的,這是為什麼?註解的繼承這個概念該如何理解?
  • 13.0.0.6 什麼是依賴注入?依賴注入案例舉例說明,有哪些方式,具備什麼優勢?依賴查詢和依賴注入有什麼區別?
  • 13.0.0.7 路由框架為何需要依賴注入,不用的話行不行?路由用什麼方式注入,這些注入方式各具何特點,為何選擇註解注入?
  • 13.0.0.8 實際開發中使用到註解有哪些,使用註解替代列舉?如何通過註解限定傳入的型別?為何說列舉損耗效能?

01.什麼是apt

  • 什麼是apt
    • APT,就是Annotation Processing Tool的簡稱,就是可以在程式碼編譯期間對註解進行處理,並且生成Java檔案,減少手動的程式碼輸入。註解我們平時用到的比較多的可能會是執行時註解,比如大名鼎鼎的retrofit就是用執行時註解,通過動態代理來生成網路請求。編譯時註解平時開發中可能會涉及的比較少,但並不是說不常用,比如我們經常用的輪子Dagger2, ButterKnife, EventBus3 都在用,所以要緊跟潮流來看看APT技術的來龍去脈。
  • 編譯時註解。
    • 也有人叫它程式碼生成,其實他們還是有些區別的,在編譯時對註解做處理,通過註解,獲取必要資訊,在專案中生成程式碼,執行時呼叫,和直接執行手寫程式碼沒有任何區別。而更準確的叫法:APT - Annotation Processing Tool
  • 大概原理
    • Java API 已經提供了掃描原始碼並解析註解的框架,開發者可以通過繼承 AbstractProcessor 類來實現自己的註解解析邏輯。APT 的原理就是在註解了某些程式碼元素(如欄位、函式、類等)後,在編譯時編譯器會檢查 AbstractProcessor 的子類,並且自動呼叫其 process() 方法,然後將新增了指定註解的所有程式碼元素作為引數傳遞給該方法,開發者再根據註解元素在編譯期輸出對應的 Java 程式碼

02.annotationProcessor和apt區別

  • annotationProcessor和apt區別
    • Android 官方的 annotationProcessor 同時支援 javac 和 jack 編譯方式,而 android-apt 只支援 javac 方式。當然,目前 android-apt 在 Android Gradle 外掛 2.2 版本上面仍然可以正常執行,如果你沒有想支援 jack 編譯方式的話,可以繼續使用 android-apt。
    • 目前比如一些常用框架dagger2,butterKnife,ARouter等,都支援annotationProcessor
  • 什麼是jack編譯方式?
    • Jack (Java Android Compiler Kit)是新的Android 編譯工具,從Android 6.0 開始加入,替換原有的編譯工具,例如javac, ProGuard, jarjar和 dx。它主要負責將java程式碼編譯成dex包,並支援程式碼壓縮,混淆等。
  • Jack工具的主要優勢
    • 完全開放原始碼,原始碼均在AOSP中,合作伙伴可貢獻原始碼
    • 加快編譯原始碼,Jack 提供特殊的配置,減少編譯時間:pre-dexing, 增量編譯和Jack編譯伺服器.
    • 支援程式碼壓縮,混淆,重打包和multidex,不在使用額外單獨的包,例如ProGuard。

03.專案目錄結構

  • 專案目錄結構如圖:
    • app:Demo
    • AptAnnotation:java Library主要放一些專案中需要用到的自定義註解及相關程式碼
    • AptApi:Android Library. 是我們真正對外發布並交由第三方使用的庫,它引用了apt-jar包
    • AptCompiler:java Library主要是應用apt技術處理註解,生成相關程式碼或者相關原始檔,是核心所在。

04.該案例作用

  • 前期僅僅是為了學習,同時先讓demo執行起來,雖然網上很多講解apt的部落格寫的很詳細,但是還是有必要結合實際案例練習一下。
  • 使用apt實現點選事件【編譯期註解生成程式碼】
    • 在一定時間內,按鈕點選事件只能執行一次。未到指定時間,不執行點選事件。
  • 使用apt實現setContentView功能【執行期註解案例】
    • 使用簡單的註解,便可以設定佈局,等效於setContentView(R.layout.activity_main)
  • 使用apt實現路由【綜合型案例】
    • 比較全面的介紹從零起步,一步一步封裝簡易的路由開源庫。一共用10篇部落格記錄了大部分的過程,想要更加深入瞭解,歡迎clone該專案。

05.使用說明

  • 如下所示
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化OnceClick,並設定點選事件間隔是2秒
        OnceInit.once(this,2000);
    }
    
    @OnceClick(R.id.tv_1)
    public void Click1(){
        Log.d("tag--------------------","tv_1");
    }
    複製程式碼

06.編譯期註解生成程式碼

  • 如下所示,在app/build/generated/source/apt/debug/MainActivity$$_Once_Proxy目錄下
    // 編譯生成的程式碼,不要修改
    // 更多內容:https://github.com/yangchong211
    package com.ycbjie.ycapt;
    
    import android.view.View;
    import com.ycbjie.api.Finder;
    import com.ycbjie.api.AbstractInjector;
    
    public class MainActivity$$_Once_Proxy<T extends MainActivity> implements AbstractInjector<T> {
    
        public long intervalTime;
    
        @Override
        public void setIntervalTime(long time) {
            intervalTime = time;
        }
    
        @Override
        public void inject(final Finder finder, final T target, Object source) {
            View view;
            view = finder.findViewById(source, 2131165325);
            if(view != null){
                view.setOnClickListener(new View.OnClickListener() {
                long time = 0L;
                @Override
                public void onClick(View v) {
                    long temp = System.currentTimeMillis();
                    if (temp - time >= intervalTime) {
                        time = temp;
                        target.Click1();
                    }
                }});
            }
            view = finder.findViewById(source, 2131165326);
            if(view != null){
                view.setOnClickListener(new View.OnClickListener() {
                long time = 0L;
                @Override
                public void onClick(View v) {
                    long temp = System.currentTimeMillis();
                    if (temp - time >= intervalTime) {
                        time = temp;
                        target.Click2(v);
                    }
                }});
            }
      }
    
    }
    複製程式碼

07.執行期註解案例

  • 首先先定義自定義註解
    //@Retention用來修飾這是一個什麼型別的註解。這裡表示該註解是一個執行時註解。
    @Retention(RetentionPolicy.RUNTIME)
    //@Target用來表示這個註解可以使用在哪些地方。比如:類、方法、屬性、介面等等。
    //這裡ElementType.TYPE 表示這個註解可以用來修飾:Class, interface or enum declaration。
    //當你用ContentView修飾一個方法時,編譯器會提示錯誤。
    @Target({ElementType.TYPE})
    //這裡的interface並不是說ContentView是一個介面。
    //就像申明類用關鍵字class。申明列舉用enum。申明註解用的就是@interface。
    public @interface ContentView {
        //返回值表示這個註解裡可以存放什麼型別值。
        int value();
    }
    複製程式碼
  • 然後需要在activity中做註解解析
    @SuppressLint("Registered")
    public class ContentActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            //註解解析
            //遍歷所有的子類
            for (Class c = this.getClass(); c != Context.class; c = c.getSuperclass()) {
                assert c != null;
                //找到修飾了註解ContentView的類
                ContentView annotation = (ContentView) c.getAnnotation(ContentView.class);
                if (annotation != null) {
                    try {
                        //獲取ContentView的屬性值
                        int value = annotation.value();
                        //呼叫setContentView方法設定view
                        this.setContentView(value);
                    } catch (RuntimeException e) {
                        e.printStackTrace();
                    }
                    return;
                }
            }
        }
    }
    複製程式碼
  • 關於如何使用,注意你寫的Activity需要實現ContentActivity,才能讓註解生效
    @ContentView(R.layout.activity_four)
    public class FourActivity extends ContentActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            findViewById(R.id.tv_1).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(FourActivity.this,"執行期註解",Toast.LENGTH_SHORT).show();
                }
            });
        }
    }
    複製程式碼

09.使用註解搭建路由[綜合案例]

  • 9.1 ARouter路由解析
    • 比較詳細地分析了阿里路由庫
  • 9.1 搭建路由條件
    • 為何需要路由?實現路由方式有哪些,這些方式各有何優缺點?使用註解實現路由需要具備的條件以及簡單原理分析……
  • 9.2 通過註解去實現路由跳轉
    • 自定義Router註解,Router註解裡有path和group,這便是仿照ARouter對路由進行分組。然後看看註解生成的程式碼,手寫路由跳轉程式碼。
  • 9.3 自定義路由Processor編譯器
    • Processor介紹,重要方法,Element的作用,修飾方法的註解和ExecutableElement
  • 9.4 利用apt生成路由對映檔案
    • 在Activity類上加上@Router註解之後,便可通過apt來生成對應的路由表,那麼究竟是如何生成的程式碼呢?
    • 在元件化開發中,有多個module,為何要在build.gradle配置moduleName,又是如何通過程式碼拿到module名稱?
    • process處理方法如何生成程式碼的,又是如何寫入具體的路徑,寫入檔案的?
    • 看完這篇文章,應該就能夠理解上面這些問題呢!
  • 9.5 路由框架的設計和初始化
    • 編譯期是在你的專案編譯的時候,這個時候還沒有開始打包,也就是你沒有生成apk呢!路由框架在這個時期根據註解去掃描所有檔案,然後生成路由對映檔案。這些檔案都會統一打包到apk裡,app執行時期做的東西也不少,但總而言之都是對對映資訊的處理,如執行執行路由跳轉等。那麼如何設計框架呢?
    • 生成的註解程式碼,又是如何把這些路由對映關係拿到手,或者說在什麼時候拿到手比較合適?為何註解需要進行初始化操作?
    • 如何得到得到路由表的類名,如何得到所有的routerAddress---activityClass對映關係?
  • 9.6 路由框架設計注意要點
    • 需要注意哪些要點?
  • 9.7 為何需要依賴注入
    • 有哪些注入的方式可以解耦,你能想到多少?路由框架為何需要依賴注入?路由為何用註解進行依賴注入,而不是用反射方式注入,或者通過構造方法注入,或者通過介面方式注入?
  • 9.8 Activity屬性注入
    • 在跳轉頁面時,如何傳遞intent引數,或者如何實現跳轉回撥處理邏輯?
  • 9.9 路由開源庫的使用
    • 不帶引數直接跳轉
      @Router(path = Path.six)
      public class SixActivity extends AppCompatActivity {
      
          @Override
          protected void onCreate(@Nullable Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_six);
          }
      
      }
      
      ARouter.getsInstance().build(Path.six)
                  .navigation(MainActivity.this, new NavigationCallback() {
              @Override
              public void onFound(Postcard postcard) {
                  Log.e("NavigationCallback","找到跳轉頁面");
              }
      
              @Override
              public void onLost(Postcard postcard) {
                  Log.e("NavigationCallback","未找到");
              }
      
              @Override
              public void onArrival(Postcard postcard) {
                  Log.e("NavigationCallback","成功跳轉");
              }
          });
      複製程式碼
    • 帶引數跳轉
      @Router(path = Path.five)
      public class FiveActivity extends AppCompatActivity {
      
          @Extra
          String title;
      
          @Override
          protected void onCreate(@Nullable Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_five);
              //新增這行程式碼,實際上就是自動生成了下面獲取引數值的程式碼
              ARouter.getsInstance().inject(this);
              //如果不新增插入註解,則可以直接用下面的程式碼。
              //Intent intent = getIntent();
              //String title = intent.getStringExtra("title");
              Toast.makeText(this, "title=" + title, Toast.LENGTH_SHORT).show();
          }
      
      }
      
      Bundle bundle = new Bundle();
      bundle.putString("title","標題-------------");
      ARouter.getsInstance()
              .build(Path.five)
              .withBundle(bundle)
              .navigation();
      複製程式碼
    • 路由註解生成的程式碼位置
      • image

10.其他說明

00.參考案例

01.關於部落格彙總連結

02.關於我的部落格

關於apt實踐與總結開源庫地址

github.com/yangchong21…

相關文章