註解(Annotation)自定義註解(四)--編譯期註解解析講解和手寫ButterKnife
註解(Annotation)自定義註解(四)–編譯期註解解析講解和手寫ButterKnife
前言
前面兩篇講解了執行期註解的使用和xutils原始碼的解析,以及手動打造自己的IOC框架。但是執行期註解由於效能問題被一些人所詬病,所以這裡我們講下編譯器註解的使用和實戰。
介紹
編譯器註解的核心原理依賴APT(Annotation Processing Tolls)實現,例如,我們常用的ButterKnife,Dagger,Retrofit等開源庫都是基於APT的。那麼編譯器註解是如何工作的呢?這就是我們要討論的內容。
編譯時Annotation註解的基本原理是,在某些程式碼元素上(比如型別,函式,欄位)添加註解,然後在編譯時編譯器會檢查AbstractProcessor的子類,並且呼叫該型別的process函式,然後將添加了註解的元素傳遞給process中,使得開發者可以在編譯期可以進行處理,比如,根據註解生成新的ava類,這也是BufferKnife等庫的基本原理。
編譯處理時,是分開進行的。如果在某個處理中產生的新的JAVA檔案,那麼就需要另外一個處理來處理新生成的原始檔,反覆進行,知道沒有新的檔案產生。在處理完檔案後,再進行編譯。JDK5提供了APT工具來對註解進行處理。編寫註解處理器和核心是:AnnotationProcessorFactory和AnnotationProcessor兩個介面,前者是為某些註解型別建立註解處理器工廠,後者是表示註解處理器。
對於編譯器,程式碼中元素的結構是不會變的。JDK中為這些元素定義了一個基本型別Element類,它有幾個子類:
PackageElement--包元素,包含包的資訊 TypeElement-------型別元素,描述欄位資訊 ExecutableElement--可執行元素,代表函式型別的元素 VariableElement------變數元素 TypeParameterElement--型別引數元素
使用上面的抽象來對於程式碼中的基本元素。
我們先來分析BuffKnife使用以及原始碼,然後在手動打造我們自己的框架。
BuffKnife使用
public class MainActivity extends AppCompatActivity { @Bind(R.id.icon) ImageView icon; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); } @OnClick(R.id.icon) public void onClick() { Toast.makeText(this, "圖片點選了", Toast.LENGTH_LONG).show(); } }
使用非常簡單,主要注意:屬性是不能private就行。
butterknife的原始碼解析
按照邏輯,我們應該先看ButterKnife.bind(this),下面是原始碼:
static void bind(Object target, Object source, Finder finder) {
Class<?> targetClass = target.getClass();
try {
if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
// 找到viewBinder
ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
if (viewBinder != null) {
// 直接執行方法
viewBinder.bind(finder, target, source);
}
} catch (Exception e) {
throw new RuntimeException("Unable to bind views for " +
targetClass.getName(), e);
}
}
如果從這裡看我們好像看不到任何的東西,其實工作流程是怎樣的呢?從上面介紹我們知道,編譯器會先呼叫AbstractProcessor的子類的process函式,我們可以看一下Bind這個Annotation註解類和ButterKnifeProcessor這兩個類:
ButterKnife 工作流程
@Retention(CLASS) @Target(FIELD)
public @interface Bind {
/** View ID to which the field will be bound. */
int[] value();
}
當你編譯你的Android工程時,ButterKnife工程中ButterKnifeProcessor類的process()方法會執行以下操作:
1.開始它會掃描Java程式碼中所有的ButterKnife註解@Bind、@OnClick、@OnItemClicked等。
2.當它發現一個類中含有任何一個註解時,ButterKnifeProcessor會幫你生成一個Java類,名字類似$$ViewBinder,這個新生成的類實現了ViewBinder介面。
3.這個ViewBinder類中包含了所有對應的程式碼,比如@Bind註解對應findViewById(), @OnClick對應了view.setOnClickListener()等等。
4.最後當Activity啟動ButterKnife.bind(this)執行時,ButterKnife會去載入對應的ViewBinder類呼叫它們的bind()方法。
現在我們總該明白為什麼我們的生成的屬性和方法不能私有了吧?我們最後看一下編譯時生成的class類吧
public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {
@Override public void bind(final Finder finder, final T target, Object source) {
View view;
view = finder.findRequiredView(source, 2131427372, "field 'icon' and method 'onClick'");
target.icon = finder.castView(view, 2131427372, "field 'icon'");
view.setOnClickListener(
new butterknife.internal.DebouncingOnClickListener() {
@Override public void doClick(View p0) {
target.onClick();
}
});
}
@Override public void unbind(T target) {
target.icon = null;
}
}
這裡沒有詳細分析ButterKnifeProcessor類,有興趣的同學可以自己看下。
手寫BuffKnife
必備知識點:
- 基本的註解知識
- 反射
- 編譯期註釋處理API
- JavaPoet(用來生成java原始檔,不是必須,可自己手動拼接)
- Auto (自動配置META-INF,不是必須,可以手動配置)
上面後面兩個工具不是必須的,如果感興趣,可以自己手動拼接java原始檔和手動配置Auto。
基本的配置:
這邊要注意幾點:
aptlib和annotationlib是java庫,不用android庫是因為androidSDK中沒有javax.annotation.*
android程式呼叫java庫時,需要在java庫中引入com.google.android:android:XXXX和com.android.support:support-annotations:XXXXjar包
想使用APT,專案要應用APT外掛:
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
然後引入javajar包:
apt project(':aptlib')
compile project(':annotationlib')
知道上面的知識,就可以開始寫我們的程式碼了。
讓我們先來看看註解類吧
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface ViewInjector {
int value();
}
//自動配置META-INF
@AutoService(Processor.class)
public class ViewInjectProcessor extends AbstractProcessor {
//在元素上進行操作的某些實用工具方法
private Elements elementUtils;
//用來建立新源、類或輔助檔案的 Filer
private Filer filer;
//用來在型別上進行操作的某些實用工具
private Types typeUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
typeUtils = processingEnv.getTypeUtils();
}
//返回支援的註解型別
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(ViewInjector.class.getCanonicalName());
}
//型別與欄位的關聯表,用於在寫入Java檔案時按型別來寫不同的檔案和欄位
final Map<String, List<VariableElement>> map = new HashMap<String, List<VariableElement>>();
//註解處理的核心方法
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//獲取使用ViewInjector註解的所有元素
Set<? extends Element> elementSet = roundEnv.getElementsAnnotatedWith(ViewInjector.class);
for (Element element:elementSet) {
checkAnnotationValid(element,ViewInjector.class);
//註解的欄位
VariableElement varElement= (VariableElement)element;
//型別的完整路徑名,比如某個Activity的完整路徑
String className = getParentClassName(varElement);
//獲取這個型別的所有註解,例如某個Activity中的所有View的註解物件
List<VariableElement> cacheElements = map.get(className);
if(cacheElements==null){
cacheElements = new LinkedList<VariableElement>();
}
//將元素新增到該型別對應的欄位列表中
cacheElements.add(varElement);
//以類的路徑為key,欄位列表為value,存入map.這裡是將所在欄位按所屬的型別進行分
map.put(className,cacheElements);
}
//生成java原始檔
generate();
return false;
}
//生成java原始檔
private void generate() {
Iterator<Map.Entry<String, List<VariableElement>>> iterator = map.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, List<VariableElement>> entry = iterator.next();
List<VariableElement> cacheElements = entry.getValue();
if (cacheElements == null || cacheElements.size() == 0) {
continue;
}
InjectorInfo info = InjectorInfoUtil.createInjectorInfo(processingEnv,cacheElements.get(0));
//下面全是JavaPoet的基本使用,不明白的可以點選上面的連結去看
final ClassName className = ClassName.get(info.packageName,info.classlName);
final ClassName InterfaceName=ClassName.get(InjectAdapter.class);
MethodSpec.Builder injectsBuilder = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(void.class)
.addParameter(className, "target");
for (VariableElement element:cacheElements) {
InjectorInfo temInfo = InjectorInfoUtil.createInjectorInfo(processingEnv,element);
ViewInjector annotation = element.getAnnotation(ViewInjector.class);
int value = annotation.value();
String fieldName = element.getSimpleName().toString();
String type = element.asType().toString();
injectsBuilder.addStatement("target." + fieldName + " = ("+type+")(target).findViewById(" + value + ")");
}
MethodSpec injects = injectsBuilder.build();
TypeSpec typeSpec = TypeSpec.classBuilder(info.newClassName)
.addSuperinterface(ParameterizedTypeName.get(InterfaceName, className))
.addModifiers(Modifier.PUBLIC)
.addMethod(injects)
.build();
JavaFile javaFile = JavaFile.builder(info.packageName, typeSpec).build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace(); }
}
}
/*列印錯誤的方法 */
protected void error(Element element, String message, Object... args) {
if (args.length > 0) { message = String.format(message, args);
}
processingEnv.getMessager().printMessage(ERROR, message, element);
}
//型別的完整路徑名,比如某個Activity的完整路徑
private String getParentClassName(VariableElement varElement) {
TypeElement typeElement = (TypeElement) varElement.getEnclosingElement();
String packageName = AnnotationUtil.getPackageName(processingEnv,typeElement);
return packageName + "." + typeElement.getSimpleName().toString();
}
}
測試:
public class MainActivity extends AppCompatActivity {
@ViewInjector(R.id.tv)
TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewInjectUtil.injectView(this);
tv.setText("fddfdfdfdfdfdfdfdfdfdf");
}
}
重新編譯一下,到當前目錄app\build\generated\source\apt\debug\com\github\essayjoke\可以看到MainActivity$InjectAdapter.java檔案,我們開啟看一下:
package github.com.stoneviewinject;
import com.example.InjectAdapter;
import java.lang.Override;
public class MainActivity$$InjectAdapter implements InjectAdapter<MainActivity> {
@Override
public void inject(MainActivity target) {
target.tv = (android.widget.TextView)(target).findViewById(2131492945);
}
}
我們再來看下是否真的可以使用:
看到這裡我就放心了,哈哈。
下面很簡單了,有興趣,可以自己根據BuffKnife的bind來實現。
註解的知識已經講完了,自己也學到很多東西,希望一直能夠這樣堅持下去。