前言
最近在看有关运行时注解的相关内容,在android studio 上开发遇到了不少的坑,希望通过这篇博客来总结这几天来的成果。
与编译时注解有关的类和方法
### 相关的类
这些方法会在接下来的工程中有所运用
代码编写 android studio 下开发
该案例是来源博客使用编译时注解方式实现View注入(Android Studio),该案例是实现类似ButterKnife的View注入, 自己在android studio 上开发还是遇到了不少的坑
1. 新建工程
在本案例中只建立了三个工程,ioc-annotation ,ioc-compiler 和 annotationtTest工程
- ioc-compiler 是 java Library 这个是一个Java Library,一定不能为Android Library ,也不能被Android模块的dependencies中使用compile引用,不然会找不到javax相关的类。主要用来处理注解,并生成相关的代码。
- ioc-annotation 是 android Libraray 被android模块调用实现View的ViewInject
- annotationtTest 是普通android 工程 存放测试案例
2 代码编写
2.1 编写注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int viewId();
}
2.2 编写ViewInjector 接口
public interface ViewInjector<T> {
public void inject(T t, Object obj);
}
2.3 编写 ViewInjectProcessor
2.3.1 什么是注解解释器 Processor
- 注解需要通过注解处理器进行处理,所有的注解处理器都实现了Processor接口,一般我们选择继承AbstractProcessor来创建自定义注解处理器。
继承AbstractProcessor,实现public boolean process(Set annotations, RoundEnvironment roundEnv)方法。方法参数中annotations包含了该处理器声明支持并已经在源码中使用了的注解,roundEnv则包含了注解处理的上下文环境。 此方法返回true时,表示此注解已经被处理完毕,返回false时将会交给其他处理器继续处理。 - 覆盖getSupportedSourceVersion方法,返回处理器支持的源码版本,一般直接返回SourceVersion.latestSupported()即可。
覆盖getSupportedAnnotationTypes方法,返回处理器想要处理的注解类型,此处需返回一个包含了所有注解完全限定名的集合。
在Java 7及以上,可以使用类注解@SupportedAnnotationTypes和@SupportedSourceVersion替代上面的方法进行声明。
2.3.2 代码编写
// JDK 7 以后使用
//@SupportedSourceVersion(SourceVersion.RELEASE_7)
//@SupportedAnnotationTypes({"com.zs.annotation.BindView"})
@AutoService(Processor.class)
public class ViewInjectProcessor extends AbstractProcessor {
private Elements elementsUtils;
private Filer filer; //文件的写入
private Messager messager; //打印Log信息之类
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.elementsUtils = processingEnv.getElementUtils();
this.filer = processingEnv.getFiler();
this.messager = processingEnv.getMessager();
}
/**
* @param annotations
* @param roundEnv
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Map<String, ElementInfo> maps = new HashMap<>();
// 拿到所有具有该注解的Elemnet
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
// 拿到该element的封装的TypeElement
for (Element element : elements) {
if (!element.getKind().isField()) {
continue;
}
// VariableElement 表示一个字段,局部变量等
VariableElement variableElement = (VariableElement) element;
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
String typeEleName = typeElement.getQualifiedName().toString();
ElementInfo elementInfo = maps.get(typeEleName);
if (elementInfo == null) {
elementInfo = new ElementInfo(typeElement, elementsUtils);
maps.put(typeEleName, elementInfo);
}
BindView bindView = variableElement.getAnnotation(BindView.class);
int viewId = bindView.viewId();
elementInfo.elementMap.put(viewId, variableElement);
}
for (Map.Entry<String, ElementInfo> entry : maps.entrySet()) {
try {
ElementInfo ei = entry.getValue();
JavaFileObject javaFileObject = filer.createSourceFile(ei.getProxyClassName(), ei.getTypeElement());
Writer writer = javaFileObject.openWriter();
//TODO
writer.write(ei.generateJavaCode());
writer.flush();
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return true;
}
/**
* 返回支持的注解类型
*
* @return
* @SupportedAnnotationTypes({"com.zs.annotation.BindView"})代替
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationType = new LinkedHashSet<>();
annotationType.add(BindView.class.getCanonicalName());
return annotationType;
}
/**
* 返回支持的源码版本
* 基本默认为下面的形式就行
* 也可以用注解@SupportedSourceVersion(SourceVersion.RELEASE_7)代替
*
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
class ElementInfo {
private final static String SUFFIX = "ViewInjector";
private TypeElement typeElement;
private Elements elementUtils;
public Map<Integer, Element> elementMap = new HashMap<>();
public ElementInfo(TypeElement typeElement, Elements elementUtils) {
this.elementUtils = elementUtils;
this.typeElement = typeElement;
}
public String getProxyClassName() {
return typeElement.getSimpleName() + "$$" + SUFFIX;
}
public String getProxyPackgeName() {
String fullName = this.typeElement.getQualifiedName().toString();
return fullName.substring(0, fullName.lastIndexOf("."));
}
public Element getTypeElement() {
return this.typeElement;
}
/**
* 生成代理类的代码
*
* @return
*/
public String generateJavaCode() {
StringBuilder builder = new StringBuilder();
builder.append("package " + this.getProxyPackgeName()).append(";\n\n");
builder.append("import com.zs.*;\n");
builder.append("public class ").append(this.getProxyClassName()).append(" implements " + SUFFIX + "<" + this.typeElement.getQualifiedName() + ">");
builder.append("\n{\n");
generateMethod(builder);
builder.append("\n}\n");
return builder.toString();
}
private void generateMethod(StringBuilder builder) {
builder.append("public void inject("+this.typeElement.getQualifiedName()+" host , Object object )");
builder.append("\n{\n");
for(int id : elementMap.keySet()){
VariableElement variableElement =(VariableElement) elementMap.get(id);
String name = variableElement.getSimpleName().toString();
String type = variableElement.asType().toString() ;
// messager.printMessage(Diagnostic.Kind.ERROR,"id="+id);
builder.append(" if(object instanceof android.app.Activity)");
builder.append("\n{\n");
builder.append("host."+name).append(" = ");
builder.append("(" + type + ")(((android.app.Activity)object).findViewById(" + id + "));");
//messager.printMessage(Diagnostic.Kind.ERROR, "id2=" + id);
builder.append("\n}\n").append("else").append("\n{\n");
builder.append("host."+name).append(" = ");
builder.append("("+type+")(((android.view.View)object).findViewById("+id+"));");
builder.append("\n}\n");
}
builder.append("\n}\n");
}
}
}
2.4 编写 Ioc 即通过反射来调用通过编译时注解自动生成的类
public class Ioc {
private final static String SUFFIX = "ViewInjector";
public static void inject(Activity activity) {
inject(activity, activity);
}
public static void inject(Object host, Object root) {
try {
String prefixName = host.getClass().getName();
//拿到动态生成类的全名称
String fullName = prefixName + "$$" + SUFFIX;
Class proxyClass = Class.forName(fullName);
ViewInjector viewInjector = (ViewInjector) proxyClass.newInstance();
viewInjector.inject(host, root);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.5 编写测试用例
public class MainActivity extends AppCompatActivity {
@BindView(viewId = R.id.tv)
public TextView tv;
@BindView(viewId = R.id.btn)
Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
Ioc.inject(this);
// button = (Button) findViewById(R.id.btn);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tv.setText("通过编译时注解改变");
}
});
}
}
3. 踩过的坑
3. 1 Java Library 和 android Library 以及 android module之间的依赖问题
在android studio1.5上,将android Library 添加为Java Library 依赖时可以添加成功但不能导入android Library中的类,不知道是为什么
3.2 non-zero exit value 2 错误
Error:Execution failed for task ':app:transformClassesWithDexForDebug'.
> com.android.build.api.transform.TransformException: com.android.ide.common.process.ProcessException: org.gradle.process.internal.ExecException: Process 'command 'F:\Program Files (x86)\Java\jdk1.8.0_31\bin\java.exe'' finished withnon-zero exit value 2
解决
这个错误在app的build.gradle里面添加下面这句就好了。
android {
defaultConfig {
...
multiDexEnabled true }
}
3.3 bad class file magic (cafebabe) or version (0034.0000)
Error:com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000) 和
Error:Execution failed for task ':app:transformClassesWithDexForDebug'.
> com.Android.build.api.transform.TransformException: com.android.ide.common.process.ProcessException: org.gradle.process.internal.ExecException: Process 'command 'F:\Program Files (x86)\Java\jdk1.8.0_31\bin\java.exe'' finished with non-zero exit value 1
解决
在新建的java Libraray 的 build.gradle中添加 下文中的红色内容
apply plugin: 'java'
dependencies {
//compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.google.auto.service:auto-service:1.0-rc2'
targetCompatibility = JavaVersion.VERSION_1_7
sourceCompatibility = JavaVersion.VERSION_1_7
}
3.4 通过编译时注解生成的代码存放目录问题
在这篇博文 使用编译时注解方式实现View注入(Android Studio)中说动态生成的代码存放的目录为app->build->generated->source->apt->debug,如果没有显示该目录可以尝试clean,然后菜单栏->Build0->Make Project就可以了 ,但在我的android studio 1.5 上该方法并不起作用,但在目app\build\intermediates\classes\debug\下找到了动态生成的代码
3.5 新建工程问题
在android studio 中用来存放Processor,即要存放自定义的注解解释器的工程一定要是 java Library ,不然就找不到AbstractProcessor这个类
4. 参考资料
http://blog.youkuaiyun.com/qduningning/article/details/51485869
http://my.oschina.net/u/134491/blog/663124
http://blog.youkuaiyun.com/lmj623565791/article/details/51931859
https://moxun.me/archives/63?utm_source=tuicool&utm_medium=referral