手把手教你实现Android编译期注解

手把手教你实现Android编译期注解

2023年6月28日发(作者:)

⼿把⼿教你实现Android编译期注解⼀、编译期注解在开发中的重要性从早期令⼈惊艳的ButterKnife,到后来的以ARouter为⾸的各种路由框架,再到现在⾕歌⼤⼒推⾏的Jetpack组件,越来越多的第三⽅框架都在使⽤编译期注解这门技术,可以说不管你是想要深⼊研究这些第三⽅框架的原理 还是要成为⼀个Android⾼级开发⼯程师,编译期注解都是你不得不好好掌握的⼀门基础技术。本⽂从基础的运⾏期注解⽤法开始,逐步演进到编译期注解的⽤法,让你真正明⽩编译期注解到底应该在什么场景下使⽤,怎么⽤,⽤了有哪些好处。⼆、⼿写运⾏期注解类似下⾯这种写法,当View⼀多得不停的findViewById 写很多⾏,⼿写起来很⿇烦,我们⾸先尝试⽤运⾏期注解来解决这个问题,看看能不能⾃动处理这些findViewById的操作。⾸先是⼯程结构,肯定要定义⼀个lib module。其次定义我们的注解类:有了这个注解的类,我们就可以在我们的MainAcitivity先⽤起来,虽然此时这个注解还并未起到什么作⽤。到这⾥要稍微想⼀下,此时我们要做的是 通过注解来将 赋值给对应的field,也就是你定义的那些view对象(例如红框中的tv),对于我们的lib⼯程来说,因为是MainActivity 要依赖lib,⾃然你lib不可以依赖Main所属的app⼯程了,这⾥有2个原因:A依赖B ,B依赖A的循环依赖是肯定会报错的;既然你要做⼀个lib 那你肯定不能依赖使⽤者的宿主 否则怎么能叫lib呢?所以这个问题就变成了,lib⼯程 只能拿到Acitivty,拿不到宿主的MainActivity , 既然拿不到宿主的MainActivity,那我怎么知道这个activity有多少个field?这⾥就要⽤到反射了。public class BindingView { public static void init(Activity activity) { Field[] fields = ss().getDeclaredFields(); for (Field field : fields) { //获取 被注解 BindView annotation = otation(); if (annotation != null) { int viewId = (); essible(true); try { (activity, ewById(viewId)); } catch (IllegalAccessException e) { tackTrace(); } } } }}最后我们在宿主的MainActivity中调⽤⼀下这个⽅法 即可:到这⾥其实有⼈就要问了,这个运⾏时注解看起来也不难啊,为啥好像⽤的⼈不是很多?问题就出在刚才反射的那堆⽅法⾥,反射⼤家都知道 会对Android运⾏时带来⼀些性能损耗,⽽这⾥的代码是⼀段循环, 也就是说这⾥的代码会随着你使⽤lib的Activity的界⾯复杂程度的提⾼ ⽽变得越来越慢,这是⼀个会随着你界⾯复杂度提⾼⽽逐步劣化的过程, 单次反射对于今天的⼿机来说⼏乎已经不存在什么性能消耗了,但是这种for循环中使⽤反射还是尽量少⽤。三、⼿写编译期注解为了解决这个问题,就要使⽤编译期注解。现在我们来尝试⽤编译期注解来解决上述的问题。前⾯我们说过,运⾏期注解可以⽤反射来拿到宿主的field 从⽽完成需求,为了解决反射的性能问题,我们其实想要的代码是这样的:我们可以在app 的module 中新建⼀个MainActivityViewBinding的类:然后在我们的BindingView(注意我们的BindingView是在lib module下的)中来调⽤这个⽅法不就解决这个反射的问题了吗?但是这⾥会有个问题 就是你既然是⼀个lib 你不能依赖宿主 ,所以在lib Module 中你其实拿不到 MainActivityViewBinding 这个类的,还是得利⽤反射。可以看⼀下上⾯注释掉的代码,为啥不直接字符串写死?因为你是lib库你当然得是动态的,不然怎么给别⼈⽤?其实就是获取宿主的class名称然后加上⼀个固定的后缀ViewBinding 即可。这个时候 我们就拿到这个Binding的class了,对吧,剩下就是调⽤构造⽅法即可。public class BindingView { public static void init(Activity activity) { try { Class bindingClass = e(ss().getCanonicalName() + "ViewBinding"); Constructor constructor = laredConstructor(ss()); tance(activity); } catch (ClassNotFoundException | NoSuchMethodException e) { tackTrace(); } catch (IllegalAccessException e) { tackTrace(); } catch (InstantiationException e) { tackTrace(); } catch (InvocationTargetException e) { tackTrace(); } }}看下此时的代码结构:有⼈这⾥要问,这⾥你不还是⽤了反射么,对! 这⾥虽然⽤了反射,但是我这⾥的反射只会调⽤⼀次,不管你的activity有都少field,在我这⾥反射⽅法都只会执⾏⼀次。所以性能肯定是⽐之前的⽅案要快很多倍的。接着看,虽然此刻代码可以正常运⾏,但是还有⼀个问题, 虽然我可以在lib中调⽤到我们app宿主的类的构造⽅法,但是,宿主的这个类依旧是我们⼿写的啊?那你这个lib库 还是没有起到任何可以让我们少写代码的作⽤。这个时候就需要我们的apt 出场了,也就是编译期注解的核⼼部分了。我们创建⼀个Java Library,注意是Java lib不是android lib,然后在appmodule中引⼊他。注意 引⼊的⽅式 不是imp了,是annotation processor ;然后我们来修改⼀下lib_processor,⾸先创建⼀个 注解处理类:再创建⽂件resources/META-INF/services/sor ,这⾥要注意 ⽂件夹创建不要写错了。然后再这个Processor指定 ⼀下我们的注解处理器即可:到这⾥还没完,我们得告诉这个注解处理器 只处理我们的BindView注解即可,否则这个注解处理器默认处理全部注解 速度就太慢了,但是此时我们的BindView这个注解类还在lib仓⾥⾯,显然我们要调整⼀下我们的⼯程结构:我们再新建⼀个Javalib,只放BindView即可,然后让我们的lib_processor和app 都依赖这个lib_interface即可。再稍微修改⼀下代码,此时我们是编译期处理,所Policy不⽤是runtime了。@Retention()@Target()public @interface BindView { int value();}public class BindingProcessor extends AbstractProcessor { Messager messager; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { messager = sager(); essage(, " BindingProcessor init"); (processingEnvironment); } @Override public boolean process(Set set, RoundEnvironment roundEnvironment) { return false; } //要⽀持哪些注解 @Override public Set getSupportedAnnotationTypes() { return ton(onicalName()); }}到此我们的⼤部分⼯作就处理完毕了。再看⼀下代码结构(这⾥的代码结构⼀定要理解清楚为什么这样设计,否则你是学不会编译期注解的)。我们现在已经能够做到 通过 lib 这个sdk 调⽤到MainActivityViewBinding这个⾥⾯的⽅法,但是他 还在app仓是我们⼿写的,不太智能,还没办法⽤。我们需要在注解处理器⾥⾯ ,动态的⽣成这个类,只要能完成这个步骤,那我们的SDK也就基本完成了。这⾥要提⼀下,很多⼈注解始终学不会就是卡在这⾥,因为太多的⽂章或者教程上来就是Javapoet 那⼀套代码,压根学不会,或者只能复制粘贴别⼈的东西,稍微变动⼀下就不会了,其实这⾥最佳的学习⽅式是先⽤StringBuffer 字符串拼接的⽅式 拼出我们想要的代码就可以了,通过这个字符串拼接的过程 来理解对应的api以及⽣成java代码的思路,然后最后再⽤JavaPoet来优化代码即可。我们可以先思考⼀下, 如果⽤字符串拼接的⽅式来做这个⽣成类的操作要完成哪些步骤。⾸先要获取哪些类使⽤了我们的BindView注解;获取这些类中使⽤了BindView注解的field以及他们对应的值;拿到这些类的类名称以便我们⽣成诸如MainActivityViewBinding这样的类名;拿到这些类的包名,因为我们⽣成的类要和注解所属的类属于同⼀个package 才不会出现field 访问权限的问题;上述条件都具备以后 就⽤字符串拼接的⽅式 拼接出我们想要的java代码 即可。这⾥就直接上代码了,重要部分 直接看注释即可,有了上⾯的步骤分析再看代码注释应该不难理解。public class BindingProcessor extends AbstractProcessor { Messager messager; Filer filer; Elements elementUtils; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { //主要是输出⼀些重要的⽇志使⽤ messager = sager(); //你就理解成最终我们写java⽂件 要⽤到的重要 输出参数即可 filer = er(); //⼀些⽅便的utils⽅法 //⼀些⽅便的utils⽅法 elementUtils = mentUtils(); //这⾥要注意的是 是可以让编译失败的 ⼀些重要的参数校验可以⽤这个来提⽰⽤户你哪⾥写的不对 essage(, " BindingProcessor init"); (processingEnvironment); } private void generateCodeByStringBuffer(String className, List elements) throws IOException { //你要⽣成的类 要和 注解的类 同属⼀个package 所以还要取 package的名称 String packageName = kageOf((0)).getQualifiedName().toString(); StringBuffer sb = new StringBuffer(); // 每个java类 的开头都是 ("package "); (packageName); (";n"); // public class XXXActivityViewBinding { final String classDefine = "public class " + className + "ViewBinding { n"; (classDefine); //定义构造函数的开头 String constructorName = "public " + className + "ViewBinding(" + className + " activity){ n"; (constructorName); //遍历所有element ⽣成诸如 =ewById() 之类的语句 for (Element e : elements) { ("activity." + pleName() + "=ewById(" + otation().value() + ");n"); } ("n}"); ("n }"); //⽂件内容确定以后 直接⽣成即可 JavaFileObject sourceFile = SourceFile(className + "ViewBinding"); Writer writer = iter(); (ng()); (); } @Override public boolean process(Set set, RoundEnvironment roundEnvironment) { // key 就是使⽤注解的class的类名 element就是使⽤注解本⾝的元素 ⼀个class 可以有多个使⽤注解的field Map> fieldMap = new HashMap<>(); // 这⾥ 获取到 所有使⽤了 BindView 注解的 element for (Element element : mentsAnnotatedWith()) { //取到 这个注解所属的class的Name String className = losingElement().getSimpleName().toString(); //取到值以后 判断map中 有没有 如果没有就直接put 有的话 就直接在这个value中增加⼀个element if ((className) != null) { List elementList = (className); (element); } else { List elements = new ArrayList<>(); (element); (className, elements); } } //遍历map,开始⽣成辅助类 for (> entry : et()) { try { generateCodeByStringBuffer((), ue()); } catch (IOException e) { tackTrace(); } } return false; } //要⽀持哪些注解 @Override @Override public Set getSupportedAnnotationTypes() { return ton(onicalName()); }}最后看下效果:虽然⽣成的代码格式不太好看,但是运⾏起来是ok的。这⾥要注意⼀下Element 这个接⼝,实际上使⽤编译期注解的时候 如果能够理解了Element,那后续的⼯作就简单不少。主要关注Element的这5个⼦类即可,举个例⼦:package tionlib_2;//PackageElement |表⽰⼀个包程序元素// TypeElement 表⽰⼀个类或接⼝程序元素。public class VivoTest { //VariableElement |表⽰⼀个字段、enum 常量、⽅法或构造⽅法参数、局部变量或异常参数。 int a; //VivoTest 这个⽅法 :ExecutableElement|表⽰某个类或接⼝的⽅法、构造⽅法或初始化程序(静态或实例),包括注释类型元素。 //int b 这个函数参数: TypeParameterElement |表⽰⼀般类、接⼝、⽅法或构造⽅法元素的形式类型参数。 public VivoTest(int b ) { this.a = b; }}四、Javapoet⽣成代码有了上⾯的基础 再⽤ Javapoet 写⼀遍字符串拼接来⽣成java代码的过程, 就不会难以理解了。private void generateCodeByJavapoet(String className, List elements) throws IOException { //声明构造⽅法 r constructMethodBuilder = uctorBuilder().addModifiers().addParameter(ess(className), "activity"); //构造⽅法⾥⾯ 增加语句 for (Element e : elements) { tement("activity." + pleName() + "=ewById(" + otation().value() + ");"); } //声明类 TypeSpec viewBindingClass = uilder(className + "ViewBinding").addModifiers().addMethod(()).build(); String packageName = kageOf((0)).getQualifiedName().toString(); JavaFile build = r(packageName, viewBindingClass).build(); o(filer);}这⾥要提⼀下,现在越来越多的⼈使⽤Kotlin语⾔开发app,你甚⾄可以使⽤/square/kotl… 来直接⽣成Kotlin代码。有兴趣的可以尝试⼀下。五、编译期注解的总结⾸先是⼤家关注的性能⽅⾯,对于运⾏时注解来说,会产⽣⼤量的反射代码,⽽且反射调⽤的次数会随着项⽬复杂度的提⾼⽽变的越来越多,是⼀个逐步劣化的过程,⽽对于编译期注解来说,反射的调⽤次数是固定的,他并不会随着项⽬复杂度的提⾼⽽变的性能越来越差,实际上对于⼤多数运⾏时注解的项⽬都可以通过编译期注解来⼤幅提⾼框架的性能,⽐如著名的Dagger、EventBus 等等,他们的⾸个版本都是运⾏时注解,后续版本都统⼀替换成了编译期注解。其次回顾⼀下前⾯我们编译期注解的开发流程以后,可以得出以下⼏点结论:编译期注解只能⽣成代码,但是不能修改代码;注解⽣成的代码 必须要⼿动被调⽤,他⾃⼰是不会被调⽤的;对于SDK的编写者来说,即使是编译期注解,往往也免不了⾄少要⾛⼀次反射,⽽反射的作⽤主要就是调⽤你注解处理器⽣成的代码。这⾥可能会有⼩伙伴问,既然编译期注解只能⽣成代码不能修改代码,那作⽤很有限啊,为啥不直接⽤类似于ASM 、Javassist 等字节码⼯具呢,这些⼯具不但可以⽣成代码⽽且还可以修改代码,功能更强劲。因为这些字节码⼯具⽣成的直接是class,且写法复杂容易出错,也不易于调试,⼩规模写⼀下类似于防⽌快速点击之类的东西还可以,⼤规模开发第三⽅框架其实也挺不⽅便的,远远不如编译期注解来的效率⾼。此外,再仔细想想,我们前⽂中提到的编译期注解的写法做成第三⽅库给别⼈使⽤以后,还是需要使⽤者⼿动的在合适的时机调⽤⼀下 “init”⽅法的,但是有些出⾊的第三⽅库可以做到连init⽅法都不需要使⽤者⼿动调⽤了,使⽤起来⾮常⽅便,这⼜是怎么做到的?其实也不难,多数情况都是这些第三⽅库⽤编译期注解⽣成了代码以后,再配合ASM等字节码⼯具直接帮你调⽤了init⽅法 ,从⽽让你免去⼿动调⽤的过程。核⼼仍旧是编译期注解,只不过是⽤字节码⼯具省略了⼀步⽽已。

发布者:admin,转转请注明出处:http://www.yc00.com/news/1687955139a60589.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信