本文來自於騰訊bugly開發者社區,非經作者同意,請勿轉載,原文地址:http://dev.qq.com/topic/578753c0c9da73584b025875
話說我們做程序員的,都應該多少是個懶人,我們總是想辦法驅使我們的電腦幫我們干活,所以我們學會了各式各樣的語言來告訴電腦該做什麼——盡管,他們有時候也會誤會我們的意思。
突然有一天,我覺得有些代碼其實,可以按照某種規則生成,但你又不能不寫——不是所有的重復代碼都可以通過重構並采用高端技術比如泛型來消除的——比如我最痛恨的代碼:
TextView textView = (TextView) findViewById(R.id.text_view);
Button button = (Button) findViewById(R.id.button);
這樣的代碼,你總不能不寫吧,真是讓人沮喪。突然想到以前背單詞的故事:正著背背不過 C,倒著背背不過 V。嗯,也許寫 Android app,也是寫不過 findViewById
的吧。。
我們今天要介紹的 ButterKnife 其實就是一個依托 Java 的注解機制來實現輔助代碼生成的框架,讀完本文,你將能夠了解到 Java 的注解處理器的強大之處,你也會對 dagger2 和 androidannotations 這樣類似的框架有一定的認識。
說真的,我一直對於 findViewById
這個的東西有意見,後來見到了 Afinal 這個框架,於是我們就可以直接通過注解的方式來注入,哇塞,終於可以跟 findViewById
說『Byte Byte』了,真是好開心。
什麼?寨見不是介麼寫麼?
不過,畢竟是移動端,對於用反射實現注入的 Afinal 之類的框架,我們總是難免有一種發自內心的抵觸,於是。。。
別哭哈,不用反射也可以的~~
這個世界有家神奇的公司叫做 Square,裡面有個大神叫 Jake Wharton,開源了一個神奇的框架叫做 ButterKnife,這個框架雖然也采用了注解進行注入,不過人家可是編譯期生成代碼的方式,對運行時沒有任何副作用,果真見效快,療效好,只是編譯期有一點點時間成本而已。
說句題外話,現如今做 Android 如果不知道 Jake Wharton,我覺得面試可以直接 Pass 掉了。。。哈哈,開玩笑啦
怎麼介紹一個東西,那真是一個折學問題。別老說我沒文化,我的意思是比較曲折嘛。
我們還是要先簡單介紹一些 ButterKnife 的基本用法,這些知識你在 ButterKnife 這裡也可以看到。
簡單來說,使用 ButterKnife 需要三步走:
private MethodSpec createBindMethod() {
/*創建了一個叫做 bind 的方法,添加了 @Override 注解,方法可見性為 public
以及一些參數類型 */
MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(FINDER, "finder", FINAL)
.addParameter(TypeVariableName.get("T"), "target", FINAL)
.addParameter(Object.class, "source");
if (hasResourceBindings()) {
// Aapt can change IDs out from underneath us, just suppress since all will work at runtime.
result.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "ResourceType")
.build());
}
// Emit a call to the superclass binder, if any.
if (parentViewBinder != null) {
result.addStatement("super.bind(finder, target, source)");
}
/* 關於 unbinder,我們一直都沒有提到過,如果我們有下面的注入配置:
@Unbinder
ButterKnife.Unbinder unbinder;
* 那麼這時候就會在生成的代碼中添加下面的代碼,這實際上就是構造 unbinder
*/
// If the caller requested an unbinder, we need to create an instance of it.
if (hasUnbinder()) {
result.addStatement("$T unbinder = new $T($N)", unbinderBinding.getUnbinderClassName(),
unbinderBinding.getUnbinderClassName(), "target");
}
/*
* 這裡就是注入 view了,addViewBindings 這個方法其實就生成功能上類似
TextView textView = (TextView) findViewById(...) 的代碼
*/
if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {
// Local variable in which all views will be temporarily stored.
result.addStatement("$T view", VIEW);
// Loop over each view bindings and emit it.
for (ViewBindings bindings : viewIdMap.values()) {
addViewBindings(result, bindings);
}
// Loop over each collection binding and emit it.
for (Map.Entry<FieldCollectionViewBinding, int[]> entry : collectionBindings.entrySet()) {
emitCollectionBinding(result, entry.getKey(), entry.getValue());
}
}
/*
* 注入 unbinder
*/
// Bind unbinder if was requested.
if (hasUnbinder()) {
result.addStatement("target.$L = unbinder", unbinderBinding.getUnbinderFieldName());
}
/* ButterKnife 其實不止支持注入 View, 還支持注入 字符串,主題,圖片。。
* 所有資源裡面你能想象到的東西
*/
if (hasResourceBindings()) {
//篇幅有限,我還是省略掉他們吧
...
}
return result.build();
}
不知道為什麼,這段代碼讓我想起了我寫代碼的樣子。。那分明就是 ButterKnife 在替我們寫代碼嘛。
當然,這只是生成的代碼中最重要的最核心的部分,為了方便理解,我把 demo 裡面生成的這個方法列出來方便查看:
@Override
public void bind(final Finder finder, final T target, Object source) {
//構造 unbinder
Unbinder unbinder = new Unbinder(target);
//下面開始 注入 view
View view;
view = finder.findRequiredView(source, 2130968576, "field 'title'");
target.title = finder.castView(view, 2130968576, "field 'title'");
//... 省略掉其他成員的注入 ...
//注入 unbinder
target.unbinder = unbinder;
}
我一直覺得,既然 View 都能注入了,咱能不能把 layout 也注入了呢?顯然這沒什麼難度嘛,可為啥 Jake 大神沒有做這個功能呢?我覺得主要是因為。。。你想哈,你注入個 layout,大概要這麼寫
@BindLayout(R.layout.main)
public class AnyActivity extends Activity{...}
可我們平時怎麼寫呢?
public class AnyActivity extends Activity{
@Override
protected void onCreate(Bundle savedInstances){
super.onCreate(savedInstances);
setContentView(R.layout.main);
}
}
你別說你不繼承 onCreate
方法啊,所以好像始終要寫一句,性價比不高?誰知道呢。。。
不過呢,咱們接下來就運用我們的神功,給 ButterKnife 添磚加瓦(這怎麼感覺像校長說的呢。。嗯,他說的是社河會蟹主@義),讓 ButterKnife 可以 @BindLayout
。先看效果:
//注入 layout
@BindLayout(R.layout.simple_activity)
public class SimpleActivity extends Activity {
...
}
生成的代碼:
public class SimpleActivity$$ViewBinder<T extends SimpleActivity> implements ViewBinder<T> {
@Override
public void bind(final Finder finder, final T target, Object source) {
//生成了這句代碼來注入 layout
target.setContentView(2130837504);
//下面省略掉的代碼我們已經見過啦,就是注入 unbinder,注入 view
...
}
...
}
那麼我們要怎麼做呢?一個字,順籐摸瓜~
第一步,當然是要定義注解 BindLayout
@Retention(CLASS) @Target(TYPE)
public @interface BindLayout {
@LayoutRes int value();
}
第二步,我們要去注解處理器裡面添加對這個注解的支持:
@Override public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
...
types.add(BindLayout.class.getCanonicalName());
...
return types;
}
第三步,注解處理器的解析環節要添加支持:
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
Set<String> erasedTargetNames = new LinkedHashSet<>();
// Process each @Bind element.
for (Element element : env.getElementsAnnotatedWith(BindLayout.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseBindLayout(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindLayout.class, e);
}
}
...
}
下面是 parseBindLayout
方法:
private void parseBindLayout(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) {
/*與其他注解解析不同,BindLayout 標注的類型就是 TYPE,所以這裡直接強轉為
TypeElement,其實就是對應於 Activity 的類型*/
TypeElement typeElement = (TypeElement) element;
Set<Modifier> modifiers = element.getModifiers();
// 只有 private 不可以訪問到,static 類型不影響,這也是與其他注解不同的地方
if (modifiers.contains(PRIVATE)) {
error(element, "@%s %s must not be private. (%s.%s)",
BindLayout.class.getSimpleName(), "types", typeElement.getQualifiedName(),
element.getSimpleName());
return;
}
// 同樣的,對於 android 開頭的包內的類不予支持
String qualifiedName = typeElement.getQualifiedName().toString();
if (qualifiedName.startsWith("android.")) {
error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",
BindLayout.class.getSimpleName(), qualifiedName);
return;
}
// 同樣的,對於 java 開頭的包內的類不予支持
if (qualifiedName.startsWith("java.")) {
error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",
BindLayout.class.getSimpleName(), qualifiedName);
return;
}
/* 我們暫時只支持 Activity,如果你想支持 Fragment,需要區別對待哈,
因為二者初始化 View 的代碼不一樣 */
if(!isSubtypeOfType(typeElement.asType(), ACTIVITY_TYPE)){
error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
BindLayout.class.getSimpleName(), typeElement.getQualifiedName(), element.getSimpleName());
return;
}
// 拿到注解傳入的值,比如 R.layout.main
int layoutId = typeElement.getAnnotation(BindLayout.class).value();
if(layoutId == 0){
error(element, "@%s for a Activity must specify one layout ID. Found: %s. (%s.%s)",
BindLayout.class.getSimpleName(), layoutId, typeElement.getQualifiedName(),
element.getSimpleName());
return;
}
BindingClass bindingClass = targetClassMap.get(typeElement);
if (bindingClass == null) {
bindingClass = getOrCreateTargetClass(targetClassMap, typeElement);
}
// 把這個布局的值塞給 bindingClass,這裡我只是簡單的存了下這個值
bindingClass.setContentLayoutId(layoutId);
log(element, "element:" + element + "; targetMap:" + targetClassMap + "; erasedNames: " + erasedTargetNames);
}
第四步,添加相應的生成代碼的支持,這個在 BindingClass.createBindMethod
當中:
private MethodSpec createBindMethod() {
MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(FINDER, "finder", FINAL)
.addParameter(TypeVariableName.get("T"), "target", FINAL)
.addParameter(Object.class, "source");
if (hasResourceBindings()) {
... 省略之 ...
}
//如果 layoutId 不為 0 ,那說明有綁定,添加一句 setContentView 完事兒~~
//要注意的是,這句要比 view 注入在前面。。。你懂的,不然自己去玩空指針
if(layoutId != 0){
result.addStatement("target.setContentView($L)", layoutId);
}
...
}
這樣,我們就可以告別 setContentView
了,寫個注解,非常清爽,隨意打開個 Activity
一眼就看到了布局在哪裡,哈哈哈哈哈
其實是說你胖。。
androidannotations 同樣是一個注入工具,如果你稍微接觸一下它,你就會發現它的原理與 ButterKnife 如出一轍。下面我們給出其中非常核心的代碼:
private void processThrowing(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) throws Exception {
if (nothingToDo(annotations, roundEnv)) {
return;
}
AnnotationElementsHolder extractedModel = extractAnnotations(annotations, roundEnv);
AnnotationElementsHolder validatingHolder = extractedModel.validatingHolder();
androidAnnotationsEnv.setValidatedElements(validatingHolder);
try {
AndroidManifest androidManifest = extractAndroidManifest();
LOGGER.info("AndroidManifest.xml found: {}", androidManifest);
IRClass rClass = findRClasses(androidManifest);
androidAnnotationsEnv.setAndroidEnvironment(rClass, androidManifest);
} catch (Exception e) {
return;
}
AnnotationElements validatedModel = validateAnnotations(extractedModel, validatingHolder);
ModelProcessor.ProcessResult processResult = processAnnotations(validatedModel);
generateSources(processResult);
}
我們就簡單看下,其實也是注解解析和代碼生成幾個步驟,當然,由於 androidannotations 支持的功能要復雜的多,不僅僅包含 UI 注入,還包含線程切換,網絡請求等等,因此它的注解解析邏輯也要復雜得多,閱讀它的源碼時,建議多多關注一下它的代碼結構設計,非常不錯。
從使用的角度來說,ButterKnife 只是針對 UI 進行注入,功能比較單一,而 androidannotations 真是有些龐大和強大,究竟使用哪一個框架,那要看具體需求了。
Dagger 2 算是超級富二代了,媽是 Square,爹是 Google—— Dagger 2 源自於 Square 的開源項目,目前已經由 Google 接管(怎麼感覺 Google 喜當爹的節奏 →_→)。
Dagger 本是一把利刃,它也是用來注入成員的一個框架,不過相對於前面的兩個框架,它
顯得更復雜,因為它更關注於對象間的依賴關系
用它的開發者說的一句話就是(大意):有一天,我們發現我們的構造方法居然需要 3000 行,這時候我們意識到是時候寫一個框架幫我們完成構造方法了。
換句話說,如果你的構造方法沒有那麼長,其實也沒必要引入 Dagger 2,因為那樣會讓你的代碼顯得。。。不是那麼的好懂。
當然,我們放到這裡提一下 Dagger 2,是因為它 完全去反射,實現的思想與前面提到的兩個框架也是一毛一樣啊。所以你可以不假思索的說,Dagger 2 肯定至少有兩個模塊,一個是 compiler,裡面有個注解處理器;還有一個是運行時需要依賴的模塊,主要提供 Dagger 2 的注解支持等等。
本文通過對 ButterKnife 的源碼的分析,我們了解到了 ButterKnife 這樣的注入框架的實現原理,同時我們也對 Java 的注解處理機制有了一定的認識;接著我們還對 ButterKnife 進行了擴充的簡單嘗試——總而言之,使用起來非常簡單的 ButterKnife 框架的實現實際上涉及了較多的知識點,這些知識點相對生僻,卻又非常的強大,我們可以利用這些特性來實現各種各樣個性化的需求,讓我們的工作效率進一步提高。
來吧,解放我們的雙手!
騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合並功能幫助開發同學把每天上報的數千條 Crash 根據根因合並分類,每日日報會列出影響用戶數最多的崩潰,精准定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在發布後快速的了解應用的質量情況,適配最新的 iOS, Android 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!