现在分析使用各种第三方库,诸如ARouter、DBFlow、Dagger2、ButterKnife等,自定义注解都是绕不过去的点。所以本文在此重新说叨一下Android的自定义注解,并分享一些自定义注解使用技巧给大家。
[TOC]
注解概念
注解是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。这些额外的工作包含但不限于比如用于生成Java doc,比如编译时进行格式检查,比如自动生成代码等。
定义注解用的关键字是@interface
JDK定义的元注解
Java提供了四种元注解,专门负责新注解的创建工作,即注解其他注解。
@Target
定义了Annotation所修饰的对象范围,取值:
• ElementType.CONSTRUCTOR: 用于描述构造器
• ElementType.FIELD: 用于描述域
• ElementType.LOCAL_VARIABLE: 用于描述局部变量
• ElementType.METHOD: 用于描述方法
• ElementType.PACKAGE: 用于描述包
• ElementType.PARAMETER: 用于描述参数
• ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明
@Retention
定义了该Annotation作用时机,及生成的文件的保留时间,取值:
• RetentionPoicy.SOURCE: 注解保留在源代码中,编译过程中可见,编译后会被编译器所丢弃,所以用于一些检查性操作,编译过程可见性分析等。比如@Override, @SuppressWarnings
• RetentionPoicy.CLASS: 这是默认的policy。注解会被保留在class文件中,但是在运行时期间就不会识别这个注解。用于生成一些辅助代码,辅助代码生成之后,该注解的任务就结束了。如ARouter、ButterKnife等
• RetentionPoicy.RUNTIME: 注解会被保留在class文件中,同时运行时期间也会被识别,和CLASS的差别也在此。所以可以在运行时使用反射机制获取注解信息。比如@Deprecated
@Inherited
是否可以被继承,默认为false。即子类自动拥有和父类一样的注解。
@Documented
是否会保存到 Javadoc 文档中。
Android SDK内置的注解
Android SDK 内置的注解都在包com.android.support:support-annotations里,如:
• 资源引用限制类:用于限制参数必须为对应的资源类型
@AnimRes @AnyRes @ArrayRes @AttrRes @BoolRes @ColorRes等
• 线程执行限制类:用于限制方法或者类必须在指定的线程执行
@AnyThread @BinderThread @MainThread @UiThread @WorkerThread
• 参数为空性限制类:用于限制参数是否可以为空
@NonNull @Nullable
• 类型范围限制类:用于限制标注值的值范围
@FloatRang @IntRange
• 类型定义类:用于限制定义的注解的取值集合
@IntDef @StringDef
• 其他的功能性注解:
@CallSuper @CheckResult @ColorInt @Dimension @Keep @Px @RequiresApi @RequiresPermission @RestrictTo @Size @VisibleForTesting
自定义注解实例
假定要实现这样的注解功能:使用注解在Android Activity上指定path,然后根据类名获取相应的path并实现跳转。
具体实现步骤如下
1) 创建Processor Module
File -- New Module -- Choose Java Library
确保该processor module package命名为 {base}.annotationprocessor
本例中module名定位为:compiler
2) 设置Processor Module Build Gradle
设置Java编译版本,如主app和processor module都采用java 1.7,则设置如下:
// 主app module
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
// processor module
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
添加谷歌Auto-Service支持
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.auto.service:auto-service:1.0-rc2'
}
3) 创建Annotation
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface TrackName {
String name() default "";
}
4) 创建自定义Processor
这一步是最重要的一个步骤,自定义注解之所以能实现相应的功能,就是看自定义Processor如何解析了。针对TrackName,我们需要做的就是在编译期生成一个Java文件,自动将@TrackName标注的类和标注的信息记录下来。
CustomProcessor必须继承AbstractProcessor,并且需要使用Java提供的注解标注:
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({ANNOTATION_TYPE_TRACKNAME})
先定义一个通用接口
public interface IData {
/**
* 载入数据
*/
void loadInto(Map<String, String> map);
}
TrackNameProcessor的实现如下:
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({ANNOTATION_TYPE_TRACKNAME})
public class TrackNameProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (set != null && !set.isEmpty()) {
generateJavaClassFile(set, roundEnvironment);
return true;
}
return false;
}
// 生成Java源文件
private void generateJavaClassFile(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// set of track
Map<String, String> trackMap = new HashMap<>();
// print on gradle console
Messager messager = processingEnv.getMessager();
// 遍历annotations获取annotation类型 @SupportedAnnotationTypes
for (TypeElement te : annotations) {
for (Element e : roundEnv.getElementsAnnotatedWith(te)) { // 获取所有被annotation标注的元素
// 打印
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.toString());
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.getSimpleName());
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.getEnclosingElement().toString());
// 获取注解
TrackName annotation = e.getAnnotation(TrackName.class);
// 获取名称
String name = "".equals(annotation.name()) ? e.getSimpleName().toString() : annotation.name();
// 保存映射信息
trackMap.put(e.getSimpleName().toString(), name);
messager.printMessage(Diagnostic.Kind.NOTE, "映射关系:" + e.getSimpleName().toString() + "-" + name);
}
}
try {
// 生成的包名
String genaratePackageName = "com.xud.annotationprocessor";
// 生成的类名
String genarateClassName = "TrackManager$Helper";
// 创建Java文件
JavaFileObject f = processingEnv.getFiler().createSourceFile(genarateClassName);
// 在控制台输出文件路径
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + f.toUri());
Writer w = f.openWriter();
try {
PrintWriter pw = new PrintWriter(w);
pw.println("package " + genaratePackageName + ";\n");
pw.println("import java.util.Map;");
pw.println("import com.xud.annotationprocessor.IData;\n");
pw.println("/**");
pw.println("* this file is auto-create by compiler,please don`t edit it");
pw.println("* 页面路径映射关系表");
pw.println("*/");
pw.println("public class " + genarateClassName + " implements IData {");
pw.println("\n @Override");
pw.println(" public void loadInto(Map<String, String> map) {");
Iterator<String> keys = trackMap.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
String value = trackMap.get(key);
pw.println(" map.put(\"" + key + "\",\"" + value + "\");");
}
pw.println(" }");
pw.println("}");
pw.flush();
} finally {
w.close();
}
} catch (IOException x) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, x.toString());
}
}
}
这里我把编译中生成的类贴出来,如下:
package com.xud.annotationprocessor;
import java.util.Map;
import com.xud.annotationprocessor.IData;
/**
* this file is auto-create by compiler,please don`t edit it
* 页面路径映射关系表
*/
public class TrackManager$Helper implements IData {
@Override
public void loadInto(Map<String, String> map) {
map.put("RxJavaActivity","/page/rxJava");
map.put("CustomViewActivity","/page/customView");
map.put("CoordinatorActivity","/page/coordinator");
map.put("BActivity","/page/b");
map.put("MainActivity","/main");
}
}
5) Use
接下来就是如何在主app module中使用了。由于本例annotation 和 processor都写在同一个module中,所以使用时通过如下方式引入即可:
api project(':compiler')
annotationProcessor project(':compiler')
为方便从统一的地方获取Activity和path的映射信息,创建TrackManager单例来获取:
public interface TrackInfoProvide {
/**
* 通过类名查找足迹定义信息
*
* @param className
* @return
*/
String getTrackNameByClass(String className);
/**
* 将所有路径信息打印出来
*/
String getAllPagePath();
}
public class TrackManager implements TrackInfoProvide {
private Map<String, String> trackNameMap;
private static TrackManager instance;
public static TrackManager getInstance() {
if (instance == null) {
instance = new TrackManager();
}
return instance;
}
private TrackManager() {
trackNameMap = new HashMap<String,String>();
String classFullName = "com.xud.annotationprocessor.TrackManager$Helper";
try {
Class<?> clazz = Class.forName(classFullName);
IData data = (IData)clazz.newInstance();
data.loadInto(trackNameMap);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public String getTrackNameByClass(String className) {
String output = className;
if(trackNameMap != null && !trackNameMap.isEmpty()) {
String value = trackNameMap.get(className);
output = (value == null?output:value);
}
return output;
}
@Override
public String getAllPagePath() {
if (trackNameMap.isEmpty()) {
return "";
}
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, String> entry : trackNameMap.entrySet()) {
builder.append("页面:" + entry.getKey());
builder.append("\t");
builder.append("路径:" + entry.getValue());
builder.append("\n");
}
return builder.toString();
}
}
这样,就可以直接通过以下方法实现调用
TrackManager.getInstance().getAllPagePath();
TrackManager.getInstance().getTrackNameByClass("MainActivity");
自定义注解使用技巧
-
从结构上来讲,应将Annotation定义和Annotation Processor实现分别写到不同的module,方便调用方按需使用;
-
Processor生成的代码应该面向接口编程,以示例代码为例,面向接口IData生成代码,将信息写入IData传入的Map对象中,这就无需关心生成的代码结构,在实现中应用反射创建IData的实例并依接口调用。
关于第二点,这里简要的说说ARouter的实现:
假设loginModule中的MainActivity使用了ARouter,其注解为@Route(path = "/loginModule/main"),则ARouter编译时会生成文件
ARouter$$Root$$loginModule
“ARouter$$Group$$loginModule”
public class ARouter$$Root$$loginModule implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("loginModule", ARouter$$Group$$loginModule.class);
}
}
public class ARouter$$Group$$loginModule implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/loginModule/main", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/loginmodule/main", "loginmodule", null, -1, -2147483648));
}
}
public interface IRouteRoot {
/**
* Load routes to input
* @param routes input
*/
void loadInto(Map<String, Class<? extends IRouteGroup>> routes);
}
public interface IRouteGroup {
/**
* Fill the atlas with routes in group.
*/
void loadInto(Map<String, RouteMeta> atlas);
}
ARouter在初始化的时候需要将这个映射关系载入内存的,其载入的方式就是通过反射来操作的,具体实现在源码中的类 LogisticsCenter,其中有一段代码如下,有兴趣的读者可以详阅源码:
// These class was generate by arouter-compiler.
List<String> classFileNames = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
for (String className : classFileNames) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
// This one of root elements, load root.
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
// Load interceptorMeta
((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
// Load providerIndex
((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
}
}
网友评论