Android APT 實現(xiàn)控件注入框架SqInject的示例
作者
大家好,我叫小鑫,也可以叫我蠟筆小鑫😊;
本人17年畢業(yè)于中山大學,于2018年7月加入37手游安卓團隊,曾經(jīng)就職于久邦數(shù)碼擔任安卓開發(fā)工程師;
目前是37手游安卓團隊的海外負責人,負責相關業(yè)務開發(fā);同時兼顧一些基礎建設相關工作。
背景
在游戲發(fā)行中,經(jīng)常需要切包,如果直接使用R.id.xxx,在回編譯時,由于resources.arsc會重新編譯,R類中的id值和resources.arsc中的對應關系會異常,導致程序異常。我們有兩種解決方法。
一種是在切包過程中糾正R類的值,實現(xiàn)方案具體介紹可以查看
游戲發(fā)行切包資源索引沖突解決方案,鏈接如下:
//www.dbjr.com.cn/article/207579.htm
另一種解決方案則是使用getIdentifier獲取資源ID,放棄使用R類,這種方式中,編碼較為麻煩,并且getIdentifier中需要寫的是字符串,容易寫錯,并且編譯過程中是發(fā)現(xiàn)不了的。基于這種情況,我們開發(fā)了基于getIdentifier的控件注入框架
一、APT技術(shù)簡介
1、APT定義
APT(Annotation Processing Tool)即注解處理器,是一種處理注解的工具,確切的說它是javac的一個工具,它用來在編譯時掃描和處理注解。注解處理器以Java代碼作為輸入,生成.java文件作為輸出
2、注解定義
1、注解是一種能被添加到java代碼中的元數(shù)據(jù),類、方法、變量、參數(shù)和包都可以用注解修飾。
2、注解對于它所修飾的代碼沒有直接的影響
3、APT原理簡介

Annotation processing是在編譯階段執(zhí)行的,它的原理就是讀入Java源代碼,解析注解,然后生成新的Java代碼。新生成的Java代碼最后被編譯成Java字節(jié)碼,注解解析器(Annotation Processor)不能改變讀入的Java 類,比如不能加入或刪除Java方法
二、APT實戰(zhàn)使用
1、SqInject框架來源
在手游發(fā)行中,經(jīng)常需要切包,將游戲接完SDK1的包,通過反編譯,替換smali文件及其他資源文件的方式,替換為渠道SDK2的渠道包。在這個反編譯回編譯的過程中,資源索引ID(即R類和resources.arsc中的ID映射關系)會發(fā)生沖突導致程序異常,即不做特殊處理的話,渠道SDK及發(fā)行SDK是不能直接使用R類的,要使用getIdentifier獲取資源ID
要求在程序中使用getIdentifier,在開發(fā)過程中是比較麻煩的事情。在這樣的條件下,我們也無法使用如butterknife這樣的框架。因此,我們模仿butterknife開發(fā)了一套基于getIdentifier的控件注入框架SqInject。下面介紹SqInject的實現(xiàn),先來看下簡單使用哈
public class MainActivity extends AppCompatActivity {
//綁定ID
@BindView(SqR.id.tv)
TextView hello;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SqInject.bind(this);
Log.e("SqInject", hello.getText().toString());
}
//綁定點擊事件
@OnClick(SqR.id.tv)
public void click(View view) {
Intent intent = new Intent(MainActivity.this, TestActivity.class);
startActivity(intent);
}
}
2、SqInject的實現(xiàn)原理
2.1、注解處理器模塊實現(xiàn)
上文說到APT常用于生成代碼,在SqInject中APT注解處理環(huán)節(jié)中,流程如下圖所示:

在編譯過程中掃描注解,生成Java代碼,而后再次編譯
在SqInject代碼中,實現(xiàn)如下:
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class SqInjectProcessor extends AbstractProcessor {
...
//核心方法
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//控件類注解解析,ResChecker檢查資源id合法性,合法則生成"類名+$ViewBinder類,否則編譯失敗
BindViewBuilder bindViewBuilder = new BindViewBuilder(roundEnvironment, mResChecker, mElementUtils, mTypeUtils, mFiler, mMessager);
bindViewBuilder.build();
//id類注解解析,ResChecker檢查資源id合法性,合法則生成"類名+$IdBinder類",否則編譯失敗
BindIdsBuilder bindIdsBuilder = new BindIdsBuilder(roundEnvironment, mResChecker, mElementUtils, mTypeUtils, mFiler, mMessager);
bindIdsBuilder.build();
return false;
}
}
在生成控件注入相關代碼之前,框架中會先檢測資源id的合法性,本框架中在使用注解時,傳入的是字符串。可資源名稱是有可能不存在對應資源的,框架會做相應的檢測
2.2、資源檢測
Android編譯資源過程中,會生成R類,也就是說只有在R類中存在的,用getIdentifier才能夠獲取到,那么我們可以用R類來檢測傳入的參數(shù)是否合理,代碼如下:
/**
* 檢測資源id在R文件中是否存在
* @param name
* @param type
* @return
*/
public boolean isIdValid(String name, String type) {
String RClassName = mPackageNeme + ".R." + type;
TypeElement RClassType = mElementUtils.getTypeElement(RClassName);
if (RClassType == null) {
mMessager.printMessage(Diagnostic.Kind.ERROR, RClassName + "不存在,請檢查是否包名有誤,或者類型錯誤");
} else {
//遍歷屬性
for (Element element : RClassType.getEnclosedElements()) {
String fieldName = element.getSimpleName().toString();
if (name.equals(fieldName)) {
return true;
}
}
}
return false;
}
2.3、解析注解,生成代碼
以解析BindView為例,代碼如下:
/**
* 解析BindView注解
* @return
*/
private Map<TypeElement, List<VariableElement>> parseBindView(){
Set<Element> elements = (Set<Element>) mRoundEnvironment.getElementsAnnotatedWith(BindView.class);
if (!Utils.isEmpty(elements)) {
Map<TypeElement, List<VariableElement>> map = new HashMap<>();
for (Element element : elements) {
if (element instanceof VariableElement) {
//獲取該屬性所在類
TypeElement targetElement = (TypeElement) element.getEnclosingElement();
mTargetSet.add(targetElement);
if (map.get(targetElement) == null) {
List<VariableElement> targetStringLists = new ArrayList<>();
targetStringLists.add((VariableElement) element);
map.put(targetElement, targetStringLists);
} else {
map.get(targetElement).add((VariableElement) element);
}
}
}
return map;
}
return null;
}
解析完BindView注解后,使用javapoet生成代碼,篇幅有限,下面僅列出獲取參數(shù)和生成代碼的一小部分
if (mBindViewIdTargetMap != null && mBindViewIdTargetMap.get(targetElement) != null) {
List<VariableElement> viewElements = mBindViewIdTargetMap.get(targetElement);
//方法體
for (VariableElement viewElement : viewElements) {
//獲取屬性名
String fieldName = viewElement.getSimpleName().toString();
//獲取類型
TypeMirror typeMirror = viewElement.asType();
TypeElement fieldClassElement = (TypeElement) mTypeUtils.asElement(typeMirror);
mMessager.printMessage(Diagnostic.Kind.NOTE, "注解的字段類型為: " + fieldClassElement.getQualifiedName().toString());
TypeElement fieldType = mElementUtils.getTypeElement(fieldClassElement.getQualifiedName());
ClassName fieldClassName = ClassName.get(fieldType);
//獲取@BindView注解的值,即名稱
String name = viewElement.getAnnotation(BindView.class).value();
//檢測名稱是否合法
if(!mResChecker.isIdValid(name, "id")){
mMessager.printMessage(Diagnostic.Kind.ERROR, "R文件中不存在id為" + name + "的值");
}
methodBuilder.addStatement("target.$N = $T.findRequiredViewAsType(source, $S, $S, $T.class)", fieldName, JptConstants.UTILS, name, "field " + fieldName,fieldClassName);
}
}
小小總結(jié)一下,在注解處理器中,最重要的兩個環(huán)節(jié),一個是解析注解,獲取參數(shù),然后是利用javapoet框架生成代碼
下面看下生成的代碼
public class MainActivity$ViewBinder implements ViewBinder<MainActivity> {
@Override
public void bindView(final MainActivity target, final View source) {
//這里就是上面的代碼生成的
target.hello = ViewUtils.findRequiredViewAsType(source, "tv", "field hello", TextView.class);
IdUtils.findViewByName("tv", source).setOnClickListener(new DebouncingOnClickListener() {
public void doClick(View v) {
target.click(v);
}
} );
}
}
到這里,就介紹完了使用APT生成代碼了哈。本文中提到的框架SqInject是我們?nèi)粘9ぷ髦袝褂玫降腟qInject框架,該框架以開源,地址是: github.com/37sy/SqInje… 有興趣的歡迎star哈
以上就是Android APT 實現(xiàn)控件注入框架SqInject的示例的詳細內(nèi)容,更多關于Android APT控件注入框架SqInject的資料請關注腳本之家其它相關文章!
相關文章
Android性能優(yōu)化之弱網(wǎng)優(yōu)化詳解
這篇文章主要為大家介紹了Android性能優(yōu)化之弱網(wǎng)優(yōu)化示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10
Android Studio與SVN版本控制程序的協(xié)作使用指南
這篇文章主要介紹了Android Studio與SVN版本控制程序的協(xié)作使用指南,使用Gradle插件自動填寫SVN號并發(fā)布到指定目錄的方法,需要的朋友可以參考下2016-03-03
Android實戰(zhàn)教程第七篇之如何在內(nèi)存中存儲用戶名和密碼
這篇文章主要為大家詳細介紹了Android如何實現(xiàn)在內(nèi)存中存儲用戶名和密碼的相關代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11
android實現(xiàn)上傳本地圖片到網(wǎng)絡功能
這篇文章主要為大家詳細介紹了android實現(xiàn)上傳本地圖片到網(wǎng)絡功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-09-09
Android實現(xiàn)手勢滑動多點觸摸縮放平移圖片效果(二)
這篇文章主要介紹了Android實現(xiàn)手勢滑動多點觸摸縮放平移圖片效果,實現(xiàn)圖片支持多點觸控,自由的進行縮放、平移的注意事項,感興趣的小伙伴們可以參考一下2016-02-02

