组件化路由框架存在的意义 在一些复杂的业务场景下,业务需求灵活性强,很多功能都是动态配置的,可以提前进行配置,进行预部署。
组件化的本质是解耦合,解耦合的关键是解决页面之间的依赖关系。
考虑到性能的关系,要求我们的路由框架支持编译期注解。
路由框架主要就是解决业务模块之间跳转、路由拦截等需求。
怎么实现? EventBus? 广播? 隐式意图 类加载Style? 自己用数据结构Map实现?
APT工作流程
定义注解(@xxxX)
APT扫描代码中的注解
APT依据定义好的注解处理方式进行操作,生成java文件
build工程,生成.class文件
AnnotationProcessor AnnotationProcessor是APT工具中的一种,是google开发的内置框架,不需要引入,可以直接在build.gradle文件中使用。
AnnotationProcessor和Provided的区别 AnnotatignPrcessgr只在编译的时候执行依赖的库,但是库最终不打包到apk中,编译库中的代码没有直接使用的意义,也没有提供开放的api调用,最终的目的是得到编译库中生成的文件,供我们调用。
Provided虽然也是编译时执行,最终不会打包到apk中,但是跟AnngtatignProcessor有着根本的不同。
传统页面路由操作方式缺陷
多人协同开发的时候,大家都去AndroidManifest.xml中定义各种IntentEilter,爆炸
登录、埋点这种非常通用的逻辑怎么处理?AOP怎么控制?
1 2 3 4 Intent intent = new Intent (mContext, XXActivity.class);intent.putExtra("key" "value" ); startActivity(intent); startActivityForResult(intent, 100 );
组件化页面路由优势
多模块分开发
允许自定义拦截
提供IOC容器
提供降级处理
支持InstanRun
Aroute路由怎么处理的? ARouter
路由的本质是键值对
通过APT技术,寻找到所有带有注解@Router的组件,将其注解值path和对应的Activity保存到一个map里,可以吗?内存处理问题?
对于一个大型项目,组件数量会很多,可能会有一两百或者更多,把这么多组件都放到这个Map里,显然会对内存造成很大的压力。怎么解决?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 android { compilesdkVersion 28 defaultconfig{ javaCompileoptions{ annotationProcessorOptions{ arguments = [moduleName:project .getName()] } } } dependencies { api 'com.alibaba:arouter-api:1.3.1' annotationProcessorcom.alibaba:arouter-compiler:1.1 .4 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class MyApplication extends Application { @Override public void oncreate () { super .onCreate(); ARouter.openLog(); ARouter.openDebug(); ARouter.init( application: this ); } @Override public void onTerminate () { super .onTerminate(); ARouter.getInstance().destroy(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Route(path="/app/Activity1") public class Activity1 extends AppCompatActivity { @Autowired String name; @Autowired int age; @Autowired(name="test") Bean bean; private Textview key_tv; private TextView name tv; @Override protected void onCreate (Bundle savedInstancestate) { super .onCreate(savedInstancestate); setcontentview(R.layout.activity2); ARouter.getInstance().inject( thiz: this ); key_tv= findviewById(R.id.key tv); name tv=findviewById(R.id.name tv); key_tv.setText("name" + name + "age==>" +age + "" ); name_tv.setText(bean+"" ); } }
1 2 3 4 5 6 7 ARouter.getInstance() .build(path: "/app/Activity1" ) .withInt("age" ,38 ) .withstring("name" , "John" ) .withParcelable("test" ,new Gean ( luge: "luge" ,i: 38 )) .navigation();
路由实践 基础实现 单例模式保证路由表只有一份
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Router { private static volatile Router mInstance; private Router () {} public static Router getInstance () { if (mInstance == null ){ synchronized (Router.class){ if (mInstance == null ){ mInstance = new Router (); } } } return mInstance; } private static Map<String,class<? extends Activity >> routers = new HashMap <>(); }
基于单例模式构建,主要包含路由表的创建与维护、页面跳转以及路由注册功能。
单例模式确保了整个应用中仅有一份路由表实例,避免了数据的不一致性。
在路由表中存储了路径与 Activity 类的映射关系,通过提供跳转方法,依据传入的路径从路由表获取对应的 Activity 类,进而创建 Intent 实现页面跳转。
路由注册过程存在缺陷,所有模块的路由信息均在 Application 的 onCreate 方法中集中注册,当模块数量增多时,会导致 Application 类极度臃肿,代码的可维护性与扩展性显著下降。
跳转 实现跳转的方法,传递需要跳转的路径,从路由表中找到对应的Activity
1 2 3 4 5 6 7 public void startActivity (Activity activity,string path) { Class<? extends Activity > cls = routers.get(path); if (cls != null ){ Intent intent = new Intent (activity, cls); activity.startActivity(intent); } }
使用时通过
1 Router.getInstance().startActivity( activity: this , path: "/wm/main" )
注册 将数据写入路由表
1 2 3 public void regiest (String path, Class<? extends Activity> cls) { routers.put(path,cls); }
在application启动的时候完成注册 这个类在app模块中,出于所有模块的最上层,可以拿到所有子模块类的引用
1 2 3 4 5 6 7 8 9 10 11 public class MTApplication extends Application { @Override public void oncreate () { super .onCreate(); } Router.getInstance().register( path:"/wm/main" , WMActivity.class); Router.getInstance().register( path: "/food/main" , FoodActivity.class); Router.getInstance().register( path: "/food/main" , FoodActivity.class): }
1个模块需要注册10个路由信息,10个模块 Application极度臃肿
去除中心化 下沉注册项 ,解决原始实现中 Application 过于臃肿的问题
为每个子模块都添加一个注册类,用来注册路由信息,每个子类继承于父类
通过创建 IRouteLoad 接口,让每个子模块实现该接口并负责自身路由信息的注册,从而将注册逻辑分散到各个子模块。
同时,利用反射机制在 Router 类中动态加载并调用这些子模块的注册类,实现路由信息的整合。
但此方法存在局限性,由于 Router 位于底层,业务模块在上层,底层无法直接知晓上层的类,只能借助反射动态获取,这在一定程度上影响了性能与代码的简洁性。
定义接口与子模块注册类 1 2 3 4 5 6 7 8 9 10 11 12 13 public interface IRouteLoad { void loadInto (Map<String, Class<? extends Activity>> routers) ; } public class ARouter implements IRouteLoad { @Override public void loadInto (Map<String, Class<? extends Activity>> routers) { routers.put("/food/main" , FoodActivity.class); } }
在 Router 类中整合注册 在Router类中调用注册方法
1 2 3 4 5 public static void init (Application application) { new FoodRouter ().loadInto(routers); new WMRouter ().loadInto(routers); }
反射实现动态注册 怎么才能拿到实例? -> 反射
1 2 3 4 5 6 7 8 9 10 try { Class<?> cls = Class.forName("com.enjoy.routers.FoodRouter" ); IRouteLoad o = (IRouteLoad) cls.newInstance(); o.loadInto(routers); } catch (Exception e) { e.printStackTrace(); }
但是这样写不行,Route在底层,业务模块子类在上层,底层是不知道上层的类的,只有上层知道下层
怎么做?
编译流程:class ->dex-> apk 1、拿到apk,找到所有的类(pms) 2、从apk dex中找到所有com.enjoy.routers包下同时实现了IRouteLoad的类(ClassLoader)
所有安装过的apk都会复制一份在/data/app/…/base.apk中,通过pms可以拿到
通过pms(包管理器)获得程序所有的apk(instant run会产生很多split apk)
1 2 3 4 5 6 7 8 9 10 11 12 13 private static List<String> getSourcePaths (Context context) throws PackageManager.NameNotFoundException { ApplicationInfo applicationInfo = context.getPackageManager.getApplicationInfo(context.getPackageName(),flags:0 ); List<String>sourcePaths =new ArrayList <>(); sourcePaths.add(applicationInfo.sourceDir); if (BUiLd.VERSION.SDK INT>= BUiLd.VERSION CODES.LOLLIPOP){ if (null != applicationInfo.splitSourceDirs) { sourcePaths.addAll(Arrays.asList(applicationInfo.splitsourceDirs)); } } return sourcePaths; }
通过包名返回类的名称
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static Set<String> getfileNameByPackageName (Application context, final stringpackageName) throws PackageManager.NameNotFoundException { final set<string> classNames = new Hashset <>(); List<String> paths = getSourcePaths(context); for (final string path :paths) { DexFile dexfile = null ; try { dexfile =new DexFile (path); Enumeration<String> dexFntries =dexfile.entries(); while (dexEntries.hasMoreElements()){ String className = dexntries.nextElement(); if (className.startsWith(packageName)){ classNames.add(className); } } } catch (IExceptione) { e.printStackTrace(); } finally { if (null != dexfile){ try { dexfile.close(); } catch (IOException e){ e.printStackTrace(); } } } } return classNames; }
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 try { Set<String>classNames = ClassUtils.getFileNameByPackageName(application,packageName:"com.enjoy.routers" ); for (String className :classNames){ Class<?> cls =Class.forName(className); if (IRouteLoad.class.isAssignableFrom(cls)){ IRouteLoad load = (IRouteLoad) cls.newInstance(); load.loadInto(routers); } } }catch (Exception e) { e.printstackTrace(): }
APT自动生成路由注册类 APT:annotation processor tools 注解处理器
其原理是在编译期间扫描源代码中的注解信息,然后根据这些注解生成额外的代码文件,从而实现自动化的代码生成,减少手动编写重复代码的工作量并降低出错概率。在路由框架中,利用 APT 可以自动生成路由注册类,使得路由信息的注册更加智能和高效。
监听:监听javac编译java文件,有指定的注解,就回调注解处理器代码
定义注解 定义一个用于标记路由类或方法的注解
1 2 3 4 5 6 7 8 9 10 import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface Route { String path () ; }
编写 APT 处理器 创建一个类继承自 AbstractProcessor,并重写 process 方法来处理注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 package com.enjoy.router.compiler;import java.util.set;import javax.annotation.processing.AbstractProcessor;import javax.annotation.processing.RoundEnvironment;import javax.lang.model.element.TypeElement;import com.squareup.javapoet.ClassName;import com.squareup.javapoet.JavaFile;import com.squareup.javapoet.MethodSpec;import com.squareup.javapoet.ParameterizedTypeName;import com.squareup.javapoet.TypeSpec;@AutoService(Processor.class) public class A extends AbstractProcessor { private Messager messager; private Elements elementUtils; private Filer filer; @Override public synchronized void init (ProcessingEnvironment processingEnv) { super .init(processingEnv); messager = processingEnv.getMessager(); elementUtils = processingEnv.getElementUtils(); filer = processingEnv.getFiler(); } @Override public boolean process (Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { List<RouteClass> routeClasses = new ArrayList <>(); for (Element element : roundEnv.getElementsAnnotatedWith(Route.class)) { if (element instanceof TypeElement) { TypeElement typeElement = (TypeElement) element; String path = typeElement.getAnnotation(Route.class).path(); routeClasses.add(new RouteClass (path, typeElement)); } } if (!routeClasses.isEmpty()) { generateRouterMap(routeClasses); } return true ; } private void generateRouterMap (List<RouteClass> routeClasses) { try { ClassName routerMapClassName = ClassName.get("com.enjoy.router" , "RouterMap" ); ParameterizedTypeName mapType = ParameterizedTypeName.get( ClassName.get(Map.class), ClassName.get(String.class), ClassName.get(Class.class).parameterizedBy(ClassName.get(Activity.class)) ); MethodSpec.Builder loadIntoMethodBuilder = MethodSpec.methodBuilder("loadInto" ) .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(mapType, "routers" ); for (RouteClass routeClass : routeClasses) { loadIntoMethodBuilder.addStatement("routers.put($S, $T.class)" , routeClass.path, routeClass.typeElement); } TypeSpec routerMapClass = TypeSpec.classBuilder("RouterMap" ) .addSuperinterface(ClassName.get("com.enjoy.router" , "IRouteLoad" )) .addModifiers(Modifier.PUBLIC) .addMethod(loadIntoMethodBuilder.build()) .build(); JavaFile.builder("com.enjoy.router" , routerMapClass) .build() .writeTo(filer); } catch (Exception e) { messager.printMessage(Diagnostic.Kind.ERROR, "Error generating RouterMap class: " + e.getMessage()); } } @Override public Set<String> getSupportedAnnotationTypes () { Set<String> annotations = new LinkedHashSet <>(); annotations.add(Route.class.getCanonicalName()); return annotations; } @Override public SourceVersion getSupportedSourceVersion () { return SourceVersion.latestSupported(); } private static class RouteClass { String path; TypeElement typeElement; public RouteClass (String path, TypeElement typeElement) { this .path = path; this .typeElement = typeElement; } } }
使用 @AutoService 的方式来自动注册此处理器,会在 build/classes/java/main 下生成 META-INF/services/javax.annotation.processing.Processor 文件:
在上述代码中:
init
方法用于初始化一些在注解处理过程中需要用到的工具类,如 Messager(用于打印编译期的日志信息)、Elements(用于获取元素相关信息)和 Filer(用于生成新的文件)。
process
方法是核心处理逻辑,它遍历所有被 Route 注解标记的类元素,获取注解中的路径信息,并将类信息和路径信息封装成 RouteClass 对象存储在列表中。然后,如果找到了带有注解的类,就调用 generateRouterMap 方法生成路由注册类。
generateRouterMap
方法首先定义了生成的注册类的类名和注册方法的参数类型(即路由表的类型),然后构建注册方法,通过遍历 RouteClass 列表,生成将每个路由类注册到路由表的代码语句(如 addStatement 方法中的代码)。接着构建注册类,使其实现 IRouteLoad 接口,并将注册方法添加到类中。最后,使用 JavaFile 类将生成的注册类写入文件系统。
配置 APT 处理器 在项目的 build.gradle 文件中进行如下配置,使得 APT 处理器能够在编译时被调用:
1 2 3 4 5 6 7 8 9 10 apply plugin: 'java-library' dependencies { annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7' implementation 'com.squareup.javapoet:javapoet:1.13.0' implementation 'javax.annotation:javax.annotation-api:1.3.2' }
在编译项目时,APT 就会自动扫描带有 Route 注解的类,根据注解信息生成路由注册类,该注册类实现了 IRouteLoad 接口,并在 loadInto 方法中包含了将所有带有 Route 注解的类注册到路由表的代码。这样就实现了路由注册的自动化,无需手动编写大量的注册代码,提高了开发效率和代码质量,同时也减少了因手动编写注册代码而可能产生的错误。
字节码插桩优化路由初始性能 自定义gradle插件+字节码插桩
在原始实现中,如 getFileNameByPackageName
方法在运行时遍历 apk 中所有类来获取特定包名下的类,这一过程耗时严重,影响程序启动速度。
1 2 3 4 5 6 7 while (dexEntries.hasMoreElements()){ String className = dexntries.nextElement(); if (className.startsWith(packageName)){ classNames.add(className); } }
在路由系统的原始实现中,存在一个性能瓶颈问题。例如 getFileNameByPackageName
方法,它在运行时需要遍历整个 APK 中的所有类,目的是获取特定包名下的类。这一过程非常耗时,尤其是在 APK 包含大量类文件的情况下,会严重拖慢程序的启动速度。因为在程序启动时,路由系统需要尽快完成初始化,以便能够响应页面跳转等操作,而这种全量类的扫描操作会占用大量的启动时间,导致用户体验不佳。
为了解决这个问题,引入字节码插桩技术。通过在编译期间对字节码进行修改,将原本在运行时进行的全量类扫描和路由信息加载操作提前到编译期完成。具体来说,就是在 loadRouterMap
方法中插入预先生成的路由信息加载逻辑,这样在运行时,如果已经通过字节码插桩完成了路由信息的加载,就可以直接跳过耗时的类扫描过程,从而显著提高程序启动速度与路由初始化性能。
字节码插桩的实现需要借助相关字节码操作库,这里主要使用了 ASM 库。ASM 是一个强大的 Java 字节码操作框架,它允许我们直接读取、修改和生成 Java 字节码。在自定义 Gradle 插件的逻辑中,利用 ASM 对字节码进行分析与修改。
同时,为了在 Gradle 构建过程中实现字节码插桩,需要自定义 Gradle 插件。这个插件将在 Gradle 构建的特定阶段介入,对项目中的字节码进行处理。
在 gradle 构建脚本中应用自定义插件:
1 apply plugin: com.enjoy.router
修改 getFileNameByPackageName 方法:
1 2 3 4 5 6 7 8 9 10 11 private static boolean registerByPlugin = false ;private static void loadRouterMap () {}public static Set<String> getfileNameByPackageName (Application context, final stringpackageName) throws PackageManager.NameNotFoundException { loadRouterMap(); if (registerByPlugin) return ; }
最终运行后loadRouterMap
中会自动写入
1 2 3 new FoodRouter ().loadInto(routers);new WMRouter ().loadInto(routers);registerByPlugin = true ;
不直接在loadRouterMap
中实现方法是因为:
目前处在框架开发,不知道业务开发的模块
拿不到上层代码的引用
在gradle编译时可以拿到所有类的编译,再将业务代码的类(ARounter、BRouter…)插入到Router中
在项目同级目录下新建一个目录buildSrc/src/main/java
,其中buildSrc
名称固定,在这个文件夹下新建一个类
1 2 3 4 5 public class A { public static void test () { System.out.println("11111111111111111" ); } }
这个类可以在gradle中通过
直接调用
自定义 Gradle 插件的构建与配置 自定义插件类的实现 在 buildSrc/src/main/java/com.enjoy.plugin
目录下创建 MyPlugin.java
文件
1 2 3 4 5 6 7 8 9 10 11 package com.enjoy.plugin;import org.gradle.api.Plugin;import org.gradle.api.Project;public class MyPlugin implements Plugin <Project>{ @override public void apply (Project target) { System.out.println("1111" ); } }
在 apply 方法中,我们目前只是简单地打印了一个信息。但在实际的路由字节码插桩场景中,这里将是我们注册各种转换和处理逻辑的地方。 在应用模块(例如 app 目录)下的 build.gradle
中,应用我们的自定义插件:
1 apply plugin:com.enjoy.plugin.MyPlugin
引入Android依赖 由于字节码插桩技术需要用到 Android 提供的 Transform API,需要在插件中引入相关依赖。在 buildSrc
目录下新建 build.gradle
文件,并添:
1 2 3 4 5 6 7 8 repositories { google() jcenter() } dependencies { implementation 'com.android.tools.build:gradle:4.1.0' }
这里引入了 com.android.tools.build:gradle 依赖,它包含了我们需要的 Transform API 以及其他与 Android 构建相关的工具和类。
在 MyPlugin 类中,我们需要获取 Android 扩展,并注册 MyTransform
类。MyTransform
类将负责实际的字节码转换和插桩操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.enjoy.plugin;import com.android.build.gradle.AppExtension;import org.gradle.api.Plugin;import org.gradle.api.Project;public class MyPlugin implements Plugin <Project> { @Override public void apply (Project target) { AppExtension android = target.getExtensions().getByType(AppExtension.class); android.registerTransform(new MyTransform ()); } }
基本信息定义 MyTransform
类实现了 Transform
接口,这个接口定义了一系列方法,用于在 Gradle 构建过程中对字节码进行转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import com.android.build.api.transform.Transform;import java.util.set;public class MyTransform implements Transform { @Override public string getName () { return "com.enjoy.plugin" ; } @Override public set<Qualifiedcontent.contentType> getInputTypes(){ return TransformManager.CONTENT_CLASS; } @Override public set<?super Qualifiedcontent.scope> getscopes(){ return TransformManager.SCOPE_FULL_PROJECT; } @Overridepublic boolean isIncremental () { return false ; } }
getName 方法返回这个转换的名称,这里我们设置为 “com.enjoy.plugin”。
getInputTypes 方法指定了我们要处理的内容类型,这里设置为 TransformManager.CONTENT_CLASS,表示我们要处理的是类文件。
getScopes 方法定义了处理的范围,TransformManager.SCOPE_FULL_PROJECT 表示处理整个项目的类文件,包括主模块和依赖的库模块。
isIncremental 方法设置为 false,表示不使用增量编译,每次构建都会对所有类文件进行处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Override public void transform (@NonNull TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { Collection<TransformInput> inputs = transformInvocation.getInputs(); TransformOutputProvider outputProvider = transformInvocation.getOutputProvider(); outputProvider.deleteAll(); for (TransformInput input : inputs) { Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs(); processsDireactory(directoryInputs, outputProvider); Collection<JarInput> jarInputs = input.getJarInputs(); processJar(jarInputs,outputProvider); if (destFile != null ) { RegisterCodeGenerator.insertInitCodeTo(registerList, destFile); } } }
从 transformInvocation 中获取输入信息(TransformInput)和输出提供者(TransformOutputProvider)。然后,删除输出目录下的所有文件,为新生成的字节码文件腾出空间。
遍历输入信息中的目录输入(DirectoryInput)和 JAR 输入(JarInput)。对于目录输入,调用 processDirectory 方法进行处理(这里未展示 processDirectory 方法的具体实现,但它主要是对目录中的类文件进行遍历和处理);对于 JAR 输入,调用 processJar 方法进行处理(同样未展示具体实现,主要是处理 JAR 包中的类文件)。
如果满足条件(destFile 不为空),则调用 RegisterCodeGenerator.insertInitCodeTo 方法进行插桩操作。这个方法是整个字节码插桩的关键部分,它将在特定的类文件(这里是与路由相关的类文件)中插入预先生成的路由信息加载逻辑。
RegisterCodeGenerator 类:插桩逻辑的核心实现 insertInitCodeTo 这个方法的主要目的是对指定的 JAR 文件(这里是包含 Router.class 的 JAR 文件)进行处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public static void insertInitCodeTo (List<String> registerList, File jarFile) throws IOException { File optJar = new File (jarFile.getParent(), jarFile.getName() + ".opt" ); if (optJar.exists()) { optJar.delete(); } JarFile file = new JarFile (jarFile); Enumeration<JarEntry> enumeration = file.entries(); JarOutputStream jarOutputStream = new JarOutputStream (new FileOutputStream (optJar)); while (enumeration.hasMoreElements()) { JarEntry jarEntry = enumeration.nextElement(); String entryName = jarEntry.getName(); ZipEntry zipEntry = new ZipEntry (entryName); InputStream inputStream = file.getInputStream(jarEntry); jarOutputStream.putNextEntry(zipEntry); if (entryName.equals("com/enjoy/router/api/Router.class" )) { byte [] bytes = referHackWhenInit(registerList, inputStream); jarOutputStream.write(bytes); } else { jarOutputStream.write(IOUtils.toByteArray(inputStream)); } inputStream.close(); jarOutputStream.closeEntry(); } file.close(); if (jarFile.exists()) { jarFile.delete(); } optJar.renameTo(jarFile); }
创建一个临时文件 optJar,用于存储修改后的字节码。
打开原始的 JAR 文件 jarFile,遍历其中的每个条目(JarEntry)。对于每个条目,如果是我们要插桩的 Router.class 文件,则调用 referHackWhenInit 方法获取修改后的字节码,并将其写入到新的 JAR 文件(optJar)中;如果不是 Router.class 文件,则直接将原始字节码拷贝到新的 JAR 文件中。
关闭原始 JAR 文件,删除原始 JAR 文件,并将临时文件 optJar 重命名为原始 JAR 文件的名称,完成字节码的替换。
referHackWhenInit 插桩操作的核心逻辑之一
1 2 3 4 5 6 7 8 9 10 11 private static byte [] referHackWhenInit(List<String> registerList, InputStream inputStream) throws IOException { ClassReader cr = new ClassReader (inputStream); ClassWriter cw = new ClassWriter (cr, ClassWriter.COMPUTE_FRAMES); ClassVisitor cv = new MyClassVisitor (Opcodes.ASM5, cw, registerList); cr.accept(cv, ClassReader.EXPAND_FRAMES); return cw.toByteArray(); }
创建一个 ClassReader 对象,它用于读取输入流中的字节码信息,就像一个字节码解析器。
创建一个 ClassWriter 对象,它用于生成修改后的字节码。这里使用 ClassWriter.COMPUTE_FRAMES 模式,让 ASM 自动计算字节码的栈帧信息,简化了我们的开发。
创建一个自定义的 MyClassVisitor 类的实例,这个类继承自 ClassVisitor,它将在类的访问过程中进行插桩操作。将 MyClassVisitor 传递给 ClassReader 的 accept 方法,启动字节码的分析和修改过程。
返回修改后的字节码数组
MyClassVisitor类 MyClassVisitor
类继承自 ClassVisitor
,它在类的方法访问过程中进行特殊处理。当访问到名为 loadRouterMap
的方法时,它会创建一个自定义的 RouteMethodVisitor
类的实例,用于在这个方法中插入特定的字节码指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static class MyClassVisitor extends ClassVisitor { private List<String> registerList; MyClassVisitor(int api, ClassVisitor cv, List<String> registerList) { super (api, cv); this .registerList = registerList; } @Override public MethodVisitor visitMethod (int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super .visitMethod(access, name, desc, signature, exceptions); if (name.equals("loadRouterMap" )) { mv = new RouteMethodVisitor (Opcodes.ASM5, mv, registerList); } return mv; } }
RouteMethodVisitor类 RouteMethodVisitor
类继承自 MethodVisitor
,它在方法的指令访问过程中进行插桩操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 static class RouteMethodVisitor extends MethodVisitor { private List<String> registerList; RouteMethodVisitor(int api, MethodVisitor mv, List<String> registerList) { super (api, mv); this .registerList = registerList; } @Override public void visitInsn (int opcode) { if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) { for (String name : registerList) { mv.visitTypeInsn(Opcodes.NEW, name); mv.visitInsn(Opcodes.DUP); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, name, "<init>" , "()V" , false ); mv.visitFieldInsn(Opcodes.GETSTATIC, "com/enjoy/router/api/Router" , "routers" , "Ljava/util/Map" ); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, name, "loadInto" , "(Ljava/util/Map;)V" , false ); } mv.visitInsn(Opcodes.ICONST_1); mv.visitFieldInsn(Opcodes.PUTSTATIC, "com/enjoy/router/api/Router" , "registerByPlugin" , "Z" ); } super .visitInsn(opcode); } }
当遇到方法返回指令(opcode 在 Opcodes.IRETURN 到 Opcodes.RETURN 之间)时,它会遍历 registerList 中的每个注册类名。
对于每个注册类名,它会创建该类的一个实例(通过 visitTypeInsn、visitInsn 和 visitMethodInsn 指令),然后调用该实例的 loadInto 方法,将路由信息加载到 Router 类的 routers 映射表中。
将 registerByPlugin 静态变量设置为 true,表示路由信息已经通过插桩方式加载完成。这样在运行时,getFileNameByPackageName 方法就可以根据这个标志直接跳过耗时的类扫描过程,从而提高程序的启动速度和路由初始化性能。
ASN 操作Java 字节码的框架,按照Class文件的格式,解析、修改、生成Class,可以动态生成类或者增强现有类的全功能。
正如GSON操作json的框架。
整体思路 目标是在loadRouterMap
方法的字节码中插入类似如下的指令逻辑:
其中FoodRouter
和WMRouter
都是动态生成的,他们都继承了Router类
,有几个就会生成几个,最后把registerByPlugin
置为true
。
1 2 3 new FoodRouter ().loadInto(routers);new WMRouter ().loadInto(routers);registerByPlugin = true ;
在字节码层面,这涉及到创建类的实例(使用 NEW 指令创建对象,配合 INVOKESPECIAL 调用构造函数初始化对象)、调用实例方法(通过 INVOKEVIRTUAL 指令)以及设置静态变量(使用 PUTSTATIC 指令)等操作。并且由于 FoodRouter 和 WMRouter 这类路由注册类是动态生成的,数量不确定,所以需要遍历它们的列表来依次插入对应的指令。
1 2 javac Router.java javap -v Router.class
代码实现步骤 修改 visitInsn 方法来插入具体指令
visitInsn 方法是在遍历方法体字节码指令时,对特定指令(这里关注方法返回指令)进行处理并插入我们期望指令的关键方法,修改后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override public void visitInsn (int opcode) { if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) { for (String name : registerList) { mv.visitTypeInsn(Opcodes.NEW, name); mv.visitInsn(Opcodes.DUP); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, name, "<init>" , "()V" , false ); mv.visitFieldInsn(Opcodes.GETSTATIC, "com/enjoy/router/api/Router" , "routers" , "Ljava/util/Map" ); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, name, "loadInto" , "(Ljava/util/Map;)V" , false ); } mv.visitInsn(Opcodes.ICONST_1); mv.visitFieldInsn(Opcodes.PUTSTATIC, "com/enjoy/router/api/Router" , "registerByPlugin" , "Z" ); } super .visitInsn(opcode); }
在上述代码中:
外层的 for 循环遍历 registerList,针对每个动态生成的路由注册类执行以下操作:
首先通过 visitTypeInsn 配合 Opcodes.NEW 创建类的实例,将创建对象的指令插入字节码中。
接着使用 visitInsn 配合 Opcodes.DUP 复制栈顶元素,这是为了后续调用构造函数做准备,因为 INVOKESPECIAL 指令在调用实例的构造函数时会消耗一个栈顶元素作为 this 指针。
然后通过 visitMethodInsn 调用构造函数进行初始化,指定了构造函数的名称 、描述符 ()V(表示无参且返回 void)以及所属的类(即当前循环的路由注册类)。
再通过 visitFieldInsn 获取 Router 类中的静态 routers 字段,以便传递给 loadInto 方法作为参数。
最后使用 visitMethodInsn 调用当前路由注册类实例的 loadInto 方法,将路由信息加载到 routers 映射表中,这里指定了方法的名称 loadInto、参数类型描述符 (Ljava/util/Map;)V(表示接收一个 Map 类型参数且返回 void)以及所属的类(当前路由注册类)。
在循环结束后,通过 visitInsn 配合 Opcodes.ICONST_1 将整数常量 1 压入栈顶,然后使用 visitFieldInsn 配合 PUTSTATIC 操作码将 registerByPlugin 静态变量设置为 true,完成整个插桩逻辑中关键指令的插入。
这样,在编译期间,通过字节码插桩就能够在 loadRouterMap 方法中插入我们期望的路由信息加载逻辑以及设置相应的完成标志,从而避免在运行时去遍历查找路由注册类,提高了路由初始化性能。