注解框架Butterknife解析

1.什么是注解
2.注解的分類
3.編譯時注解的原理
4.APT
5.創建項目及依賴
6.編碼實現
7.總結

我們首先了解一下什么是注解以及注解的核心原理,在掌握原理的前提下自己動手實現一個注解框架。通過代碼的編寫能夠對Butterknife底層實現有更加清楚的認識。

注解

注解在Java文檔中定義如下:

An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

翻譯一下,大概的意思是:

注解是一種元數據, 可以添加到java代碼中. 類、方法、變量、參數、包都可以被注解,注解對注解的代碼沒有直接影響。

注解的分類

  • <b>運行時注解:</b>指的是運行階段利用反射,動態獲取被標記的方法、變量等,如EvenBus。
  • <b>編譯時注解:</b>指的是程序在編譯階段會根據注解進行一些額外的處理,如ButterKnife。

運行時注解和編譯時注解,都可以理解為通過注解標識,然后進行相應處理。兩者的區別是:前者是運行時執行的,反射的使用會降低性能;后者是編譯階段執行的,通過生成輔助類實現效果。

運行時注解由于性能問題被一些人所詬病,所以本文主要講解編譯時注解的原理,并實現自己的Butterknife框架。

編譯時注解的原理

編譯時注解的核心原理依賴APT(Annotation Processing Tools)實現:

編譯時Annotation解析的基本原理是,在某些代碼元素上(如類型、函數、字段等)添加注解,在編譯時javac編譯器會檢查AbstractProcessor的子類,并且調用該類型的process函數,然后將添加了注解的所有元素都傳遞到process函數中,使得開發人員可以在編譯器進行相應的處理,例如,根據注解生成新的Java類,這也就是ButterKnife Dragger等開源庫的基本原理

那么APT又是什么呢?

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

下面以Butterknife為例:

public class MainActivity extends AppCompatActivity {

  @BindView(R.id.tv_main)
  TextView tvMain;

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

這是Butterknife最簡單的用法,我們只需要加上一個注解 @BindView并指定對應的Id就可以了,從而避免了findViewById(),那么它底層是怎么實現的呢?本篇文章重點不是介紹Butterknife的實現原理,所以這里只是簡單的說一下它底層的實現。這里我們只寫了一個MainActivity.java文件,編譯后我們查看一下class文件,我們會發現在MainActivity中多了一個內部類ViewBinder,其實Butterknife就是在這個內部類中關聯對應控件的,下面以偽代碼的形式簡單說明一個它底層實現的原理。

字節碼文件
public class MainActivity extends AppCompatActivity {

  @BindView(R.id.tv_main)
  TextView tvMain;

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

  /*
  * 當bind()方法被調用之后,tvMain就有對應的值了
  * */
  private static class ViewBinder{
       public static void bind(MainActivity activity){
           activity.tvMain = (TextView) activity.findViewById(R.id.tv_main);
       }
   }
}

大概的原理是這樣的:在編譯的時候如果某個類中使用了注解,Butterknife就會在其中“添加”一個內部類,在內部類中實現控件的關聯。我們知道編譯java源文件的工具是javac,其實在javac中有一個注解處理工具(依賴APT)用來編譯時掃描和處理的注解的工具。我們可以為特定的注解,注冊你自己的注解處理器,來實現自己的處理邏輯。

上面我們了解了基本原理,接下來我們實戰演練


項目結構

我們的項目結構如上圖所示:每個庫都有自己的實現功能,最中通過我們的項目依賴相應的庫來使用。

創建App

我們新建一個工程,因為我們要處理注解需要用到APT,所以在app中需要使用apt的插件
<b>github:</b>https://github.com/Aexyn/android-apt

關聯APT插件:
Step1: 在我們工程目錄下的build.gradle文件中添加如下代碼:
Step2: 在我們項目目錄下的build.gradle文件中添加如下代碼:

創建Java庫(定義注解)

inject-annotion

創建Android庫

inject

<b>關聯Java庫(inject-annotion)</b>

關聯java庫

創建Java庫(處理注解庫)

我們需要在編譯的時候根據注解創建新的類并添加到源文件中,所有需要引用幾個依賴。并且要關聯上一個Java庫

  • com.google.auto.service:auto-service:谷歌提供的Java 生成源代碼庫
  • com.squareup:javapoet:提供了各種 API 讓你用各種姿勢去生成 Java 代碼文件
  • com.google.auto:auto-common:生成代碼的庫
依賴

<b>全部創建完畢后,我們的工程目錄如下:</b>

項目目錄

關聯庫

讓我們的項目(app)去關聯注解庫

關聯庫

編寫代碼

1.定義注解:inject-annotion

/**
 * @Retention(RetentionPolicy.CLASS):編譯時被保留,在class文件中存在,但JVM將會忽略
 * @Target(ElementType.FIELD) :出現的位置(字段、枚舉的常量)
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

2.定義方法:inject

  • <b>InjectView.java</b>

    public class InjectView {
      public  static  void bind(Activity activity)
      {
          String clsName=activity.getClass().getName();
          try {
              //獲取內部類
              Class<?> viewBidClass=Class.forName(clsName+"$$ViewBinder");
              //創建內部類的實例
              ViewBinder viewBinder= (ViewBinder) viewBidClass.newInstance();
              viewBinder.bind(activity);//綁定頁面
          } catch (ClassNotFoundException e) {
              e.printStackTrace();
          } catch (InstantiationException e) {
              e.printStackTrace();
          } catch (IllegalAccessException e) {
              e.printStackTrace();
          }
      }
    }
    
  • <b>ViewBinder.java</b>

    public interface ViewBinder <T>{
      void  bind(T tartget);
    }
    

3.處理注解:inject-compiler

  • <b>FieldViewBinding.java</b>

    /**
     * 注解信息封裝類
     */
    public class FieldViewBinding {
    
        private String name;// 字段的名字 textview
        private TypeMirror type ;// 字段的類型 --->TextView
        private int resId;// 對應的id R.id.textiew
    
        public FieldViewBinding(String name, TypeMirror type, int resId) {
            this.name = name;
            this.type = type;
            this.resId = resId;
        }
    
        public String getName() {
            return name;
        }
    
        public TypeMirror getType() {
            return type;
        }
    
        public int getResId() {
            return resId;
        }
    }
    
  • <b>BindViewProcessor.java</b>


    /**
     * 注解處理類
     */
    @AutoService(Processor.class)
    public class BindViewProcessor extends AbstractProcessor {
    
        private Elements elementUtils;
        private Types typeUtils;
        private Filer filer;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            elementUtils = processingEnvironment.getElementUtils();
            typeUtils = processingEnvironment.getTypeUtils();
            filer = processingEnvironment.getFiler();
        }
    
        /* 設置處理那些注解 */
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> types = new LinkedHashSet<>();
            types.add(BindView.class.getCanonicalName());
            return types;
        }
    
        /* 設置支持的JDk版本 */
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            Map<TypeElement, List<FieldViewBinding>> targetMap = new HashMap<>();
            for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
    
                TypeElement enClosingElement = (TypeElement) element.getEnclosingElement();
                List<FieldViewBinding> list = targetMap.get(enClosingElement);
                if (list == null) {
                    list = new ArrayList<>();
                    targetMap.put(enClosingElement, list);
                }
                int id = element.getAnnotation(BindView.class).value();
                String fieldName = element.getSimpleName().toString();
                TypeMirror typeMirror = element.asType();
                FieldViewBinding fieldViewBinding = new FieldViewBinding(fieldName, typeMirror, id);
                list.add(fieldViewBinding);
            }
            for (Map.Entry<TypeElement, List<FieldViewBinding>> item : targetMap.entrySet()) {
                List<FieldViewBinding> list = item.getValue();
    
                if (list == null || list.size() == 0) {
                    continue;
                }
                TypeElement enClosingElement = item.getKey();
                String packageName = getPackageName(enClosingElement);
                String complite = getClassName(enClosingElement, packageName);
                ClassName className = ClassName.bestGuess(complite);
                ClassName viewBinder = ClassName.get("com.example.inject", "ViewBinder");
                TypeSpec.Builder result = TypeSpec.classBuilder(complite + "$$ViewBinder")
                        .addModifiers(Modifier.PUBLIC)
                        .addTypeVariable(TypeVariableName.get("T", className))
                        .addSuperinterface(ParameterizedTypeName.get(viewBinder, className));
    
                MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                        .addModifiers(Modifier.PUBLIC)
                        .returns(TypeName.VOID)
                        .addAnnotation(Override.class)
                        .addParameter(className, "target", Modifier.FINAL);
                for (int i = 0; i < list.size(); i++) {
                    FieldViewBinding fieldViewBinding = list.get(i);
                    String pacckageNameString = fieldViewBinding.getType().toString();
                    ClassName viewClass = ClassName.bestGuess(pacckageNameString);
                    methodBuilder.addStatement
                            ("target.$L=($T)target.findViewById($L)", fieldViewBinding.getName()
                                    , viewClass, fieldViewBinding.getResId());
                }
                result.addMethod(methodBuilder.build());
    
                try {
                    JavaFile.builder(packageName, result.build())
                            .addFileComment("auto create make")
                            .build().writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return false;
        }
    
        /* 獲取類名 */
        private String getClassName(TypeElement enClosingElement, String packageName) {
            int packageLength = packageName.length() + 1;
            return enClosingElement.getQualifiedName().toString().substring(packageLength).replace(".", "$");
        }
    
        /* 獲取包名 */
        private String getPackageName(TypeElement enClosingElement) {
            return elementUtils.getPackageOf(enClosingElement).getQualifiedName().toString();
        }
    }
    

測試

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.text)
    TextView textview;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectView.bind(this);
        if(textview == null){
            Toast.makeText(this,"注解處理失敗",Toast.LENGTH_SHORT).show();
        }else{
            textview.setText("世界你好!");
        }
    }
}

用法跟Butterknife一樣,頁面上有一個TextView,使用注解關聯,如果關聯失敗,彈出提示信息。否則設置顯示為“世界你好!”。

演示

總結

通過上述代碼的編寫,我們能更加對Butterknife的底層實現有更清楚的認識,雖然只是實現了綁定View。在編譯時javac編譯器會檢查AbstractProcessor的子類,并且調用該類型的process函數,然后將添加了注解的所有元素都傳遞到process函數中,我們需要繼承該類重寫此方法我們就能獲取我們想要處理的注解。在里面做具體的綁定邏輯。
AutoService注解處理器是Google開發的,用來生成META-INF/services/javax.annotation.processing.Processor文件的。我們可以在注解處理器中使用注解。非常方便。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容