一、概述
一个支持模块间的路由、通信、解耦,帮助Android App进行组件化改造的框架。
GitHub:https://github.com/alibaba/ARouter
二、由来
说起来由,就不得不提起Android中组件化开发的思想,组件化是最近Android比较流行的架构设计方案,它对模块(module)间进行高度的解耦、分离等,能极大的改善开发效率。但是组件化架构设计,导致模块(module)间没有相互之间的引用,或则只是单向的持有引用,所以给模块(module)间的跳转startActivity带来了不小的阻力。这时候就需要一个框架,既不会破坏组件化的架构设计方案,又能解决模块(module)间路由跳转。而ARouter就可以很好的解决这个问题。
可能有人会想到隐式跳转也可以解决这个问题,当然这也是一种方案,但是如果整个项目都用隐式跳转,这样Manifest文件会有很多过滤配置,不利于后期的维护。
通过反射机制拿到Activity的class文件也可以实现跳转,其实,大量的使用反射,会对性能有很大的影响,其次因为组件开发的时候组件module之间是没有相互引用的,你只能通过找到类的路径去反射拿到这个class。
三、回顾使用
既然相较于隐式跳转和反射机制,ARouter更容易解决模块(module)间路由跳转的问题。那么它又是如何做到的呢?在介绍它如何做到之前,先回顾一下我们是如何使用它实现路由跳转的,具体步骤参考https://github.com/alibaba/ARouter/blob/master/README_CN.md
- 1、在每个需要对其他module提供调用的Activity中,都会声明类似下面@Route注解,我们称之为路由地址。
//模块Main
@Route(path = "/Main/MainActivity")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
//模块Login
@Route(path = "/Login/LoginActivity")
public class LoginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
}
}
- 2、在继承Application的自定义Application中初始化ARouter。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ARouter.init(this);
}
}
- 3、发起路由操作
// 1. 模块Main跳转Login
ARouter.getInstance().build("/Login/LoginActivity").navigation();
// 2. 跳转并携带参数
ARouter.getInstance().build("/Login/LoginActivity")
.withLong("key", aaa)
.withString("value", "bbbb")
.navigation();
ARouter只做了三件事情就解决了组件间路由跳转:
-
(1)在项目的编译期通过注解处理器扫描所有添加@Route注解的Activity类,然后将Route注解中的path地址和Activity.class文件映射关系保存到它自己生成的java文件。
-
(2)app进程启动的时候会加载这些类文件,把保存这些映射关系的数据读到内存里(保存在map里)。
-
(3)然后在进行路由跳转的时候,通过build()方法传入要到达页面的路由地址,通过调用navigation()方法,它的内部会调用startActivity(intent)进行跳转。
四、细节
我们心里有这三步大概的概念之后,再来看看每一步中的细节。
在第一步中 -----在程序编译时候完成
- 它是如何得到path地址和Activity.class文件映射关系?
通过APT(Annotation Processing Tool),即注解处理工具。apt是在编译期对代码中指定的注解进行解析。
(1)首先第一步,定义注解:源码地址
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
/**
* Path of route
*/
String path();
/**
* Used to merger routes, the group name MUST BE USE THE COMMON WORDS !!!
*/
String group() default "";
/**
* Name of route, used to generate javadoc.
*/
String name() default "";
/**
* Extra data, can be set by user.
* Ps. U should use the integer num sign the switch, by bits. 10001010101010
*/
int extras() default Integer.MIN_VALUE;
/**
* The priority of route.
*/
int priority() default -1;
}
(2)第二步,在Activity上使用注解
//模块Main
@Route(path = "/Main/MainActivity")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
(3)第三步,注解处理器,在编译器找到加入注解的类文件,进行处理,这里只展示关键代码,具体代码
@AutoService(Processor.class)
@SupportedAnnotationTypes({ANNOTATION_TYPE_ROUTE, ANNOTATION_TYPE_AUTOWIRED})
public class RouteProcessor extends BaseProcessor {
private Map<String, Set<RouteMeta>> groupMap = new HashMap<>(); // ModuleName and routeMeta.(key:模块名,value:路由表)
private Map<String, String> rootMap = new TreeMap<>(); // Map of root metas, used for generate class file in order.(根元的映射,用于生成类文件的顺序。)
}
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
// Start generate java source, structure is divided into upper and lower levels, used for demand initialization.
for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
//将所有@Route()注解存入docSource
rootMap.put(groupName, groupFileName);
docSource.put(groupName, routeDocList);
}
}
- 它是如何生成映射关系的Java文件?
通过APT得到映射关系的集合后,通过JavaPoet生成Java文件。javapoet是鼎鼎大名的squareup出品的一个开源库,是用来生成java文件的一个library,它提供了简便的api供你去生成一个java文件。
// Generate groups
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler);
// Write provider into disk
String providerMapFileName = NAME_OF_PROVIDER + SEPARATOR + moduleName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(providerMapFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IProviderGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfProviderBuilder.build())
.build()
).build().writeTo(mFiler);
// Write root meta into disk.
String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(rootFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(elementUtils.getTypeElement(ITROUTE_ROOT)))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfRootBuilder.build())
.build()
).build().writeTo(mFiler);
- 3 生成的Java文件长啥样?在哪能找到?
public class ARouter$$Group$$Main implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/Main/MainActivity", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/main/mainactivity", "main", null, -1, -2147483648));
}
}
image.png
在第二步中-----在程序初始化时候完成
- 它是如何得到Java文件中的path地址和Activity.class文件映射关系?
上节我们已经通过apt生成了映射文件,然而就要考虑在合适的时机拿到这些映射文件中的信息,以供上层业务做跳转使用。那么在什么时机去拿到这些映射文件中的信息呢?首先我们需要在上层业务做路由跳转之前把这些路由映射关系拿到手,但我们不能事先预知上层业务会在什么时候做跳转,那么拿到这些路由关系最好的时机就是应用程序初始化的时候。这就是我们第二大部分“回顾使用”的第二步
public class MyApplication extends Application {
@Override
public void onCreate() {
ARouter.init(this);//第二步,初始化
super.onCreate();
}
}
那我们就沿着ARouter.init(this)点进去看看,是如何拿到映射关系。
_ARouter源码地址
//com.alibaba.android.arouter.launcher.ARouter
//https://github.com/alibaba/ARouter/blob/master/arouter-api/src/main/java/com/alibaba/android/arouter/launcher/ARouter.java
/**
* Init, it must be call before used router.
*/
public static void init(Application application) {
if (!hasInit) {//我们可以知道,初始化时找到这些类文件会有一定的耗时,ARouter这里会有一些优化,只会遍历找一次类文件,找到之后就会保存起来,下次app进程启动会检索是否有保存这些文件,如果有就会直接调用保存后的数据去初始化。
logger = _ARouter.logger;
_ARouter.logger.info(Consts.TAG, "ARouter init start.");
hasInit = _ARouter.init(application);
if (hasInit) {
_ARouter.afterInit();
}
_ARouter.logger.info(Consts.TAG, "ARouter init over.");
}
}
//com.alibaba.android.arouter.launcher._ARouter
//https://github.com/alibaba/ARouter/blob/master/arouter-api/src/main/java/com/alibaba/android/arouter/launcher/_ARouter.java
protected static synchronized boolean init(Application application) {
mContext = application;
LogisticsCenter.init(mContext, executor);//关进代码,注意这个executor是个线程池
logger.info(Consts.TAG, "ARouter init success!");
hasInit = true;//拿到之后,将hasInit置为true。
mHandler = new Handler(Looper.getMainLooper());
return true;
}
//com.alibaba.android.arouter.core.LogisticsCenter
//https://github.com/alibaba/ARouter/blob/4660c11bab2b91515451ada04f943f8ea4b79ace/arouter-api/src/main/java/com/alibaba/android/arouter/core/LogisticsCenter.java
/**
* LogisticsCenter init, load all metas in memory. Demand initialization,,
* LogisticsCenter init,在内存中加载所有metas。需求初始化。
*/
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
// 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.");
// These class was generated by arouter-compiler.
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);//关键代码
if (!routerMap.isEmpty()) {
//SharedPreferences做保存记录,优化初始化。
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);
}
}
}
//com.alibaba.android.arouter.utils.ClassUtils
//https://github.com/alibaba/ARouter/blob/4660c11bab2b91515451ada04f943f8ea4b79ace/arouter-api/src/main/java/com/alibaba/android/arouter/utils/ClassUtils.java
/**
* 通过指定包名,扫描包下面包含的所有的ClassName
*
* @param context U know
* @param packageName 包名
* @return 所有class的集合
*/
public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
final Set<String> classNames = new HashSet<>();
List<String> paths = getSourcePaths(context);
final CountDownLatch parserCtl = new CountDownLatch(paths.size());
for (final String path : paths) {
DefaultPoolExecutor.getInstance().execute(new Runnable() {
@Override
public void run() {
DexFile dexfile = null;
try {
if (path.endsWith(EXTRACTED_SUFFIX)) {
//NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
dexfile = DexFile.loadDex(path, path + ".tmp", 0);
} else {
dexfile = new DexFile(path);
}
Enumeration<String> dexEntries = dexfile.entries();
while (dexEntries.hasMoreElements()) {
String className = dexEntries.nextElement();
if (className.startsWith(packageName)) {
classNames.add(className);
}
}
} catch (Throwable ignore) {
Log.e("ARouter", "Scan map file in dex files made error.", ignore);
} finally {
if (null != dexfile) {
try {
dexfile.close();
} catch (Throwable ignore) {
}
}
parserCtl.countDown();
}
}
});
}
parserCtl.await();
return classNames;
}
总结:
(1)通过ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)得到apt生成的所有实现IRouteRoot接口的类文件集合,通过上面的讲解我们知道,拿到这些类文件便可以得到所有的路由地址和Activity映射关系。
(2)getFileNameByPackageName()通过开启子线程,去扫描apk中所有的dex,遍历找到所有包名为packageName的类名,然后将类名再保存到classNames集合里。
在第三步中-----在程序跳转时候完成
- 有了映射关系它是如何实现跳转?
ARouter.getInstance().build("/Login/LoginActivity").navigation();
在build的时候,传入要跳转的路由地址,build()方法会返回一个Postcard对象,我们称之为跳卡。然后调用Postcard的navigation()方法完成跳转。用过ARouter的对这个跳卡都应该很熟悉吧!Postcard里面保存着跳转的信息,Postcard继承于RouteMeta。下面我把Postcard类和RouteMeta类的代码实现粘下来:
//https://github.com/alibaba/ARouter/blob/4660c11bab2b91515451ada04f943f8ea4b79ace/arouter-api/src/main/java/com/alibaba/android/arouter/facade/Postcard.java
public final class Postcard extends RouteMeta {
// Base
private Uri uri;
private Object tag; // A tag prepare for some thing wrong.
private Bundle mBundle; // Data to transform
private int flags = -1; // Flags of route
private int timeout = 300; // Navigation timeout, TimeUnit.Second
private IProvider provider; // It will be set value, if this postcard was provider.
private boolean greenChannel;
private SerializationService serializationService;
// Animation
private Bundle optionsCompat; // The transition animation of activity
private int enterAnim = -1;
private int exitAnim = -1;
//.....省略后面的代码
}
//https://github.com/alibaba/ARouter/blob/4660c11bab2b91515451ada04f943f8ea4b79ace/arouter-annotation/src/main/java/com/alibaba/android/arouter/facade/model/RouteMeta.java
public class RouteMeta {
private RouteType type; // Type of route
private Element rawType; // Raw type of route(原始路由类型)
private Class<?> destination; // Destination(目的地,终点)
private String path; // Path of route(路由path)
private String group; // Group of route(路由分组)
private int priority = -1; // The smaller the number, the higher the priority
private int extra; // Extra data
private Map<String, Integer> paramsType; // Param type
private String name;
//.....省略后面的代码
}
Postcard类和RouteMeta里面有了跳转的目的地Class<?> destination,接下的方法navigation()我想都能猜到,他是用啥跳转了吧——startActivity。
//https://github.com/alibaba/ARouter/blob/4660c11bab2b91515451ada04f943f8ea4b79ace/arouter-api/src/main/java/com/alibaba/android/arouter/launcher/_ARouter.java#L351
final Intent intent = new Intent(currentContext, postcard.getDestination());//获取目的地Class<?> destination
intent.putExtras(postcard.getExtras());
// Set flags.
int flags = postcard.getFlags();
if (-1 != flags) {
intent.setFlags(flags);
} else if (!(currentContext instanceof Activity)) { // Non activity, need less one flag.
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
// Set Actions
String action = postcard.getAction();
if (!TextUtils.isEmpty(action)) {
intent.setAction(action);
}
// Navigation in main looper.
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode, currentContext, intent, postcard, callback);//跳转
}
});
五、结束语
(1)本文只是大概的分析了ARouter的跳转流程的实现原理,里面还有很多的小细节没有讲到,如过滤器等。
(2)ARouter涉及到几个关键的技术点,如:APT、Javapoet,可以再深入研究。他们在很多的框架中都是关键点。如我们常用的ButterKnife,其原理就是通过注解处理器在编译期扫描代码中加入的@BindView、@OnClick等注解进行扫描处理,然后生成XXX_ViewBinding类,实现了view的绑定。
(3)本文Dome地址:https://github.com/yuzhizhe/ARouter_Sample
网友评论