04 IOC架構(gòu)設(shè)計(jì)【ButterKnife原理悴势、自己手?jǐn)]一個(gè)】

三分鐘的介紹

相信很多人都在開發(fā)中都使用過(guò)ButterKnife吧窗宇!沒(méi)有用過(guò)的也都聽過(guò)。ButterKnife是一個(gè)專注于Android系統(tǒng)的View注入框架,以前總是要寫很多findViewById來(lái)找到View對(duì)象特纤,有了ButterKnife可以很輕松的省去這些步驟担映。

說(shuō)說(shuō)人家的優(yōu)點(diǎn)

  1. 簡(jiǎn)化代碼,提升開發(fā)效率
    強(qiáng)大的View綁定和Click事件處理功能

  2. 不會(huì)影響app運(yùn)行效率
    ButterKnife采用編譯時(shí)注解的方式生成代碼叫潦,運(yùn)行是不會(huì)影響App效率

用法

GitHub地址(Star 25.1k):https://github.com/JakeWharton/butterknife

class ExampleActivity extends Activity {
  @BindView(R.id.user) EditText username;
  @BindView(R.id.pass) EditText password;

  @BindString(R.string.login_error) String loginErrorMessage;

  @OnClick(R.id.submit) void submit() {
    // TODO call server...
  }

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
    // TODO Use fields...
  }
}

原理

僅僅一個(gè)注解加一行代碼可以實(shí)現(xiàn) findViewById蝇完,它們都做了哪些事情呢?
1矗蕊、給元素加注解標(biāo)記
2短蜕、收集注解的元素生成Java類(編譯器執(zhí)行)
3、動(dòng)態(tài)注入
源碼走一波
第一步:加注解沒(méi)啥好說(shuō)的傻咖,過(guò)
第二步:收集注解生成Java類

在編譯時(shí)朋魔,通過(guò)處理注解元素,生成新的 Java 代碼類卿操,該Java代碼 里面包含了我們的 findViewById(R.id.xxx)警检、view.setonclickListener(new lis... )的這些動(dòng)作;

ButterKnifeProcessor.java(GitHub中的源碼)

public boolean process(Set<? extends TypeElement> set, RoundEnvironment env) {
        print("process:");
        print("env"+env.getRootElements());
        Map<TypeElement, List<FieldBinding>> map = new HashMap<>();

        for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
            //get the Activity
            TypeElement activityElement = (TypeElement) element.getEnclosingElement();
            print(" activityElement:"+ activityElement.toString());
            List<FieldBinding> list = map.get(activityElement);
            if (list == null) {
                list = new ArrayList<>();
                map.put(activityElement, list);
            }
            //get  id
            int id = element.getAnnotation(BindView.class).value();
            //get fieldName
            String fieldName = element.getSimpleName().toString();
            //get mirror

            TypeMirror typeMirror = element.asType();
            print(" typeMirror:"+ typeMirror);
            FieldBinding fieldBinding = new FieldBinding(fieldName, typeMirror, id);
            list.add(fieldBinding);
        }

        for (Map.Entry<TypeElement, List<FieldBinding>> item :
                map.entrySet()) {
            TypeElement activityElement = item.getKey();

            //get packageName
            String packageName = elementUtils.getPackageOf(activityElement).getQualifiedName().toString();
            //get  activityName
            String activityName = activityElement.getSimpleName().toString();

            //transfrom type Activity with system can discern
            ClassName activityClassName = ClassName.bestGuess(activityName);
            ClassName viewBuild = ClassName.get(ViewBinder.class.getPackage().getName(), ViewBinder.class.getSimpleName());    //

            TypeSpec.Builder result = TypeSpec.classBuilder(activityClassName + "$$ViewBinder")
                    .addModifiers(Modifier.PUBLIC)
                    .addTypeVariable(TypeVariableName.get("T", activityClassName))
                    .addSuperinterface(ParameterizedTypeName.get(viewBuild,activityClassName));

            MethodSpec.Builder method = methodBuilder("bind")      //methodName
                    .addModifiers(Modifier.PUBLIC)                          // modifier
                    .returns(TypeName.VOID)
                    .addAnnotation(Override.class)
                    .addParameter(activityClassName, "target", Modifier.FINAL);
//
            List<FieldBinding> list = item.getValue();
            for (FieldBinding fieldBinding : list) {
                //
                String pacageName = fieldBinding.getType().toString();
                ClassName viewClass = ClassName.bestGuess(pacageName);

                method.addStatement("target.$L=($T)target.findViewById($L)", fieldBinding.getName(), viewClass, fieldBinding.getResId());

            }
//
            result.addMethod(method.build());

            try {
                JavaFile.builder(packageName, result.build()).build().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

第三步:就是動(dòng)態(tài)注入害淤, ButterKnife.bind(this)扇雕; 源碼中最后會(huì)通過(guò)反射加載一個(gè)***_ViewBinding這個(gè)類

private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
        || clsName.startsWith("androidx.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
      if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
    } catch (ClassNotFoundException e) {
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
    } catch (NoSuchMethodException e) {
      throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
    }
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

瞧一瞧看一看MainActivity_ViewBinding .java

public class MainActivity_ViewBinding implements Unbinder {
 private MainActivity target;

 private View view7f070022;

 @UiThread
 public MainActivity_ViewBinding(MainActivity target) {
   this(target, target.getWindow().getDecorView());
 }

 @UiThread
 public MainActivity_ViewBinding(final MainActivity target, View source) {
   this.target = target;

   View view;
   view = Utils.findRequiredView(source, R.id.button, "field 'button' and method 'click'");
   target.button = Utils.castView(view, R.id.button, "field 'button'", Button.class);
   view7f070022 = view;
   view.setOnClickListener(new DebouncingOnClickListener() {
     @Override
     public void doClick(View p0) {
       target.click();
     }
   });
 }

 @Override
 @CallSuper
 public void unbind() {
   MainActivity target = this.target;
   if (target == null) throw new IllegalStateException("Bindings already cleared.");
   this.target = null;

   target.button = null;

   view7f070022.setOnClickListener(null);
   view7f070022 = null;
 }
}

看到這里已經(jīng)完全明白了,為什么只需要短短的兩行代碼了窥摄。镶奉。。

開啟手?jǐn)]模式(三步走 模式)

第一步:我們需要?jiǎng)?chuàng)建注解崭放,在項(xiàng)目中New Module -- Java Library(Library name: injectAnnotations)

創(chuàng)建一個(gè)注解

// 具體可以去了解一下注解的使用
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface InjectView {
    // 我們這里也使用了android提供的一些注解
    @IdRes
    int value();
}

官方提供了很多特別好用的類或注解哨苛,這里說(shuō)的support annotation就是特別好的工具,多使用其中的注解币砂,需要在gradle中加入

  implementation 'com.android.support:support-annotations:25.2.0'

第二步:注解生成器建峭,收集所有的注解,生成Java文件
在項(xiàng)目中New Module -- Java Library(Library name: injectCompiler)
思考一下决摧,我們的注解Module需要提供給app使用亿蒸,注解生成器Module也提供給app
那么我們需要在app的gradle中加入

     implementation project(':injectAnnotations')
    // annotationProcessor表示這是編譯時(shí)的注解處理器
    annotationProcessor project(':injectCompiler')

注解生成器Module也需要知道我都需要處理哪些注解使碾,所以需要在gradle中引入inject-annotations

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation project(':injectAnnotations')

    implementation "com.google.auto.service:auto-service:1.0-rc4"http://自動(dòng)配置的
    annotationProcessor "com.google.auto.service:auto-service:1.0-rc4" //這個(gè)在gradle5.0以上需要的
    implementation 'com.squareup:javapoet:1.11.1'//方便編寫代碼的
}

//  解決build 錯(cuò)誤:編碼GBK的不可映射字符
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

注解生成器配置OK了,接下來(lái)我們創(chuàng)建 ButterKnifeProcessor.class

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

    private Filer filer;
    private Elements mElementUtils;

    // 使用之前需要初始化三個(gè)動(dòng)作
    // 1祝懂、支持的java版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    // 2票摇、當(dāng)前APT能用來(lái)處理哪些注解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(InjectView.class.getCanonicalName());
        supportTypes.add(InjectClick.class.getCanonicalName());
        return supportTypes;
    }

    // 3、需要一個(gè)用來(lái)生產(chǎn)文件的對(duì)象
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
        mElementUtils = processingEnvironment.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 這里會(huì)把所有跟注解有關(guān)的field全部拿到,我們需要手動(dòng)進(jìn)行分類
        Set<? extends Element> viewElements = roundEnvironment.getElementsAnnotatedWith(InjectView.class);
        Set<? extends Element> clickElements = roundEnvironment.getElementsAnnotatedWith(InjectClick.class);
        // 將所有注解集合分離出以activity為單元的注解集合
        Map<Element, List<Element>> viewElementsMap  = set2Map(viewElements);
        // 將所有注解集合分離出以activity為單元砚蓬,再以控件ID為單元的注解
        Map<Element, List<Element>> clickElementsMap  = set2Map(clickElements);

        //------------生成代碼,使用Java代碼生成框架-JavaPoet解析-----------
        for (Map.Entry<Element, List<Element>> entry : viewElementsMap.entrySet()) {
            Element activityElement = entry.getKey();
            List<Element> viewFieldElementList = entry.getValue();

            //得到類名的字符串
            String activityName = activityElement.getSimpleName().toString();
            ClassName activityClassName = ClassName.bestGuess(activityName);

            // 拼裝這一行代碼:public final class xxx_ViewBinding implements IBinder
            ClassName targetTypeName = ClassName.get("com.demo.james","IBinder");
            TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName+"_ViewBinding")
                    //類名前添加public final
                    .addModifiers(Modifier.FINAL, Modifier.PUBLIC)
                    //添加類的實(shí)現(xiàn)接口矢门,并指定泛型的具體類型
                    .addSuperinterface(ParameterizedTypeName.get(targetTypeName, activityClassName))
                    //添加一個(gè)成員變量target
                    .addField(activityClassName, "target", Modifier.PRIVATE);

            // 實(shí)現(xiàn)IBinder的方法
            //  拼裝這一行代碼:public final void bind(ButterknifeActivity target)
            MethodSpec.Builder bindMethod = MethodSpec.methodBuilder("bind")//和你創(chuàng)建的bind中的方法名保持一致
                    .addAnnotation(Override.class)
                    .addParameter(activityClassName, "activity")
                    .addStatement("this.target = activity")
                    .addModifiers(Modifier.FINAL, Modifier.PUBLIC);

            // 存儲(chǔ)已findView的控件,為添加點(diǎn)擊事件的時(shí)候判斷是否需要重新findViewById
            Map<Integer, String> findViewMap = new LinkedHashMap<>();
            // 遍歷注解的字段生成findViewById
            for (Element fieldElement : viewFieldElementList) {
                String fieldName = fieldElement.getSimpleName().toString();
                //在構(gòu)造方法中添加初始化代碼

                //  在bind方法中添加
                //  target.btn = target.findViewById(2131230762);
                // 獲取注解里面的值灰蛙,也就是id
                InjectView annotation = fieldElement.getAnnotation(InjectView.class);
                int resId = annotation.value();
                findViewMap.put(resId, fieldName);
                bindMethod.addStatement("target.$L = target.findViewById($L)",fieldName,resId);
            }

            List<Element> clickFieldElementList = clickElementsMap.get(activityElement);
            if (clickFieldElementList != null){
                for (Element fieldElement : clickFieldElementList) {
                    System.out.println("clickFieldElementList : "+ fieldElement.getSimpleName().toString());
                    // 添加onCLickListener
                    //  target.btn.setOnClickListener(new View.OnClickListener() {
                    //      @Override
                    //      public void onClick(View view) {
                    //        target.test();
                    //      }
                    //   });
                    ClassName viewClass = ClassName.get("android.view","View");
                    TypeSpec onCLick = TypeSpec.anonymousClassBuilder("")
                            .superclass(ClassName.bestGuess("android.view.View.OnClickListener"))
                            .addMethod(MethodSpec.methodBuilder("onClick")
                                    .addAnnotation(Override.class)
                                    .addModifiers(Modifier.PUBLIC)
                                    .addParameter(viewClass, "view")
                                    .returns(void.class)
                                    .addStatement("target.$L()", fieldElement.getSimpleName().toString())
                                    .build())
                            .build();
                    InjectClick annotation = fieldElement.getAnnotation(InjectClick.class);
                    int resId = annotation.value();
                    if (findViewMap.get(resId) == null){
                        bindMethod.addStatement("target.findViewById($L).setOnClickListener($L)",resId,onCLick);
                    }else{
                        bindMethod.addStatement("target.$L.setOnClickListener($L)",findViewMap.get(resId),onCLick);
                    }
                }
            }
            classBuilder.addMethod(bindMethod.build());

            //開始生成
            try {
                //得到包名
                String packageName = mElementUtils.getPackageOf(activityElement)
                        .getQualifiedName().toString();
                JavaFile.builder(packageName,classBuilder.build())
                        //添加類的注釋
                        .addFileComment("butterknife 自動(dòng)生成")
                        .build().writeTo(filer);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    private Map<Element, List<Element>> set2Map(Set<? extends Element> viewElements) {
        Map<Element, List<Element>> viewElementsMap  = new LinkedHashMap<>();
        for (Element fieldElement : viewElements) {
            //element.getSimpleName()得到的是這個(gè)field注解名祟剔, Button btn;  輸出 btn
            System.out.println("field name : "+fieldElement.getSimpleName());
            Element activityElement = fieldElement.getEnclosingElement();
            //得到的是這個(gè)field所在類的類名
            System.out.println("activityElement name : "+activityElement.getSimpleName());

            //以類對(duì)象為key值存儲(chǔ)一個(gè)類中所有的field到集合中
            List<Element> elementList = viewElementsMap.get(activityElement);
            if (elementList == null){
                elementList = new ArrayList<>();
                viewElementsMap.put(activityElement, elementList);
            }
            elementList.add(fieldElement);
        }
        return viewElementsMap;
    }
}

第三步:動(dòng)態(tài)注入,需要提供一個(gè)給用戶使用的東東

public class JettButterKnife{
    public static void bind(Activity activity){
        String name = activity.getClass().getName() + "_ViewBinding" ;
        try {
            Class<?> clazz = Class.forName(name);
            IBinder iBinder = (IBinder) clazz.newInstance();
            iBinder.bind(activity);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
public class ButterknifeActivity extends AppCompatActivity {

    @InjectView(R.id.button6)
    Button btn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_butterknife);
        JettButterKnife.bind(this);
        btn.setText("我要賦值咯摩梧!物延。。仅父。叛薯。");
    }

    @InjectClick(R.id.button8)
    public void test(){
        Toast.makeText(this, "點(diǎn)的就是我", Toast.LENGTH_LONG).show();
    }
}

基本上都已經(jīng)注釋了,自己手?jǐn)]一個(gè)BufferKnife笙纤,實(shí)現(xiàn)了findViewById與onClick的注解功能耗溜。

總結(jié)

一晃已經(jīng)凌晨?jī)牲c(diǎn)了,熬不牢J∪荨6端!
確實(shí)一個(gè)插件需要考慮的事情非常多腥椒,不動(dòng)手去做是想不到的阿宅。之前只是實(shí)現(xiàn)了findViewById,但是要正在加onClick的時(shí)候笼蛛,還需要考慮更多洒放。代碼中還有很多驗(yàn)證的地方?jīng)]有去做,比如:一個(gè)ID多個(gè)注解伐弹、ID的有效性等等拉馋,代碼還存在很多優(yōu)化的地方,今天就到這里了惨好,有問(wèn)題可以留言一起探討探討。宏悦。创淡。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末辣垒,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子龄句,更是在濱河造成了極大的恐慌回论,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件分歇,死亡現(xiàn)場(chǎng)離奇詭異傀蓉,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)职抡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門葬燎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人缚甩,你說(shuō)我怎么就攤上這事谱净。” “怎么了擅威?”我有些...
    開封第一講書人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵壕探,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我郊丛,道長(zhǎng)李请,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任厉熟,我火速辦了婚禮捻艳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘庆猫。我一直安慰自己认轨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開白布月培。 她就那樣靜靜地躺著嘁字,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杉畜。 梳的紋絲不亂的頭發(fā)上纪蜒,一...
    開封第一講書人閱讀 50,096評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音此叠,去河邊找鬼纯续。 笑死,一個(gè)胖子當(dāng)著我的面吹牛灭袁,可吹牛的內(nèi)容都是我干的猬错。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼茸歧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼倦炒!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起软瞎,我...
    開封第一講書人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤逢唤,失蹤者是張志新(化名)和其女友劉穎拉讯,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鳖藕,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡魔慷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了著恩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片院尔。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖页滚,靈堂內(nèi)的尸體忽然破棺而出召边,到底是詐尸還是另有隱情,我是刑警寧澤裹驰,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布隧熙,位于F島的核電站,受9級(jí)特大地震影響幻林,放射性物質(zhì)發(fā)生泄漏贞盯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一沪饺、第九天 我趴在偏房一處隱蔽的房頂上張望躏敢。 院中可真熱鬧,春花似錦整葡、人聲如沸件余。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)啼器。三九已至,卻和暖如春俱萍,著一層夾襖步出監(jiān)牢的瞬間端壳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工枪蘑, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留损谦,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓岳颇,卻偏偏與公主長(zhǎng)得像照捡,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赦役,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容