手把手教你實現(xiàn)仿ButterKnife依賴注入框架

如需轉(zhuǎn)載請評論或簡信蹈丸,并注明出處,未經(jīng)允許不得轉(zhuǎn)載

目錄

前言

目前Android社區(qū)涌現(xiàn)出越來越多的IOC框架,ButterKnife逻杖、Dagger2奋岁、EventBus3,這些框架往往能有效幫助我們簡化代碼荸百,模塊解耦闻伶,相信很多人也或多或少的用過其中一些框架。但是够话,有沒有人想過這些框架的內(nèi)部原理都是怎么樣的呢蓝翰?本文就從ButterKnife入手,手把手教你實現(xiàn)一個仿ButterKnife的IOC框架

知識準備

Annotation

我們知道annotation有三個保留級別

  • RetentionPolicy.SOURCE 注解只在源碼階段保留女嘲,在編譯器進行編譯時它將被丟棄忽視畜份。
  • RetentionPolicy.CLASS 注解只被保留到編譯進行的時候,它并不會被加載到 JVM 中欣尼。
  • RetentionPolicy.RUNTIME 注解可以保留到程序運行的時候爆雹,它會被加載進入到 JVM 中,所以在程序運行時可以獲取到它們

annotation實際上就是一個標簽愕鼓,單獨存在的時候沒有任何實際意義钙态。為了便于理解,這里再延伸一下另一個詞語—Hook菇晃,Hook的英文解釋是鉤子册倒。依我的理解,注解實際上就像這個鉤子磺送,勾住”類“剩失、”方法“、”字段“册着,為了后續(xù)想對這些被“勾住”的東西做一些操作提供了方便

更多關(guān)于注解的知識可以自己查看相關(guān)資料拴孤,這里就不多做介紹了

AnnotationProcessor

annotationProcessorAPT工具中的一種,他是Google開發(fā)的內(nèi)置框架甲捏,不需要引入演熟,可以直接在build.gradle文件中使用,如下:

  dependencies {
    annotationProcessor project(':compiler') 
  }
APT的工作

APT簡單的說就是注解處理器司顿,主要作用是可以編寫一些規(guī)則在編譯期間找出項目中的特定注解芒粹,以注解中的參數(shù)作為輸入,生成文件.java文件作為輸出大溜。注意化漆,這里的重點是生成.java文件,而不能修改已經(jīng)存在的Java類钦奋,例如不能向已有的類中添加方法

開始ButterKnife之旅

ButterKnife使用簡單介紹

先來看一下ButterKnife的常規(guī)使用方法座云,我們可以在Activity中的任意方法中直接使用這個textView疙赠,省去了findViewById的操作

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.txt_test)
    TextView textView;
    @BindView(R.id.btn_test)
    Button button

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //調(diào)用框架方法
        ButterKnife.bind(this);
        //業(yè)務(wù)代碼
        textView.setText("Hello World");
        button.setOnClickListenr(new OnClickListener(View view){})
    }
}

問題分析

我們先不去看源碼,我們可以設(shè)想一下ButterKnife.bind(this)做了什么事情朦拖,我認為大概是像下面這樣:

public class ButterKnife{
        public static void bind(MainActivity activity){
            activity.textView = activity.findViewById(R.id.txt_test);
            activity.button = activity.findViewById(R.id.btn_test)
        }
}

接下來會遇到幾個問題:

問題一:我們?nèi)绾螌⒖丶囊煤涂丶膇d關(guān)聯(lián)起來圃阳?我想我們應(yīng)該很快有答案了,@BindView注解其實就是起到了關(guān)聯(lián)的作用

問題二:前面說到璧帝,APT只能生成.java文件捍岳,而不能直接在方法中插入代碼。那么怎么辦呢睬隶,我們可以通過APT生成.java文件锣夹,然后在運行時通過反射調(diào)用它,如下所示

  1. 創(chuàng)建一個接口(接口是一種約束),這里用到了泛型苏潜,因為我們要適用所有Activity
public interface BindAdapter<T> {
    void bind(T activity);
}
  1. 我們通過APT生成BindAdapterImp類晕城,實現(xiàn)BindAdapter接口
public class BindAdapterImp implement BindAdapter<MainActivity>{
        public void bind(MainActivity activity) {
        activity.textView = activity.findViewById(R.id.txt_test);
            activity.button = activity.findViewById(R.id.btn_test)
    }
}
  1. ButterKnifebind()里,通過反射生成BindAdapterImp窖贤,在調(diào)用其bind()
public class ButterKnife {
   private static final String CLASS_NAME = "";
    public static void bind(Activity activity){
        //反射拿到class
        Class<?> bindAdapterClass = Class.forName(CLASS_NAME);
        //通過class拿到BindAdapterImp對象
        BindAdapterImp adapter = (BindAdapterImp) bindAdapterClass.newInstance();
        //調(diào)用bind
        adapter.bind(activituy)
        }
}

問題三:問題又來了砖顷,我們把生成的BindAdapterImp類放到哪個包下面能讓所有類都能調(diào)用到呢?答案是內(nèi)部類赃梧!

內(nèi)部類在編譯期間生成的實際上是單獨一個.java文件

所以我們會為每一個調(diào)用了ButterKnife.bind()Activity生成一個BindAdapterImp內(nèi)部類滤蝠,根據(jù)這個思路,我們對上面的代碼進行了一些優(yōu)化授嘀,如下所示

//這里的 MainActivity 是根據(jù)不同的Activity進行變化的
public class MainActivity$BindAdapterImp implement BindAdapter<MainActivity>{
        public void bind(MainActivity activity) {
        activity.textView = activity.findViewById(R.id.txt_test);
        activity.button = activity.findViewById(R.id.btn_test)
    }
}
public class ButterKnife {
    private static final String SUFFIX = "$BindAdapterImp";
        //做了一個緩存物咳,只有第一次bind時才通過反射創(chuàng)建對象
    static Map<Class, BindAdapter> mBindCache = new HashMap();

    public static void bind(Activity target){
        BindAdapter bindAdapter;
        if (mBindCache.get(target) != null) {
            //如果緩存中有activity,從緩存中取
            bindAdapter = mBindCache.get(target);
        } else {
            //緩存中沒有蹄皱,創(chuàng)建一個
            String adapterClassName = target.getClass().getName() + SUFFIX;
            Class<?> aClass = Class.forName(adapterClassName);
            bindAdapter = (BindAdapter) aClass.newInstance();
            mBindCache.put(aClass, bindAdapter);
        }
                //調(diào)用bind
        bindAdapter.bind(target);
    }   
}

Tips:從上面的代碼我們發(fā)現(xiàn)览闰,為了盡量避免反射的性能消耗,ButterKnife內(nèi)部會有一個緩存巷折,這是一種典型的空間換時間的做法压鉴。在做內(nèi)存優(yōu)化的時候,我們往往會提到盡量少用ButterKnife這種依賴注入框架其實就是這個原因锻拘。這個還需要大家對各自項目作出一個折中的選擇

最后油吭,我們面臨的問題實際上就是如何在編譯期生成上面BindAdapterImp類,接下來跟著我一步步來吧

創(chuàng)建一個項目

注意如果這里勾選了androidx署拟,而且想要使用Kotlin婉宰,需要用kapt取代AnnotationProcessor
關(guān)于androidx與kotlin兼容問題具體參考:
當ButterKnife8.8.1碰到AndroidX怎么辦
看懂編譯注解annotationProcessor和kapt

創(chuàng)建一個注解類

新建一個java module,命名為annotation

創(chuàng)建編譯器注解類@BindView推穷,這是一個屬性注解心包,只有在編譯期有效,經(jīng)過編譯后馒铃,注解信息會被丟棄蟹腾,不會保留到編譯好的class文件里

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

創(chuàng)建AnnotationProcessor

新建一個java module痕惋,命名為processor

創(chuàng)建注解處理器,在編譯期間去掃描@BindView所標注的屬性

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.geekholt.annotation.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class GeekKnifeProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}
  • @AutoService(Processor.class):向javac注冊我們這個自定義的注解處理器岭佳,這樣,在javac編譯時萧锉,才會調(diào)用到我們這個自定義的注解處理器方法
  • @SupportedAnnotationTypes():表示我們這個注解處理器所要處理的注解
  • @SupportedSourceVersion():代表JDK版本號珊随,這里是代表java8
  • init():初始化時會自動被調(diào)用,并傳入processingEnvironment參數(shù)柿隙,通過該參數(shù)可以獲取到很多有用的工具類: Elements , Types , Filer 等等
  • process()AnnotationProcessor掃描出的結(jié)果會存儲進roundEnvironment中叶洞,可以從中獲取注解所標注的內(nèi)容信息
  • ProcessingEnvironment
/**用于提供工具類**/
public interface ProcessingEnvironment {
        //返回注解處理器的配置參數(shù)
    Map<String, String> getOptions();
  
        //Message用來報告錯誤,警告和其他提示信息
    Messager getMessager();
  
        //Filer用于創(chuàng)建新的源文件禀崖,class文件或輔助文件(可以用JavaPoet簡化創(chuàng)建文件操作)
    Filer getFiler();
  
        //Elements包含用于操作Element的工具方法
    Elements getElementUtils();
  
        //Types包含用于操作TypeMirror的工具方法
    Types getTypeUtils();
  
        //返回Java版本
    SourceVersion getSourceVersion();
  
        //返回當前語言環(huán)境或者null(沒有語言環(huán)境)
    Locale getLocale();
}
  • RoundEnvironment
/**用于獲取注解所標注的內(nèi)容信息**/
public interface RoundEnvironment {
    boolean processingOver();
        
    //返回上一輪注解處理器是否產(chǎn)生錯誤
    boolean errorRaised();

    //返回上一輪注解處理器生成的根元素
    Set<? extends Element> getRootElements();

    //返回包含指定注解類型的元素的集合
    Set<? extends Element> getElementsAnnotatedWith(TypeElement var1);
        
    //返回包含指定注解類型的元素的集合
    Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> var1);
}
  • Element

Element代表一個靜態(tài)的衩辟,語言級別的構(gòu)件,對于Java源文件來說波附,Element代表程序元素:包艺晴,類,方法都是一種程序元素

VariableElement:代表一個字段掸屡,枚舉常量封寞,方法或者構(gòu)造方法的參數(shù),局部變量及異常參數(shù)等元素

PackageElement:代表包元素

TypeElement:代表類或接口元素

ExecutableExement:代表方法仅财,構(gòu)造函數(shù)狈究,類或接口的初始化代碼塊等元素,也包括注解類型元素

  • TypeMirror

TypeMirror代表java語言中的類型盏求。Types包括基本類型抖锥、聲明類型(類類型和接口類型)、數(shù)組碎罚、類型變量和空類型磅废。 也代表通配類型參數(shù),可執(zhí)行文件的簽名和返回類型等荆烈。TypeMirror類中最重要的是getKind()方法还蹲, 該方法返回TypeKind類型

簡單來說,Element代表源代碼耙考,TypeElement代表的是源碼中的類型元素谜喊,比如類。雖然我們可以從TypeElement中獲取類名倦始, 但是TypeElement中不包含類本身的信息斗遏,比如它的父類,要想獲取這信息需要借助TypeMirror鞋邑,可以通過Element中的asType() 獲取元素對應(yīng)的TypeMirror

創(chuàng)建BindAdapter接口

新建一個android module诵次,命名為butterknife

創(chuàng)建BindAdapter接口

package com.geekholt.butterknife;

public interface BindAdapter<T> {
    void bind(T activity);
}

處理依賴關(guān)系

  • app module
compileOnly project(':annotation')
annotationProcessor project(':processor')
api project(':butterknife')
  • processor module
api project(':annotation')

編寫AnnotationProcessor

基本工作都已經(jīng)做好了账蓉,我們的目標也已經(jīng)很明確了,我們最終想要生成的就是像下面這樣一個文件

package com.geekholt.geekknife_example;

import com.geekholt.geekknife.adapter.BindAdapter;

public class MainActivity$BindAdapterImp implement BindAdapter<MainActivity>{
        public void bind(MainActivity activity) {
                activity.textView = activity.findViewById(R.id.txt_test);
            activity.button = activity.findViewById(R.id.btn_test)
    }
}

我們需要獲取哪些內(nèi)容呢逾一?

  • 包名

  • 注解所在的類的類名(Activity名)

  • 注解的成員變量名(控件名)

  • 注解的元數(shù)據(jù)(資源Id)

所以铸本,最終完成后的AnnotationProcessor就是下面這樣,獲取到我們需要的內(nèi)容后遵堵,生成java文件箱玷,邏輯其實非常簡單,只是相關(guān)的API不是很常用陌宿,可能需要熟悉一下

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.geekholt.annotation.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ButterKnifeProcessor extends AbstractProcessor {

    private Filer mFiler;
    private Messager mMessager;
    private Elements mElementUtils;

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        for (Element element : bindViewElements) {
            //1.獲取包名
            PackageElement packageElement = mElementUtils.getPackageOf(element);
            String packName = packageElement.getQualifiedName().toString();
            print(String.format("package = %s", packName));

            //2.注解所在的類的類名
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String className = enclosingElement.getSimpleName().toString();
            print(String.format("enclosindClass = %s", enclosingElement));


            //因為BindView只作用于filed锡足,所以這里可直接進行強轉(zhuǎn)
            VariableElement bindViewElement = (VariableElement) element;
            //3.獲取注解的成員變量名
            String fieldName = bindViewElement.getSimpleName().toString();

            //4.獲取注解元數(shù)據(jù)
            BindView bindView = element.getAnnotation(BindView.class);
            int id = bindView.value();
            print(String.format("%s = %d", fieldName, id));

            //4.生成文件
            createFile(packName, className, fieldName, id);
            return true;
        }
        return false;
    }

  
    /**創(chuàng)建文件**/
    private void createFile(String packName, String className, String fieldName, int id) {
        try {
            String newClassName = className + "$BindAdapterImp";
            JavaFileObject jfo = mFiler.createSourceFile(packName + "." + newClassName, new Element[]{});
            Writer writer = jfo.openWriter();
            writer.write("package " + packName + ";");
            writer.write("\n\n");
            writer.write("import com.geekholt.butterknife.BindAdapter;");
            writer.write("\n\n\n");
            writer.write("public class " + newClassName + " implements BindAdapter<" + className + "> {");
            writer.write("\n\n");
            writer.write("public void bind(" + className + " target) {");
            writer.write("target." + fieldName + " = target.findViewById(" + id + ");");
            writer.write("\n");
            writer.write("  }");
            writer.write("\n\n");
            writer.write("}");
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

        /**打印編譯期間的日志**/
    private void print(String msg) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, msg);
    }


}

創(chuàng)建文件推薦使用javapoet:https://github.com/square/javapoet

rebuild一下項目,在相關(guān)目錄下就可以看到我們想要的文件就已經(jīng)成功生成了

反射調(diào)用生成的代碼

接下來的內(nèi)容其實我們一開始就已經(jīng)說過了壳坪,我們需要在運行時通過反射調(diào)用我們編譯期生成的類

butterKnife module下創(chuàng)建ButterKnife

public class ButterKnife {
    private static final String SUFFIX = "$BindAdapterImp";
    //做了一個緩存舶得,只有第一次bind時才通過反射創(chuàng)建對象
    static Map<Class, BindAdapter> mBindCache = new HashMap();

    public static void bind(Activity target) {
        BindAdapter bindAdapter = null;
        if (mBindCache.get(target) != null) {
            //如果緩存中有activity,從緩存中取
            bindAdapter = mBindCache.get(target);
        } else {
            //緩存中沒有爽蝴,創(chuàng)建一個
            try {
                String adapterClassName = target.getClass().getName() + SUFFIX;
                Class<?> aClass = Class.forName(adapterClassName);
                bindAdapter = (BindAdapter) aClass.newInstance();
                mBindCache.put(aClass, bindAdapter);
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
        //調(diào)用bind
        if (bindAdapter != null) {
            bindAdapter.bind(target);
        }
    }
}

在我們的MainActivity中調(diào)用ButterKnife.bind(this)

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.txt_main)
    TextView textView;

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

運行一下項目沐批,看,“ButterKnife”就順利工作了蝎亚!是不是比想象的簡單呢珠插!

運行結(jié)果

項目完整地址

https://github.com/Geekholt/ButterKnife

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市颖对,隨后出現(xiàn)的幾起案子捻撑,更是在濱河造成了極大的恐慌,老刑警劉巖缤底,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件顾患,死亡現(xiàn)場離奇詭異,居然都是意外死亡个唧,警方通過查閱死者的電腦和手機江解,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來徙歼,“玉大人犁河,你說我怎么就攤上這事∑翘荩” “怎么了桨螺?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長酿秸。 經(jīng)常有香客問我灭翔,道長,這世上最難降的妖魔是什么辣苏? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任肝箱,我火速辦了婚禮哄褒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘煌张。我一直安慰自己呐赡,他們只是感情好,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布骏融。 她就那樣靜靜地躺著链嘀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪绎谦。 梳的紋絲不亂的頭發(fā)上管闷,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天粥脚,我揣著相機與錄音窃肠,去河邊找鬼。 笑死刷允,一個胖子當著我的面吹牛冤留,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播树灶,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼纤怒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了天通?” 一聲冷哼從身側(cè)響起泊窘,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎像寒,沒想到半個月后烘豹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡诺祸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年携悯,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筷笨。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡憔鬼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出胃夏,到底是詐尸還是另有隱情轴或,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布仰禀,位于F島的核電站侮叮,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏悼瘾。R本人自食惡果不足惜囊榜,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一审胸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧卸勺,春花似錦砂沛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至悟狱,卻和暖如春静浴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背挤渐。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工苹享, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人浴麻。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓得问,卻偏偏與公主長得像,于是被迫代替她去往敵國和親软免。 傳聞我的和親對象是個殘疾皇子宫纬,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359