APT(Annotation Processing Tool) 即註解處理器,是一種註解處理工具,用來在編譯期掃描和處理註解,經過註解來生成 Java 文件。即以註解做爲橋樑,經過預先規定好的代碼生成規則來自動生成 Java 文件。此類註解框架的表明有 ButterKnife、Dragger二、EventBus 等java
Java API 已經提供了掃描源碼並解析註解的框架,開發者能夠經過繼承 AbstractProcessor 類來實現本身的註解解析邏輯。APT 的原理就是在註解了某些代碼元素(如字段、函數、類等)後,在編譯時編譯器會檢查 AbstractProcessor 的子類,而且自動調用其 process() 方法,而後將添加了指定註解的全部代碼元素做爲參數傳遞給該方法,開發者再根據註解元素在編譯期輸出對應的 Java 代碼android
這裏以 ButterKnife 爲實現目標,在講解 Android APT 的內容的同時,逐步實現一個輕量的控件綁定框架,即經過註解來自動生成以下所示的 findViewById() 代碼git
package hello.leavesc.apt;
public class MainActivityViewBinding {
public static void bind(MainActivity _mainActivity) {
_mainActivity.btn_serializeSingle = (android.widget.Button) (_mainActivity.findViewById(2131165221));
_mainActivity.tv_hint = (android.widget.TextView) (_mainActivity.findViewById(2131165333));
_mainActivity.btn_serializeAll = (android.widget.Button) (_mainActivity.findViewById(2131165220));
_mainActivity.btn_remove = (android.widget.Button) (_mainActivity.findViewById(2131165219));
_mainActivity.btn_print = (android.widget.Button) (_mainActivity.findViewById(2131165218));
_mainActivity.et_userName = (android.widget.EditText) (_mainActivity.findViewById(2131165246));
_mainActivity.et_userAge = (android.widget.EditText) (_mainActivity.findViewById(2131165245));
_mainActivity.et_singleUserName = (android.widget.EditText) (_mainActivity.findViewById(2131165244));
_mainActivity.et_bookName = (android.widget.EditText) (_mainActivity.findViewById(2131165243));
}
}
複製代碼
控件綁定的方式以下所示github
@BindView(R.id.et_userName)
EditText et_userName;
@BindView(R.id.et_userAge)
EditText et_userAge;
@BindView(R.id.et_bookName)
EditText et_bookName;
複製代碼
首先在工程中新建一個 Java Library,命名爲 apt_processor,用於存放 AbstractProcessor 的實現類。再新建一個 Java Library,命名爲 apt_annotation ,用於存放各種註解json
當中,apt_processor 須要導入以下依賴緩存
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation 'com.squareup:javapoet:1.10.0'
implementation project(':apt_annotation')
}
複製代碼
當中,JavaPoet 是 square 開源的 Java 代碼生成框架,能夠很方便地經過其提供的 API 來生成指定格式(修飾符、返回值、參數、函數體等)的代碼。auto-service 是由 Google 開源的註解註冊處理器bash
實際上,上面兩個依賴庫並非必須的,能夠經過硬編碼代碼生成規則來替代,但仍是建議使用這兩個庫,由於這樣代碼的可讀性會更高,且能提升開發效率服務器
app Module 須要依賴這兩個 Java Libraryapp
implementation project(':apt_annotation')
annotationProcessor project(':apt_processor')
複製代碼
這樣子,咱們須要的全部基礎依賴關係就搭建好了框架
首先觀察自動生成的代碼,能夠概括出幾點須要實現的地方:
一、文件和源 Activity 處在同個包名下
二、類名以 Activity名 + ViewBinding 組成
三、bind() 方法經過傳入 Activity 對象來獲取其聲明的控件對象來對其進行實例化,這也是 ButterKnife 要求須要綁定的控件變量不能聲明爲 private 的緣由
package hello.leavesc.apt;
public class MainActivityViewBinding {
public static void bind(MainActivity _mainActivity) {
_mainActivity.btn_serializeSingle = (android.widget.Button) (_mainActivity.findViewById(2131165221));
_mainActivity.tv_hint = (android.widget.TextView) (_mainActivity.findViewById(2131165333));
...
}
}
複製代碼
在 apt_processor Module 中建立 BindViewProcessor 類並繼承 AbstractProcessor 抽象類,該抽象類含有一個抽象方法 process() 以及一個非抽象方法 getSupportedAnnotationTypes() 須要由咱們來實現
/**
* 做者:leavesC
* 時間:2019/1/3 14:32
* 描述:
* GitHub:https://github.com/leavesC
* Blog:https://www.jianshu.com/u/9df45b87cfdf
*/
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> hashSet = new HashSet<>();
hashSet.add(BindView.class.getCanonicalName());
return hashSet;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
複製代碼
getSupportedAnnotationTypes() 方法用於指定該 AbstractProcessor 的目標註解對象,process() 方法則用於處理包含指定註解對象的代碼元素
BindView 註解的聲明以下所示,放在 apt_annotation 中,註解值 value 用於聲明 viewId
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
複製代碼
要自動生成 findViewById() 方法,則須要獲取到控件變量的引用以及對應的 viewid,因此須要先遍歷出每一個 Activity 包含的全部註解對象
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//獲取全部包含 BindView 註解的元素
Set<? extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class);
Map<TypeElement, Map<Integer, VariableElement>> typeElementMapHashMap = new HashMap<>();
for (Element element : elementSet) {
//由於 BindView 的做用對象是 FIELD,所以 element 能夠直接轉化爲 VariableElement
VariableElement variableElement = (VariableElement) element;
//getEnclosingElement 方法返回封裝此 Element 的最裏層元素
//若是 Element 直接封裝在另外一個元素的聲明中,則返回該封裝元素
//此處表示的即 Activity 類對象
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
Map<Integer, VariableElement> variableElementMap = typeElementMapHashMap.get(typeElement);
if (variableElementMap == null) {
variableElementMap = new HashMap<>();
typeElementMapHashMap.put(typeElement, variableElementMap);
}
//獲取註解值,即 ViewId
BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
int viewId = bindAnnotation.value();
//將每一個包含了 BindView 註解的字段對象以及其註解值保存起來
variableElementMap.put(viewId, variableElement);
}
...
return true;
}
複製代碼
當中,Element 用於表明程序的一個元素,這個元素能夠是:包、類、接口、變量、方法等多種概念。這裏以 Activity 對象做爲 Key ,經過 map 來存儲不一樣 Activity 下的全部註解對象
獲取到全部的註解對象後,就能夠來構造 bind() 方法了
MethodSpec 是 JavaPoet 提供的一個概念,用於抽象出生成一個函數時須要的基礎元素,直接看如下方法應該就能夠很容易理解其含義了
經過 addCode() 方法把須要的參數元素填充進去,循環生成每一行 findView 方法
/**
* 生成方法
*
* @param typeElement 註解對象上層元素對象,即 Activity 對象
* @param variableElementMap Activity 包含的註解對象以及註解的目標對象
* @return
*/
private MethodSpec generateMethodByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) {
ClassName className = ClassName.bestGuess(typeElement.getQualifiedName().toString());
//方法參數名
String parameter = "_" + StringUtils.toLowerCaseFirstChar(className.simpleName());
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(className, parameter);
for (int viewId : variableElementMap.keySet()) {
VariableElement element = variableElementMap.get(viewId);
//被註解的字段名
String name = element.getSimpleName().toString();
//被註解的字段的對象類型的全名稱
String type = element.asType().toString();
String text = "{0}.{1}=({2})({3}.findViewById({4}));";
methodBuilder.addCode(MessageFormat.format(text, parameter, name, type, parameter, String.valueOf(viewId)));
}
return methodBuilder.build();
}
複製代碼
完整的代碼聲明以下所示
/**
* 做者:leavesC
* 時間:2019/1/3 14:32
* 描述:
* GitHub:https://github.com/leavesC
* Blog:https://www.jianshu.com/u/9df45b87cfdf
*/
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> hashSet = new HashSet<>();
hashSet.add(BindView.class.getCanonicalName());
return hashSet;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//獲取全部包含 BindView 註解的元素
Set<? extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class);
Map<TypeElement, Map<Integer, VariableElement>> typeElementMapHashMap = new HashMap<>();
for (Element element : elementSet) {
//由於 BindView 的做用對象是 FIELD,所以 element 能夠直接轉化爲 VariableElement
VariableElement variableElement = (VariableElement) element;
//getEnclosingElement 方法返回封裝此 Element 的最裏層元素
//若是 Element 直接封裝在另外一個元素的聲明中,則返回該封裝元素
//此處表示的即 Activity 類對象
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
Map<Integer, VariableElement> variableElementMap = typeElementMapHashMap.get(typeElement);
if (variableElementMap == null) {
variableElementMap = new HashMap<>();
typeElementMapHashMap.put(typeElement, variableElementMap);
}
//獲取註解值,即 ViewId
BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
int viewId = bindAnnotation.value();
//將每一個包含了 BindView 註解的字段對象以及其註解值保存起來
variableElementMap.put(viewId, variableElement);
}
for (TypeElement key : typeElementMapHashMap.keySet()) {
Map<Integer, VariableElement> elementMap = typeElementMapHashMap.get(key);
String packageName = ElementUtils.getPackageName(elementUtils, key);
JavaFile javaFile = JavaFile.builder(packageName, generateCodeByPoet(key, elementMap)).build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
/**
* 生成 Java 類
*
* @param typeElement 註解對象上層元素對象,即 Activity 對象
* @param variableElementMap Activity 包含的註解對象以及註解的目標對象
* @return
*/
private TypeSpec generateCodeByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) {
//自動生成的文件以 Activity名 + ViewBinding 進行命名
return TypeSpec.classBuilder(ElementUtils.getEnclosingClassName(typeElement) + "ViewBinding")
.addModifiers(Modifier.PUBLIC)
.addMethod(generateMethodByPoet(typeElement, variableElementMap))
.build();
}
/**
* 生成方法
*
* @param typeElement 註解對象上層元素對象,即 Activity 對象
* @param variableElementMap Activity 包含的註解對象以及註解的目標對象
* @return
*/
private MethodSpec generateMethodByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) {
ClassName className = ClassName.bestGuess(typeElement.getQualifiedName().toString());
//方法參數名
String parameter = "_" + StringUtils.toLowerCaseFirstChar(className.simpleName());
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(className, parameter);
for (int viewId : variableElementMap.keySet()) {
VariableElement element = variableElementMap.get(viewId);
//被註解的字段名
String name = element.getSimpleName().toString();
//被註解的字段的對象類型的全名稱
String type = element.asType().toString();
String text = "{0}.{1}=({2})({3}.findViewById({4}));";
methodBuilder.addCode(MessageFormat.format(text, parameter, name, type, parameter, String.valueOf(viewId)));
}
return methodBuilder.build();
}
}
複製代碼
首先在 MainActivity 中聲明兩個 BindView 註解,而後 Rebuild Project,使編譯器根據 BindViewProcessor 生成咱們須要的代碼
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_hint)
TextView tv_hint;
@BindView(R.id.btn_hint)
Button btn_hint;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
複製代碼
rebuild 結束後,就能夠在 generatedJava 文件夾下看到 MainActivityViewBinding 類自動生成了
此時有兩種方式能夠用來觸發 bind() 方法
/**
* 做者:leavesC
* 時間:2019/1/3 14:34
* 描述:
* GitHub:https://github.com/leavesC
* Blog:https://www.jianshu.com/u/9df45b87cfdf
*/
public class ButterKnife {
public static void bind(Activity activity) {
Class clazz = activity.getClass();
try {
Class bindViewClass = Class.forName(clazz.getName() + "ViewBinding");
Method method = bindViewClass.getMethod("bind", activity.getClass());
method.invoke(bindViewClass.newInstance(), activity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
兩種方式各有優缺點。第一種方式在每次 build project 後纔會生成代碼,在這以前沒法引用到對應的 ViewBinding 類。第二種方式能夠用固定的方法調用方式,可是相比方式一,反射會略微多消耗一些性能
但這兩種方式的運行結果是徹底相同的
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MainActivityViewBinding.bind(this);
tv_hint.setText("leavesC Success");
btn_hint.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "Hello", Toast.LENGTH_SHORT).show();
}
});
}
複製代碼
經過第一節的內容,讀者應該瞭解到了 APT 其強大的功能了 。這一節再來實現一個能夠方便地將 對象進行持久化+序列化+反序列 的框架
一般,咱們的應用都會有不少配置項須要進行緩存,好比用戶信息、設置項開關、服務器IP地址等。若是採用原生的 SharedPreferences 來實現的話,則很容易就寫出以下醜陋的代碼,不只須要維護多個數據項的 key 值,並且每次存入和取出數據時都會有一大片重複的代碼,不易維護
SharedPreferences sharedPreferences = getSharedPreferences("SharedPreferencesName", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("IP", "192.168.0.1");
editor.commit();
String userName = sharedPreferences.getString("userName", "");
String ip = sharedPreferences.getString("IP", "");
複製代碼
所以,這裏就來經過 APT 來實現一個能夠方便地對數據進行 持久化+序列化+反序列化 的框架,具體的目標有如下幾點:
一、能夠將 Object 進行序列化,而且提供反序列化爲 Object 的方法
二、Object 的序列化結果能夠持久化保存到本地
三、持久化數據時須要的惟一 key 值由框架內部自動進行維護
四、序列化、反序列化、持久化的具體過程由框架外部實現,框架只負責搭建操做邏輯
目標1能夠經過 Gson 來實現,目標2則能夠經過使用騰訊開源的 MMKV 框架來實現,須要導入如下兩個依賴
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.tencent:mmkv:1.0.16'
複製代碼
這裏先預先看下框架的使用方式。新的註解以 Preferences 命名,假設 User 類中有三個字段值須要進行本地緩存,所以都爲其加上 Preferences 註解
public class User {
@Preferences
private String name;
@Preferences
private int age;
@Preferences
private Book book;
...
}
複製代碼
而咱們要作的,就是經過 APT 自動爲 User 類來生成一個 UserPreferences 子類,以後的數據緩存操做都是經過 UserPreferences 來進行
緩存整個對象
User user = new User();
UserPreferences.get().setUser(user);
複製代碼
緩存單個屬性值
String userName = et_singleUserName.getText().toString();
UserPreferences.get().setName(userName);
複製代碼
獲取緩存的對象
User user = UserPreferences.get().getUser();
複製代碼
移除緩存的對象
UserPreferences.get().remove();
複製代碼
能夠看到,整個操做都是十分的簡潔,以後就來開工吧
爲了實現目標4,須要先定義好操做接口,並由外部傳入具體的實現
public interface IPreferencesHolder {
//序列化
String serialize(String key, Object src);
//反序列化
<T> T deserialize(String key, Class<T> classOfT);
//移除指定對象
void remove(String key);
}
複製代碼
以上三個操做對於框架內部來講應該是惟一的,所以能夠經過單例模式來全局維護。APT 生成的代碼就經過此入口來調用 持久化+序列化+反序列化 方法
public class PreferencesManager {
private IPreferencesHolder preferencesHolder;
private PreferencesManager() {
}
public static PreferencesManager getInstance() {
return PreferencesManagerHolder.INSTANCE;
}
private static class PreferencesManagerHolder {
private static PreferencesManager INSTANCE = new PreferencesManager();
}
public void setPreferencesHolder(IPreferencesHolder preferencesHolder) {
this.preferencesHolder = preferencesHolder;
}
public IPreferencesHolder getPreferencesHolder() {
return preferencesHolder;
}
}
複製代碼
在 Application 的 onCreate() 方法中傳入具體的實現
PreferencesManager.getInstance().setPreferencesHolder(new PreferencesMMKVHolder());
複製代碼
public class PreferencesMMKVHolder implements IPreferencesHolder {
@Override
public String serialize(String key, Object src) {
String json = new Gson().toJson(src);
MMKV kv = MMKV.defaultMMKV();
kv.putString(key, json);
return json;
}
@Override
public <T> T deserialize(String key, Class<T> classOfT) {
MMKV kv = MMKV.defaultMMKV();
String json = kv.decodeString(key, "");
if (!TextUtils.isEmpty(json)) {
return new Gson().fromJson(json, classOfT);
}
return null;
}
@Override
public void remove(String key) {
MMKV kv = MMKV.defaultMMKV();
kv.remove(key);
}
}
複製代碼
同樣是須要繼承 AbstractProcessor 類,子類命名爲 PreferencesProcessor
首先,PreferencesProcessor 類須要生成一個序列化整個對象的方法。例如,須要爲 User 類生成一個子類 UserPreferences ,UserPreferences 包含一個 setUser(User instance) 方法
public String setUser(User instance) {
if (instance == null) {
PreferencesManager.getInstance().getPreferencesHolder().remove(KEY);
return "";
}
return PreferencesManager.getInstance().getPreferencesHolder().serialize(KEY, instance);
}
複製代碼
對應的方法生成規則以下所示。能夠看出來,大致規則仍是和第一節相似,同樣是須要經過字符串來拼接出完整的代碼。當中,T 都是替代符,做用相似於 MessageFormat
/**
* 構造用於序列化整個對象的方法
*
* @param typeElement 註解對象上層元素對象,即 Java 對象
* @return
*/
private MethodSpec generateSetInstanceMethod(TypeElement typeElement) {
//頂層類類名
String enclosingClassName = ElementUtils.getEnclosingClassName(typeElement);
//方法名
String methodName = "set" + StringUtils.toUpperCaseFirstChar(enclosingClassName);
//方法參數名
String fieldName = "instance";
MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PUBLIC)
.returns(String.class)
.addParameter(ClassName.get(typeElement.asType()), fieldName);
builder.addStatement("if ($L == null) { $T.getInstance().getPreferencesHolder().remove(KEY); return \"\"; }", fieldName, serializeManagerClass);
builder.addStatement("return $T.getInstance().getPreferencesHolder().serialize(KEY, $L)", serializeManagerClass, fieldName);
return builder.build();
}
複製代碼
此外,還須要一個用於反序列化本地緩存的數據的方法
public User getUser() {
return PreferencesManager.getInstance().getPreferencesHolder().deserialize(KEY, User.class);
}
複製代碼
對應的方法生成規則以下所示
/**
* 構造用於獲取整個序列化對象的方法
*
* @param typeElement 註解對象上層元素對象,即 Java 對象
* @return
*/
private MethodSpec generateGetInstanceMethod(TypeElement typeElement) {
//頂層類類名
String enclosingClassName = ElementUtils.getEnclosingClassName(typeElement);
//方法名
String methodName = "get" + StringUtils.toUpperCaseFirstChar(enclosingClassName);
MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get(typeElement.asType()));
builder.addStatement("return $T.getInstance().getPreferencesHolder().deserialize(KEY, $L.class)", serializeManagerClass, enclosingClassName);
return builder.build();
}
複製代碼
爲了實現目標三(持久化數據時須要的惟一 key 值由框架內部自動進行維護),在持久化時使用的 key 值由當前的 包名路徑+類名 來決定,由此保證 key 值的惟一性
例如,UserPreferences 類緩存數據使用的 key 值是
private static final String KEY = "leavesc.hello.apt.model.UserPreferences";
複製代碼
對應的方法生成規則以下所示
/**
* 定義該註解類在序列化時使用的 Key
*
* @param typeElement 註解對象上層元素對象,即 Java 對象
* @return
*/
private FieldSpec generateKeyField(TypeElement typeElement) {
return FieldSpec.builder(String.class, "KEY")
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("\"" + typeElement.getQualifiedName().toString() + SUFFIX + "\"")
.build();
}
複製代碼
其餘相應的 get 和 set 方法生成規則就再也不贅述了,有興趣研究的同窗能夠下載源碼閱讀
修改 MainActivity 的佈局
public class MainActivity extends AppCompatActivity {
@BindView(R.id.et_userName)
EditText et_userName;
@BindView(R.id.et_userAge)
EditText et_userAge;
···
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ButterKnife.bind(this);
MainActivityViewBinding.bind(this);
btn_serializeAll.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String userName = et_userName.getText().toString();
String ageStr = et_userAge.getText().toString();
int age = 0;
if (!TextUtils.isEmpty(ageStr)) {
age = Integer.parseInt(ageStr);
}
String bookName = et_bookName.getText().toString();
User user = new User();
user.setAge(age);
user.setName(userName);
Book book = new Book();
book.setName(bookName);
user.setBook(book);
UserPreferences.get().setUser(user);
}
});
btn_serializeSingle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String userName = et_singleUserName.getText().toString();
UserPreferences.get().setName(userName);
}
});
btn_remove.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
UserPreferences.get().remove();
}
});
btn_print.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
User user = UserPreferences.get().getUser();
if (user == null) {
tv_hint.setText("null");
} else {
tv_hint.setText(user.toString());
}
}
});
}
}
複製代碼
數據的整個存取過程自我感受仍是十分的簡單的,不用再本身去維護臃腫的 key 表,且能夠作到存取路徑的惟一性,仍是能夠提升一些開發效率的