一步步帶你實現簡版 ButterKnife

fancyluo發表於2018-04-20

一、專案工程介紹

一步步帶你實現簡版 ButterKnife

  • lib-annotation 是一個 Java Library 模組,主要用於自定義註解;
  • lib-compiler 是一個 Java Library 模組,需要依賴 lib-annotation 模組,主要用於解析自定義註解與生成原始檔。lib-compiler 還需要依賴 3 個開源庫來幫助開發;
    • auto-common/auto-service:為註解處理器自動生成 metadata 檔案並將註解處理器 jar 檔案加入構建路徑,不再需要我們手動建立並更新 META-INF/services/javax.annotation.processing.Processor 檔案;
    • javapoet:一款 Java 程式碼生成框架,可以令我們省去繁瑣冗雜的拼接程式碼的重複工作。
  • lib-inject 是一個 Android Library 模組,需要依賴 lib-annotation 模組,主要用於提供 Api 給 app 模組呼叫;
  • app 為應用模組,依賴 lib-compiler 與 lib-inject;

二、lib-annotation-自定義註解模組

建立一個自定義註解類BindView

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}
複製程式碼
  • @Target(ElementType.FIELD)表示該註解修飾的是成員變數;
  • @Retention(RetentionPolicy.CLASS)表示該註解只會在編譯時使用;
  • int value()為註解的值,這裡應該傳入的是一個控制元件 id;

三、lib-compiler-註解處理器模組

首先在build.gradle裡新增依賴

dependencies {
    api project(':lib-annotation')
    
    implementation 'com.google.auto:auto-common:0.8'
    implementation 'com.google.auto.service:auto-service:1.0-rc3'
    
    implementation 'com.squareup:javapoet:1.9.0'
}
複製程式碼

然後建立一個類 BindViewProcessor,通過繼承 AbstractProcessor 來自定義註解處理器,繼承 AbstractProcessor 要實現一個抽象方法process()

public class BindViewProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, 
                           RoundEnvironment roundEnvironment) {
        return false;
    }
}
複製程式碼

這裡我們先不理會這個方法,先做一些準備工作

第一步,我們需要註冊 BindViewProcessor,之前我們已經新增了 auto-service 這個庫,那麼註冊就是一個註解的事,使用@AutoService(Processor.class)

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor { }
複製程式碼

第二步,我們需要宣告支援的 Java 版本,這裡有兩種方式,一種是重寫getSupportedSourceVersion(),一種是使用註解@SupportedSourceVersion()

// 重寫方法
@Override
public SourceVersion getSupportedSourceVersion() {
	return SourceVersion.latestSupported();
}

// 使用註解
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BindViewProcessor extends AbstractProcessor { }
複製程式碼

SourceVersion 是一個列舉類,可以使用SourceVersion.RELEASE_0SourceVersion.RELEASE_8表示各個 Java 版本,也可以直接使用SourceVersion.latestSupported()表示最新的版本

第三步,我們需要宣告自定義註解處理器要處理哪些註解,同樣的,這裡也有兩種方式,一種是重寫getSupportedAnnotationTypes(),一種是使用註解@SupportedAnnotationTypes()

// 重寫方法
@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> set = new LinkedHashSet<>();
    set.add(BindView.class.getCanonicalName());
    return set;
}

// 使用註解-傳入註解的全類名
@SupportedAnnotationTypes({"com.fancyluo.lib_annotation.BindView"})
public class BindViewProcessor extends AbstractProcessor { }
複製程式碼

第四步,我們需要重寫init()方法來獲取一些輔助類

// 解析 Elementm 的工具類,主要用於獲取包名
private Elements mElementUtils;
// 主要用於輸出 Java 原始檔
private Filer mFiler;

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    mElementUtils = processingEnvironment.getElementUtils();
    mFiler = processingEnvironment.getFiler();
}
複製程式碼

第五步,這裡要重新拿起之前忽略的process()方法,這個方法是重中之重,我們要在這裡面解析自定義註解和生成 Java 原始檔。

先來看看我們要生成什麼樣的程式碼

public class MainActivity$$ViewBinder<T extends MainActivity> 
	implements ViewBinder<MainActivity> {
  @Override
  public void bind(final MainActivity target) {
    target.btnAction=(Button)target.findViewById(2131165218);
  }
}
複製程式碼

當我們使用BindView修飾程式元素的時候,我們的自定義註解處理器就可以拿到相應的程式元素的節點,通過解析節點,拿到相應的資料,然後自動的為這個程式元素所在的類生成一個輔助類,在裡面為程式元素賦值。

也可以這麼理解,我們會為使用BindView修飾的控制元件所在的 Activity 自動的生成一個輔助類,在裡面進行控制元件的findViewById

接下來的程式碼都是在process()方法裡,只是我將其分拆出來講解

@Override
public boolean process(Set<? extends TypeElement> set, 
                       RoundEnvironment roundEnvironment) {
    ...//程式碼下面講解
	return false;
}
複製程式碼

首先,我們通過 roundEnvironment 拿到所有的被BindView修飾的節點

Set<? extends Element> elements = 
    roundEnvironment.getElementsAnnotatedWith(BindView.class);
複製程式碼

這裡可以理解為一個控制元件,只是被封裝轉換成了 Element

@BindView(R.id.btnAction)
Button btnAction;

轉換成 -> Element
複製程式碼

然後遍歷 elements 集合,解析資料,將我們需要的資料封裝成一個類,並按照 TypeElement 來進行分組。TypeElement 可以理解為類節點,而 Element 是成員節點,再具體來說,TypeElement 就是 MainActivity,而 Element 就是其中的 btnAction;那麼,按照 TypeElement 分組也就是將控制元件按照其所在的 Activity 進行分組。

首先建立我們需要的資料封裝類BindViewInfo

public class FieldBinding {

    // 可以理解為:Button 這個型別
    private TypeMirror typeMirror;
    // 可以理解為:成員變數名-btnAction
    private String name;
    // 可以理解為:Button 的 id-R.id.btnAction
    private int resId;
    
    ...
    
}
複製程式碼

開始遍歷集合,並且將節點資料封裝到 BindViewInfo,並將其分組儲存到 Map 集合

// Key 為型別節點,可以理解為 MainActivity
// Value 可以理解為 MainActivity 裡面所有被 BindView 註解的成員變數資訊
Map<TypeElement, List<BindViewInfo>> cacheMap = new HashMap<>();

// 遍歷所有被 BindView 註解的成員變數,按照 Activity 進行分組
for (Element element : elements) {
    // 得到型別節點,可以理解為得到MainActivity
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    // 從快取中獲取資料,如果沒有,則新建並新增到快取
    List<BindViewInfo> fieldList = cacheMap.get(enclosingElement);
    if (fieldList == null) {
        fieldList = new ArrayList<>();
        cacheMap.put(enclosingElement, fieldList);
    }
    // 封裝被 BindView 註解的成員變數的資訊
    // 成員變數的類型別,例如 Button
    TypeMirror typeMirror = element.asType();
    // 成員變數名 例如 btnAction
    String fieldName = element.getSimpleName().toString();
    // 控制元件資源Id 例如 R.id.btn
    int resId = element.getAnnotation(BindView.class).value();
    BindViewInfo bindViewInfo = new BindViewInfo(typeMirror, fieldName, resId);
    fieldList.add(fieldBinding);
}
複製程式碼

將資料分好組快取後,我們就可以來構建我們需要的 Java 原始檔的程式碼了,前面說過,TypeElement 代表著一個Activity,而 List<BindViewInfo>就代表著裡面使用 BindView 註解修飾的控制元件,我們要為 Activity 生成一個輔助類,在裡面為這些控制元件生成 findViewById 程式碼

首先,我們遍歷 cacheMap,並解析我們需要的資料

for (Map.Entry<TypeElement, List<FieldBinding>> entry : cacheMap.entrySet()) {
    List<FieldBinding> bindingList = entry.getValue();
    // 如果該Activity沒有被BindView註解的成員變數,則執行下一個
    if (bindingList == null || bindingList.size() == 0) {
        continue;
    }
    // 獲取型別節點 例如 MainActivity
    TypeElement typeElement = entry.getKey();
    // 獲取包名 例如 com.fancyluo.k_butterknife
    String packageName = getPackageName(typeElement);
    // 獲取類名 例如 MainActivity
    String classNameStr = getClassName(packageName, typeElement);
    ClassName classNamePackage = ClassName.bestGuess(classNameStr);
    // 獲取ViewBinder
    ClassName viewBinder = ClassName.get("com.fancyluo.lib_inject", "ViewBinder");
    
    ...//程式碼下面講解
}
複製程式碼

getPackageName(typeElement)

private String getPackageName(TypeElement enClosingElement) {
  	// 獲取包節點
  	PackageElement packageElement = mElementUtils.getPackageOf(typeElement);
    //返回的是 com.fancyluo.k_butterknife
  	return packageElement.getQualifiedName().toString();
}
複製程式碼

getClassName(packageName, typeElement)

// 例如 com.fancyluo.k_butterknife.MainActivity
String qualifiedName = typeElement.getQualifiedName().toString();
// 例如 com.fancyluo.k_butterknife.
int length = packageName.length() + 1;
// 如果當前的TypeElement是內部類的話,裁剪掉包名和後面的點號,並將之後的點號替換為$
return qualifiedName.substring(length).replace(".", "$");
複製程式碼

ViewBinder是在lib_inject模組裡定義的一個介面,我們生成的輔助類需要實現這個介面並且實現介面的bind()方法進行控制元件的findViewById

拿到我們需要的資料以後,就可以開始使用 javapoet 提供的 api 來構建 Java 原始碼,下面,我們再來貼一下我們要生成的程式碼,然後我們會一步一步來構建這些程式碼。

public class MainActivity$$ViewBinder<T extends MainActivity> 
	implements ViewBinder<MainActivity> {
  @Override
  public void bind(final MainActivity target) {
    target.btnAction=(Button)target.findViewById(2131165218);
  }
}
複製程式碼

首先,我們要構建

TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(classNameStr + "$$ViewBinder")
    .addModifiers(Modifier.PUBLIC)
    .addTypeVariable(TypeVariableName.get("T", classNamePackage))
    .addSuperinterface(ParameterizedTypeName.get(viewBinder, classNamePackage));
複製程式碼
  • classBuilder 裡傳入的是類名
  • addModifiers 是設定類的訪問屬性
  • addTypeVariable 是設定類的泛型引數,傳入一個 TypeVariableName,TypeVariableName 第一個引數為泛型引數名,第二個引數為 ClassName,例如 T extends MainActivity
  • addSuperinterface 是設定當前類實現的介面,傳入一個 ParameterizedTypeName,ParameterizedTypeName 第一個引數為父介面的 ClassName,第二個引數 ClassName,例如 ViewBinder<MainActivity>

這裡就相當於構建了

public class MainActivity$$ViewBinder<T extends MainActivity> 
	implements ViewBinder<MainActivity> {
}
複製程式碼

第二,我們要構建方法

MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")//方法名
    .addAnnotation(Override.class)//新增註解
    .addModifiers(Modifier.PUBLIC)//訪問屬性
    .returns(TypeName.VOID)// 返回值
    // 新增引數:1-ClassName 2-引數名  3-引數的訪問許可權
    .addParameter(classNamePackage, "target", Modifier.FINAL);
複製程式碼

構建完方法的基本元素後,現在的程式碼結構為

public class MainActivity$$ViewBinder<T extends MainActivity> 
	implements ViewBinder<MainActivity> {
	@Override
  	public void bind(final MainActivity target) {
  		...
  	}
}
複製程式碼

最後我們來構建方法裡面的具體程式碼,也就是相應控制元件的 findViewById

for (BindViewInfo bindViewInfo : bindingList) {
    // 獲取型別名稱,例如 Button
    String packageNameStr = fieldBinding.getTypeMirror().toString();
    ClassName className = ClassName.bestGuess(packageNameStr);
    
    // $L/$T代表佔位符,$L為基本型別  $T為類型別
    // 這裡相當於生成了 target.btnAction=(Button)target.findViewById(2131165218);
    methodBuilder.addStatement("target.$L=($T)target.findViewById($L)",
                               fieldBinding.getName(),
                               className,
                               fieldBinding.getResId());
}
複製程式碼

方法完全構建完成後,我們將其新增到類裡面

typeBuilder.addMethod(methodBuilder.build());
複製程式碼

最後,我們通過 Filer 類來生成 Java 原始檔

try {
    //生成Java檔案,最終寫是通過filer類寫出的
    JavaFile.builder(packageName,result.build())
            .addFileComment("auto create make")
            .build()
            .writeTo(filer);
} catch (IOException e) {
    e.printStackTrace();
}
複製程式碼

四、lib-inject-核心 Api 模組

定義一個ViewBinder介面,之前說過,這個介面是給註解處理器自動生成的類來實現的,然後在其bind()方法裡面實現 findViewById 程式碼

public interface ViewBinder<T> {
    void bind(T target);
}
複製程式碼

接下來,定義一個核心類,其中的靜態方法bind()會傳入要繫結的 Activity,通過這個 Activity 的類名在執行時反射獲取到註解處理器生成的對應的輔助類,然後呼叫輔助類的bind方法完成控制元件的 findViewById

public class KButterKnife {

    public static void bind(Activity activity) {
        String className = activity.getClass().getName();
        try {
            Class<?> clazz = Class.forName(className+"$$ViewBinder");
            ViewBinder viewBinder = (ViewBinder) clazz.newInstance();
            viewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
    
}
複製程式碼

五、App-應用層

最後來測試使用一下,首先, 要依賴 lib-compiler 模組與 lib-inject 模組

implementation project(':lib-inject')
// lib-compiler 為註解處理器
annotationProcessor project(':lib-compiler')
複製程式碼

然後在 Activity 裡面使用

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.btnAction)
    Button btnAction;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        KButterKnife.bind(this);
        
        btnAction.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, 
                              "注入成功,哈哈哈", Toast.LENGTH_SHORT).show();
            }
        });
    }
}
複製程式碼

檢視生成的原始檔

一步步帶你實現簡版 ButterKnife

相關文章