赞
踩
作者:星星y
链接:
https://www.jianshu.com/p/2840cf1a71e1
本文由作者授权发布。
阅读源码一直是一个非常好的进阶的方式,大厂开源一个方案,自己通过阅读源码可以了解其原理,然后遇到特殊的场景,就可以自己进行扩展,例如本文作者因为遇到了插件化的场景。
如果有能力扩展后,后续甚至有能力根据场景优化,即作者后续通过 Transform API 做了一些事情,非常值得我们学习。
最近用Small实现原有项目的插件化,效果还不错,只要工程是组件化的结构就很好重构。但在使用ARouter时,由于初始化时,查询的apk路径只有base.apk,所以不能找到由Route注解自动生成的ARouter
Small插件化实战总结
https://www.jianshu.com/p/4263420d98a8
为了适配插件化版本,所以需要自己手动打造简易版的ARouter框架。
一、APT
通过APT处理用注解标记的Activity类,生成对应的映射文件。这里创建两个类型为java library的module。一个library(ARouter处理逻辑),一个compiler(处理注解,生成源码)
library的build.gradle
compilerOnly里的是Android的相关类库
compiler的build.gradle
- apply plugin: 'java'
- dependencies {
- compile 'com.squareup:javapoet:1.9.0'
- compile 'com.google.auto.service:auto-service:1.0-rc3'
- compile project(':library')
- }
- targetCompatibility = '1.7'
- sourceCompatibility = '1.7'
auto-service会自动在META-INF文件夹下生成Processor配置信息文件,使得编译时能找到annotation对应的处理类。javapoet则是由square公司出的开源库,能有优雅的生成java源文件。
接着,我们在library中创建一个注解类,Target表明修饰的类型(类或接口、方法、属性,TYPE表示类或接口),Retention表明可见级别(编译时,运行时期等,CLASS表示在编译时可见)
然后在app的gradle引入依赖
注意:gradle2.2以下需要将annotationProcessor改为apt,同时在工程根目录引入
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
在MainActivity中添加注解
- ...
- import io.github.iamyours.aarouter.annotation.Route;
-
- @Route(path = "/app/main")
- public class MainActivity extends AppCompatActivity {
- ...
- }
- package io.github.iamyours.compiler;
-
- import com.google.auto.service.AutoService;
-
- import java.util.LinkedHashSet;
- import java.util.Set;
-
- import javax.annotation.processing.AbstractProcessor;
- import javax.annotation.processing.Processor;
- import javax.annotation.processing.RoundEnvironment;
- import javax.lang.model.SourceVersion;
- import javax.lang.model.element.TypeElement;
-
- import io.github.iamyours.aarouter.annotation.Route;
-
- /**
- * Created by yanxx on 2017/7/28.
- */
- @AutoService(Processor.class)
- public class RouteProcessor extends AbstractProcessor {
-
- @Override
- public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
- System.out.println("============="+roundEnvironment);
- return true;
- }
-
-
- @Override
- public Set<String> getSupportedAnnotationTypes() {
- Set<String> annotations = new LinkedHashSet<>();
- annotations.add(Route.class.getCanonicalName());
- return annotations;
- }
-
- @Override
- public SourceVersion getSupportedSourceVersion() {
- return SourceVersion.latestSupported();
- }
-
- }
然后我们make project以下,得到如下日志信息,则表明apt配置成功。
- :app:javaPreCompileDebug
- :compiler:compileJava UP-TO-DATE
- :compiler:processResources NO-SOURCE
- :compiler:classes UP-TO-DATE
- :compiler:jar UP-TO-DATE
- :app:compileDebugJavaWithJavac
- =============[errorRaised=false, rootElements=[io.github.iamyours.aarouter.MainActivity, ...]
- =============[errorRaised=false, rootElements=[], processingOver=true]
- :app:compileDebugNdk NO-SOURCE
- :app:compileDebugSources
二、使用javapoet生成源文件
javapoet的用法可以看这里
https://github.com/square/javapoet
为了保存由Route注解标记的class类名,这里用一个映射类通过方法调用的形式保存。
具体生成的类如下
为了之后能够从Android apk中的DexFile中找到映射类,我们要把这些映射java类放到同一个package下,具体实现逻辑如下:
在compiler中
- @AutoService(Processor.class)
- public class RouteProcessor extends AbstractProcessor {
- private Filer filer;
- private Map<String, String> routes = new HashMap<>();
- private String moduleName;
-
- @Override
- public synchronized void init(ProcessingEnvironment processingEnvironment) {
- super.init(processingEnvironment);
- filer = processingEnvironment.getFiler();
- }
-
- @Override
- public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
- for (Element e : roundEnvironment.getElementsAnnotatedWith(Route.class)) {
- addRoute(e);
- }
- createRouteFile();
- return true;
- }
-
- private void createRouteFile() {
- TypeSpec.Builder builder = TypeSpec.classBuilder("AARouterMap_" + moduleName).addModifiers(Modifier.PUBLIC);
- TypeName superInterface = ClassName.bestGuess("io.github.iamyours.aarouter.IRoute");
- builder.addSuperinterface(superInterface);
- TypeName stringType = ClassName.get(String.class);
- TypeName mapType = ParameterizedTypeName.get(ClassName.get(Map.class), stringType, stringType);
- MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("loadInto")
- .addAnnotation(Override.class)
- .returns(void.class)
- .addModifiers(Modifier.PUBLIC)
- .addParameter(mapType, "routes");
- for (String key : routes.keySet()) {
- methodBuilder.addStatement("routes.put($S,$S)", key, routes.get(key));
- }
- builder.addMethod(methodBuilder.build());
- JavaFile javaFile = JavaFile.builder(ARouter.ROUTES_PACKAGE_NAME, builder.build()).build();//将源码输出到ARouter.ROUTES_PACKAGE_NAME,
- try {
- javaFile.writeTo(filer);
- } catch (IOException e) {
- // e.printStackTrace();
- }
- }
- /*
- 这里有一个注意的点事moduleName,由于每个library或application模块的环境不同,
- 也只能取到当前模块下的注解,因此需要生成不同的映射文件保存到每个模块下,
- 阿里的获取的方法是在每个模块的build文件通过annotationProcessorOptions传入,
- 这边简化直接从path获取(如“/app/login”取app,"/news/newsinfo"取news)
- */
- private void addRoute(Element e) {
- Route route = e.getAnnotation(Route.class);
- String path = route.path();
- String name = e.toString();
- moduleName = path.substring(1,path.lastIndexOf("/"));
- routes.put(path, name);
- }
-
-
- @Override
- public Set<String> getSupportedAnnotationTypes() {
- Set<String> annotations = new LinkedHashSet<>();
- annotations.add(Route.class.getCanonicalName());
- return annotations;
- }
-
- @Override
- public SourceVersion getSupportedSourceVersion() {
- return SourceVersion.latestSupported();
- }
- }
三、
ARouter初始化- public class ARouter {
- private Map<String, String> routes = new HashMap<>();
- private static final ARouter instance = new ARouter();
- public static final String ROUTES_PACKAGE_NAME = "io.github.iamyours.aarouter.routes";
- public void init(Context context){
- try {//找到ROUTES_PACKAGE_NAME目录下的映射class文件
- Set<String> names = ClassUtils.getFileNameByPackageName(context,ROUTES_PACKAGE_NAME);
- initRoutes(names);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- //通过反射初始化路由
- private void initRoutes(Set<String> names) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
- for(String name:names){
- Class clazz = Class.forName(name);
- Object obj = clazz.newInstance();
- if(obj instanceof IRoute){
- IRoute route = (IRoute) obj;
- route.loadInto(routes);
- }
- }
- }
-
- private ARouter() {
-
- }
-
- public static ARouter getInstance() {
- return instance;
- }
-
- public Postcard build(String path) {
- String component = routes.get(path);
- if (component == null) throw new RuntimeException("could not find route with " + path);
- return new Postcard(component);
- }
- }
之前我们通过RouterProcessor将映射class放到了ROUTES_PACKAGE_NAME下,我们只需要在dex文件中遍历寻找到它们即可。而alibaba的ARouter取的是当前app应用目录的base.apk寻找的dex文件,然后通过DexClassLoader加载取得DexFile。
但如果项目插件化构成的,dexFile就不只是base.apk下了,因此需要通过其他方式获取了。
- public class ClassUtils {
- //通过BaseDexClassLoader反射获取app所有的DexFile
- private static List<DexFile> getDexFiles(Context context) throws IOException {
- List<DexFile> dexFiles = new ArrayList<>();
- BaseDexClassLoader loader = (BaseDexClassLoader) context.getClassLoader();
- try {
- Field pathListField = field("dalvik.system.BaseDexClassLoader","pathList");
- Object list = pathListField.get(loader);
- Field dexElementsField = field("dalvik.system.DexPathList","dexElements");
- Object[] dexElements = (Object[]) dexElementsField.get(list);
- Field dexFilefield = field("dalvik.system.DexPathList$Element","dexFile");
- for(Object dex:dexElements){
- DexFile dexFile = (DexFile) dexFilefield.get(dex);
- dexFiles.add(dexFile);
- }
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- } catch (NoSuchFieldException e) {
- e.printStackTrace();
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- }
- return dexFiles;
- }
-
- private static Field field(String clazz,String fieldName) throws ClassNotFoundException, NoSuchFieldException {
- Class cls = Class.forName(clazz);
- Field field = cls.getDeclaredField(fieldName);
- field.setAccessible(true);
- return field;
- }
- /**
- * 通过指定包名,扫描包下面包含的所有的ClassName
- *
- * @param context U know
- * @param packageName 包名
- * @return 所有class的集合
- */
- public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws IOException {
- final Set<String> classNames = new HashSet<>();
-
- List<DexFile> dexFiles = getDexFiles(context);
- for (final DexFile dexfile : dexFiles) {
- Enumeration<String> dexEntries = dexfile.entries();
- while (dexEntries.hasMoreElements()) {
- String className = dexEntries.nextElement();
- if (className.startsWith(packageName)) {
- classNames.add(className);
- }
- }
- }
- return classNames;
- }
- }
有了上面的实现,我们就可以在初始化时,通过传入context的classloader,获取到映射路由文件,然后反射初始化他们,调用loadInto,即可得到所有的路由。
而接下来的路由跳转就很简单了,只需包装成ComponentName就行
- public class ARouter {
- ...
- public Postcard build(String path) {
- String component = routes.get(path);
- if (component == null) throw new RuntimeException("could not find route with " + path);
- return new Postcard(component);
- }
- }
https://github.com/iamyours/AARouter
现在这个版本虽然也适配Small,但是通过反射私有api找到映射class终究还是有些隐患。
后来想到另外一种方案:
每个模块build传入模块的包名,生成的文件统一命名为AARouterMap,初始化时small可以通过Small.getBundleVersions().keys获取每个插件的包名
ARouter.getInstance().init(Small.getBundleVersions().keys)
来获取每个插件的包名
- public void init(Set<String> appIds) {
- try {
- initRoutes(appIds);
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- } catch (InstantiationException e) {
- e.printStackTrace();
- }
- }
-
- private void initRoutes(Set<String> appIds) throws IllegalAccessException, InstantiationException {
- for (String appId : appIds) {
- Class clazz = null;
- try {
- clazz = Class.forName(appId + ".AARouterMap");
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- }
- if(clazz==null)continue;
- Object obj = clazz.newInstance();
- if (obj instanceof IRoute) {
- IRoute route = (IRoute) obj;
- route.loadInto(routes);
- }
- }
- }
这样就不用遍历dex获取映射,性能和安全性也会好一点。非插件化的项目也可以通过手动传包名列表适配了。
后续
之前通过APT实现了一个简易版ARouter框架,碰到的问题是APT在每个module的上下文是不同的,导致需要通过不同的文件来保存映射关系表。
因为类文件的不确定,就需要初始化时在dex文件中扫描到指定目录下的class,然后通过反射初始化加载路由关系映射。
阿里的做法是直接开启一个异步线程,创建DexFile对象加载dex。这多少会带来一些性能损耗,为了避免这些,我们通过Transform api实现另一种更加高效的路由框架。
基于Transform实现更高效的组件化路由框架
https://www.jianshu.com/p/66cf509000d3
喜欢 就关注吧,欢迎投稿!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。