手寫注入框架以ButterKnife為例(詳)

前言

在開發(fā)經(jīng)常碰到注解形式,比如我們代碼中常見@Override,也比如我之前發(fā)表的Retrofit之解析xml (詳細)全是注解操作,還有很多第三方注解注入框架,像ButterKnife,Dagger2等,注解的使用很廣泛,今日以ButterKnife為例,來探其究竟.

準備知識

java注解是在java5之后引入的,對于java注解完全不了解的請看秒懂,Java 注解 (Annotation)你可以這樣學,看完可了解注解的基本用法.注解分為運行時注解和編譯時注解,此處ButterKnife為編譯時注解.

核心原理

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

項目架構

在Inject中關聯(lián)inject-annotion

在Inject-compiler中引入jar包,如圖

Android庫Inject處理

Inject為使用方提供方法,先看是MainAcitity中是如何調(diào)用的

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);
        Toast.makeText(this,"--->  "+textview, Toast.LENGTH_SHORT).show();
        textview.setText("6666666");
    }
}

所以此處在InjectView對應處理是,提供相應方法

public class InjectView {
    public  static  void bind(Activity activity)
    {
        String clsName=activity.getClass().getName();//反射拿到使用方類名
        try {
            Class<?> viewBidClass= Class.forName(clsName+"$$ViewBinder");//反射拿到新生成的內(nèi)部類
            ViewBinder viewBinder= (ViewBinder) viewBidClass.newInstance();//反射后類型
            viewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }
}

ViewBinder是為了接收拿到的反射后的類型

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

Java庫inject-annotion處理

inject-annotion負責提供注解,此處只創(chuàng)建了BindView注解

@Target(ElementType.FIELD)//表示可以給屬性進行注解
@Retention(RetentionPolicy.CLASS)//代表運行在編譯時
public @interface BindView {
    int value();
}

Java庫Inject-compiler處理

Inject-compiler中處理核心邏輯,基本原理是,在編譯時javac編譯器會檢查AbstractProcessor的子類,并且調(diào)用該類型的process函數(shù),然后將添加了注解的所有元素都傳遞到process函數(shù)中,使得開發(fā)人員可以在編譯器進行相應的處理.
此處BindViewProcessor繼承了AbstractProcessor,并通過@AutoService(Processor.class),表明自身為注解處理器,其中主要有四個方法,在init方法中做初始化;在getSupportedAnnotationTypes方法中指明支持哪些注解,此處指明的注解即為BindView;在getSupportedSourceVersion方法中返回最新的JDK版本;在process方法中做主邏輯處理

//用@AutoService 指定注解處理器
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    /**
     * 處理Element工具類
     * 主要獲取包名
     */
    private Elements elementUtils;
    /**
     * Java文件輸出類(非常重要)生成它是用來java文件
     */
    private Filer    filer;

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

    /**
     * @return 當前注解處理器  支持哪些注解
     * 返回的是set字符串集合
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        //        types.add(Override.class.getCanonicalName());
        return types;
    }

    /**
     * 支持jdk版本
     * 一般選擇最新版本
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * javac編譯器  遇到含有BindView注解的java文件時,就會調(diào)用這個process方法
     *
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //聲明一個緩存的map集合   鍵  是Activity   值  是當前Activity 里面含有BndView成員變量的集合
        Map<TypeElement, List<FieldViewBinding>> targetMap = new HashMap<>();
        //編譯時只能通過文件輸出打印,不能通過Log,System.out.print打印
        FileUtils.print("------------>    ");
        //for循環(huán)帶有BindView注解的Element
        //將每個Element進行分組。分組的形式 是將在一個Activit的Element分為一組
        for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
            FileUtils.print("elment   " + element.getSimpleName().toString());
            //  enClosingElement可以理解成Element
            TypeElement enClosingElement = (TypeElement) element.getEnclosingElement();
            // List<FieldViewBinding>   當前Activity 含有注解的成員變量集合
            List<FieldViewBinding> list = targetMap.get(enClosingElement);
            if (list == null) {
                list = new ArrayList<>();
                targetMap.put(enClosingElement, list);//
            }
            //得到包名
            String packageName = getPackageName(enClosingElement);
            //得到id
            int id = element.getAnnotation(BindView.class).value();
            //            得到成員變量名  TextView  text;  這里得到的是text字符串
            String fieldName = element.getSimpleName().toString();
            //            當前成員變量的類類型   可以理解成  TextView
            TypeMirror typeMirror = element.asType();
            //            封裝成FieldViewBinding  類型
            FieldViewBinding fieldViewBinding = new FieldViewBinding(fieldName, typeMirror, id);
            list.add(fieldViewBinding);
        }
        //遍歷每一個Activity  TypeElement代表類類型
        for (Map.Entry<TypeElement, List<FieldViewBinding>> item : targetMap.entrySet()) {
            List<FieldViewBinding> list = item.getValue();

            if (list == null || list.size() == 0) {
                continue;
            }
            //enClosingElement 表示 activity
            TypeElement enClosingElement = item.getKey();
            //            得到包名
            String packageName = getPackageName(enClosingElement);
            //截取字符串    MainActivity
            String complite = getClassName(enClosingElement, packageName);
            //遵循Javapoet規(guī)范,MainActivity為類類型  在這里封裝成ClassName
            ClassName className = ClassName.bestGuess(complite);
            //          ViewBinder類型
            ClassName viewBinder = ClassName.get("com.base.inject", "ViewBinder");
            //            開始構建java文件
            //            從外層包名  類名開始構建
            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);
            //            構建方法里面的具體邏輯,這里的邏輯代表findViewById
            for (int i = 0; i < list.size(); i++) {
                FieldViewBinding fieldViewBinding = list.get(i);
                //-->android.text.TextView
                String pacckageNameString = fieldViewBinding.getType().toString();
                ClassName viewClass = ClassName.bestGuess(pacckageNameString);
                //$L  代表占位符  和StringFormater類似。$L代表基本類型  $T代表  類類型
                methodBuilder.addStatement
                        ("target.$L=($T)target.findViewById($L)", fieldViewBinding.getName()
                                , viewClass, fieldViewBinding.getResId());
            }

            result.addMethod(methodBuilder.build());

            try {
                //生成Java文件頭部的注釋說明,裝逼用
                JavaFile.builder(packageName, result.build())
                        .addFileComment("auto create make")
                        .build().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }


        }
        return false;
    }

    //enClosingElement.getQualifiedName().toString()返回的是com.example.administrator.butterdepends.MainActivity
    private String getClassName(TypeElement enClosingElement, String packageName) {
        int packageLength = packageName.length() + 1;
        //        replace(".","$")的意思是  如果當前的enClosing為內(nèi)部類的話
        //       裁剪掉包名和最后一個點號,將去掉包名后,后面還有點號則替換成$符號
        return enClosingElement.getQualifiedName().toString().substring(packageLength).replace(".", "$");
    }

    private String getPackageName(TypeElement enClosingElement) {
        //返回的是  com.example.administrator.butterknifeframwork。通過工具類獲取的
        return elementUtils.getPackageOf(enClosingElement).getQualifiedName().toString();
    }
}

此處重點說明process方法,第一個for循環(huán)用來拿到所有使用方class,以及該class調(diào)用的所有注解,所以建立一個map集合,TypeElement對應的是使用方class文件,List<FieldViewBinding>>代表該classs文件中用到的所有注解.最早返回的是Element形式,此處將其轉(zhuǎn)化為FieldViewBinding來存儲.
FieldViewBinding代碼

public class FieldViewBinding {
   private String name;//  textview
   private TypeMirror type ;//--->TextView
   private int resId;//--->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;
   }
}

第二個for循環(huán)是生成每個調(diào)用方class相對應的內(nèi)部類class文件,是由外往內(nèi)拼接生成的,先拼接創(chuàng)建包名類名,然后拼接構建方法名,最終的拼接效果為

代碼運行成功可以看到這個新生成的class文件,其實就是一個對應的內(nèi)部類
注意:在jar包中測試代碼不能使用log日志,所以此處使用FileUtils,以文件輸出打印,會在桌面生成一個log.txt文件顯示所有l(wèi)og信息,以便于代碼出bug時進行調(diào)試,奉上FileUtils代碼

public class FileUtils {
    public static void print(String text)
    {
        File file=new File("C:\\Users\\Administrator\\Desktop\\log.txt");
        if(!file.exists())
        {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            FileWriter fileWriter=new FileWriter(file.getAbsoluteFile(),true);
            fileWriter.write(text+"\n");
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

以上為手寫注入ButterKnife的全部流程,其實并不復雜,因為只是簡單實現(xiàn)效果,Dagger2等也是相同的道理

此框架和ButterKnife使用方法是一致的,使用時別忘了在gradle中加入

結尾

奉上代碼地址https://github.com/HaowangSmith/HaowangButterKnife
相關知識鏈接ButterKnife源碼分析

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 1.什么是注解2.注解的分類3.編譯時注解的原理4.APT5.創(chuàng)建項目及依賴6.編碼實現(xiàn)7.總結 我們首先了解一下...
    慕涵盛華閱讀 686評論 0 6
  • 前面寫了Android 開發(fā):由模塊化到組件化(一),很多小伙伴來問怎么沒有Demo啊?之所以沒有立刻放demo的...
    涅槃1992閱讀 8,219評論 4 37
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,511評論 19 139
  • 什么是注解注解分類注解作用分類 元注解 Java內(nèi)置注解 自定義注解自定義注解實現(xiàn)及使用編譯時注解注解處理器注解處...
    Mr槑閱讀 1,147評論 0 3
  • 羔羊尚知哺育恩,曲膝傾向是跪蹲。生靈萬物隨孝道,世中有愛母為尊。古有慈母手中線,今存遠游念兒心。常言百善孝為首,無...
    無世真人閱讀 814評論 3 11

友情鏈接更多精彩內(nèi)容