路由的思想和ARouter相关介绍

·  阅读 126

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

组件之间没有依赖,在不同的组件之间进行页面的跳转的方式

方式问题
反射当然你用反射拿到Activity的class文件也可以实现跳转,但是大量的使用反射跳转对性能会有影响
隐式跳转项目中不可能所有的跳转都是隐式的,这样Manifest文件会有很多过滤配置,而且非常不利于后期维护
EventBus消息满天飞,溯源很难
广播比较麻烦,太重
路由(ARouter)可以很好解决

路由的大致思想

用过ARouter路由框架的同学应该都知道,在每个需要对其他module提供调用的Activity中,都会声明类似下面@Route注解,我们称之为路由地址。

@Route(path = "/main/main")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}


@Route(path = "/module1/module1main")
public class Module1MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_module1_main);
    }
}
复制代码

路由框架会在项目的编译期通过注解处理器扫描所有添加@Route注解的Activity类,然后将Route注解中的path地址和Activity.class文件映射关系保存到它自己生成的java文件中

public class MyRouters{

    //项目编译后通过apt生成如下方法
    public static HashMap<String, ClassBean> getRouteInfo(HashMap<String, ClassBean> routes) {
        route.put("/main/main", MainActivity.class);
        route.put("/login/login", LoginActivity.class);
        ....
    }
}
复制代码

如果我们想在MainActivity调到login模块的登陆页面,我们只需要

//不同模块之间启动Activity
public void login(String name, String password) {
    HashMap<String, ClassBean> route = MyRouters.getRouteInfo(new HashMap<String, ClassBean>);
    LoginActivity.class classBean = route.get("/login/login");
    Intent intent = new Intent(this, classBean);
    startActivity(intent);
}
复制代码

这样是不是很简单就实现了路由的跳转,既没有隐式意图的繁琐,也没有反射对性能的损耗

路由框架原理

image.png

将需要相互跳转的界面信息传递至arouter中存储关联 & 实现跳转

ARouter的缺陷

ARouter的缺陷就在于拿到这个Map的过程 我们在使用ARouter时都需要初始化,ARouter所做的即是在初始化时利用反射扫描指定包名下面的所有className,然后再添加map

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
    mContext = context;
    executor = tpe;

    try {
        long startInit = System.currentTimeMillis();
        //load by plugin first
        loadRouterMap();
        if (registerByPlugin) {
            logger.info(TAG, "Load router map by arouter-auto-register plugin.");
        } else {
            Set<String> routerMap;

            // It will rebuild router map every times when debuggable.
            if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
                logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
                // 反射扫描对应包
                routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
                if (!routerMap.isEmpty()) {
                    context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
                }

                PackageUtils.updateVersion(context);    // Save new version name when router map update finishes.
            } else {
                logger.info(TAG, "Load router map from cache.");
                routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
            }

            logger.info(TAG, "Find router map finished, map size = " + routerMap.size() + ", cost " + (System.currentTimeMillis() - startInit) + " ms.");
            startInit = System.currentTimeMillis();

            for (String className : routerMap) {
                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);
                }
            }
        }

        logger.info(TAG, "Load root element finished, cost " + (System.currentTimeMillis() - startInit) + " ms.");

        if (Warehouse.groupsIndex.size() == 0) {
            logger.error(TAG, "No mapping files were found, check your configuration please!");
        }

        if (ARouter.debuggable()) {
            logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size()));
        }
    } catch (Exception e) {
        throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
    }
}
复制代码
  • 初次打开时会利用ClassUtils.getFileNameByPackageName来扫描对应包下的所有className
  • 在初次扫描后会存储在SharedPreferences中,这样后续就不需要再扫描了,这也是一个优化

以上两个过程都是耗时操作,即是ARouter初次打开时可能会造成慢的原因

利用字节码插桩优化ARouter首次启动耗时

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
    //load by plugin first
    loadRouterMap();
    if (registerByPlugin) {
        logger.info(TAG, "Load router map by arouter-auto-register plugin.");
    } else {
        ....
    }
}

private static void loadRouterMap() {
	//registerByPlugin一直被置为false
    registerByPlugin = false;
}
复制代码

在初始化时,会在扫描之前,判断registerByPlugin,如果我们需要的map已经被插件注册了,那也就不需要进行下面的耗时操作了
但是我们可以看到在loadRouterMap中,registerByPlugin一直被设为false
registerByPlugin是不是一直没有生效?
这里面其实用到了字节码插桩来在loadRouterMap方法中插入代码

编译插桩

在代码编译期间修改已有的代码或者生成新代码。实际上,我们项目中经常用到的 Dagger、ButterKnife 甚至是 Kotlin 语言,它们都用到了编译插桩的技术。

java文件的编译过程

image.png

从上图可以看出,我们可以在 1、2 两处对代码进行改造。
1.在.java文件编译成.class文件时,APTAndroidAnnotation 等就是在此处触发代码生成。
2.在.class文件进一步优化成.dex文件时,也就是直接操作字节码文件,这就是字码码插桩

ARouter注解生成用了第一种方法,而启动优化则用了第二种方法

image.png

ASM是一个十分强大的字节码处理框架,基本上可以实现任何对字节码的操作,也就是自由度和开发的掌控度很高.
但是其相对来说比AspectJ上手难度要高,需要对Java字节码有一定了解.
不过ASM为我们提供了访问者模式来访问字节码文件,这种模式下可以比较简单的做一些字节码操作,实现一些功能。
同时ASM可以精确的只注入我们想要注入的代码,不会额外生成一些包装代码,所以性能上影响比较微小。

字节码插桩对ARouter的具体优化

//源码代码,插桩前
private static void loadRouterMap() {
	//registerByPlugin一直被置为false
    registerByPlugin = false;
}
//插桩后反编译代码
private static void loadRouterMap() {
    registerByPlugin = false;
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
    register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi");
}
复制代码

1.插桩前源码与插桩后反编译代码如上所示
2.插桩后代码即在编译期在loadRouterMap中插入了register代码
3.通过这种方式即可避免在运行时通过反射扫描className,优化了启动速度

使用 Gradle 插件实现路由表的自动加载

apply plugin: 'com.alibaba.arouter'

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath "com.alibaba:arouter-register:?"
    }
}
复制代码

可选使用,通过 ARouter 提供的注册插件进行路由表的自动加载(power by AutoRegister), 默认通过扫描 dex 的方式 进行加载通过 gradle 插件进行自动注册可以缩短初始化时间解决应用加固导致无法直接访问 dex 文件,初始化失败的问题,需要注意的是,该插件必须搭配 api 1.3.0 以上版本使用!

分类:
Android
标签:
分类:
Android
标签: