RePlugin集成ARouter
# 一、功能需求
# 1、组件解耦 (ARouter)
阿里开源的 ARouter (opens new window) 在组件化开发中这个库相当有用,其核心功能就是组件解耦,比如以往要跳转另一个 Activity 时,会使用如下代码:
val intent = Intent(this, OtherActivity::class.java)
startActivity(intent)
这样的代码意味着耦合性极强,当前的 Activity 代码层面上直接引用了 OtherActivity,当项目中应用了模块化开发(Module 之间不直接依赖)或多渠道变体(不同渠道指定不同的 src 目录)时,将会导致 OtherActivity 无法引用,这时工程就会报错了,而使用 ARouter 时,上述代码可以转变成如下代码:
ARouter.getInstance().build("/module2/other").navigation()
@Route(path = "/module2/other")
class OtherActivity : BaseActivity() {
...
}
很显示,使用了 ARouter 之后,Activity 之间不再存在代码级引用,这就是组件解耦。ARouter 还支持 Fragment,更多使用方式请查看官方文档: https://github.com/alibaba/ARouter/blob/master/README_CN.md (opens new window)
# 2、插件化 (RePlugin)
项目的架构设计涉及到插件化,我们使用的是 RePlugin (opens new window),当然,每个公司对 RePlugin 的使用方式可能是不一样的,大致分为以下两种:
- 方式一: 宿主是主 app,包含主要的业务功能,只一些小功能需要通过插件方式动态更新。
- 方式二: 宿主是马甲包,插件才是主 app,宿主 apk 只管一件事:更新主 app 插件。
这里,我们基于方式二来使用 RePlugin,因为方式二中主 app 是插件,涉及大量业务逻辑,并且工程会用到多渠道变体(不同渠道指定不同的 src 目录),所以,我们需要在插件中使用 ARouter,而宿主的功能很简单,用不到 ARouter。
>>>>>>>>>>>>>>>>>>>注意:以下所有的分析都以此为基础<<<<<<<<<<<<<<<<<<<<
# 二、问题与解决方案
按照 ARouter 的官方文档,在插件工程中对 ARouter 进行了依赖,作为单品时,所有功能都正常使用,但作为插件时,就完全失效了~ 界面提示:There's no route matched! Path = [/arouter/service/interceptor] Group = [arouter]
# 1、ARouter 初始化失败
在 ARouter 的 issue 中也有很多 issue 提到与插件化框架搭配时出了问题,官方的回应表示对 RePlugin 等插件化框架支持不好,相关 issue 有:
- https://github.com/alibaba/ARouter/issues/714 (opens new window)
- https://github.com/alibaba/ARouter/issues/281 (opens new window)
- https://github.com/alibaba/ARouter/issues/172 (opens new window)
以下是 app 分别作为单品或插件时 ARouter 的日志输出:
// >>>>>>>>>>>>>>>>>>> 单品日志输出
I/ARouter::: ARouter openLog[ ]
I/ARouter::: ARouter openDebug[ ]
I/ARouter::: ARouter init start.[ ]
I/ARouter::: Run with debug mode or new install, rebuild router map.[ ]
I/ARouter::: VM with name 'Android' has multidex support
E/ARouter::: InstantRun support error, com.android.tools.fd.runtime.Paths
I/ARouter::: Thread production, name is [ARouter task pool No.1, thread No.1][ ]
D/ARouter::: Filter 6 classes by packageName <com.alibaba.android.arouter.routes>
I/ARouter::: Find router map finished, map size = 6, cost 43 ms.[ ]
I/ARouter::: Load root element finished, cost 4 ms.[ ]
D/ARouter::: LogisticsCenter has already been loaded, GroupIndex[2], InterceptorIndex[0], ProviderIndex[2][ ]
I/ARouter::: ARouter init success![ ]
D/ARouter::: The group [arouter] starts loading, trigger by [/arouter/service/interceptor][ ]
D/ARouter::: The group [arouter] has already been loaded, trigger by [/arouter/service/interceptor][ ]
I/ARouter::: Thread production, name is [ARouter task pool No.1, thread No.2][ ]
I/ARouter::: ARouter init over.[ ]
// >>>>>>>>>>>>>>>>>>> 插件日志输出
I/ARouter::: ARouter openLog[ ]
I/ARouter::: ARouter openDebug[ ]
I/ARouter::: ARouter init start.[ ]
I/ARouter::: Run with debug mode or new install, rebuild router map.[ ]
I/ARouter::: VM with name 'Android' has multidex support
E/ARouter::: InstantRun support error, com.android.tools.fd.runtime.Paths
I/ARouter::: Thread production, name is [ARouter task pool No.1, thread No.1][ ]
D/ARouter::: Filter 0 classes by packageName <com.alibaba.android.arouter.routes>
I/ARouter::: Find router map finished, map size = 0, cost 14 ms.[ ]
I/ARouter::: Load root element finished, cost 0 ms.[ ]
E/ARouter::: No mapping files were found, check your configuration please![ ]
D/ARouter::: LogisticsCenter has already been loaded, GroupIndex[0], InterceptorIndex[0], ProviderIndex[0][ ]
I/ARouter::: ARouter init success![ ]
W/ARouter::: ARouter::There is no route match the path [/arouter/service/interceptor], in group [arouter][ ]
I/ARouter::: ARouter init over.[ ]
大致分析一下日志发现,当作为插件时,路由映射配置为 0(Find router map finished, map size = 0
),为了找到 ARouter 初始化失败的原因,简单的跟踪了一下源码:
ARouter 初始化失败,那么一般要从初始化入口开始查起,即
ARouter.init(application)
public final class ARouter {
public static void init(Application application) {
if (!hasInit) {
hasInit = _ARouter.init(application);
...
}
}
}
final class _ARouter {
protected static synchronized boolean init(Application application) {
LogisticsCenter.init(mContext, executor);
...
return true;
}
}
可以看到 ARouter#init()
最终会执行 LogisticsCenter.init(mContext, executor)
,下面来看 LogisticsCenter.init(mContext, executor)
的源码:
public class LogisticsCenter {
/**
* LogisticsCenter init, load all metas in memory. Demand initialization
*/
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
// Step1. 获取到 app 所有的 class
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.");
// These class was generated by arouter-compiler.
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.");
// Step2. 加载 路由配置、拦截器 相关类
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);
}
}
...
}
}
这段代码的核心是最后的 for 循环,大致意思就是通过判断类名规则,过滤出 ARouter 相关的特定类,比如 路由分组信息
相关类、拦截器
相关类,并分别加载进 Warehouse.groupsIndex
和 Warehouse.interceptorsIndex
,不过它不是出问题的关键。
补充:ARouter 的工程源码中有
arouter-annotation
和arouter-compiler
这 2 个额外的 Module,这是使用 ARouter 能够进行依赖注入
、组件解耦
的技术核心:编译时注解
+JavaPoet
。就组件解耦而言,ARouter 通过编译时注解技术,可以在工程编译期间,获取到使用了@Route
注解的类,结合JavaPoet
生成特定的 class 文件,用来描述 path 与组件之间的对应关系(以及各个参数)。
根据输出日志信息,可以知道 routerMap
中的元素个数为 0,而 routerMap
是通过 ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)
得到的,再来看 ClassUtils
源码:
public class ClassUtils {
/**
* 通过指定包名,扫描包下面包含的所有的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();
Log.d(Consts.TAG, "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
return classNames;
}
/**
* get all the dex path
*
* @param context the application context
* @return all the dex path
* @throws PackageManager.NameNotFoundException
* @throws IOException
*/
public static List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {
ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
File sourceApk = new File(applicationInfo.sourceDir);
List<String> sourcePaths = new ArrayList<>();
sourcePaths.add(applicationInfo.sourceDir); //add the default apk path
//the prefix of extracted file, ie: test.classes
String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
// 如果VM已经支持了MultiDex,就不要去Secondary Folder加载 Classesx.zip了,那里已经么有了
// 通过是否存在sp中的multidex.version是不准确的,因为从低版本升级上来的用户,是包含这个sp配置的
if (!isVMMultidexCapable()) {
//the total dex numbers
int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
//for each dex file, ie: test.classes2.zip, test.classes3.zip...
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
File extractedFile = new File(dexDir, fileName);
if (extractedFile.isFile()) {
sourcePaths.add(extractedFile.getAbsolutePath());
//we ignore the verify zip part
} else {
throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
}
}
}
if (ARouter.debuggable()) { // Search instant run support only debuggable
sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo));
}
return sourcePaths;
}
}
因为 ClassUtils
的 getFileNameByPackageName()
和 getSourcePaths()
代码很关键,所以原封不动拷贝过来了,大致的逻辑是:通过包名获取到 apk 安装时解压出来的所有 dex 文件路径,再通过加载 dex 文件,提取出其中所有的 class,然后再回到 LogisticsCenter.init(mContext, executor)
中最后的那个 for 循环,加载出所有的路由和拦截器配置。那问题就来了:
# Q: 为什么 routerMap
中的元素个数为 0 ?或者说,为什么 dex 文件提取不出 class ?
A: 根据 RePlugin 官方 wiki 中,对插件的目录结构的介绍中,可以知道,插件的 dex 文件就不在常规目录下,所以 ARouter 压根就获取不到插件的 dex 文件,更别说加载插件中的路由及拦截器配置了。
https://github.com/Qihoo360/RePlugin/wiki/插件的管理#插件的目录结构 (opens new window)
# Q: 那要怎样才能让 ARouter 加载到插件的 dex 文件呢?
A: 理论上可以在插件中,通过反射的方式给 ARouter 的 Warehouse.groupsIndex
追加路由配置信息。或者对 ARouter 进行代码改造,在 LogisticsCenter.init(mContext, executor)
中插入获取当前插件 dex 文件中所有 class 的代码。
# Q: 这样就能让 ARouter 在插件中正常工作了吗?
A: 理论上是的。不过 Activity 可能还是会跳转失败。
# 2、Activity 跳转失败
ARouter 组件路由的关键方法就是 navigation()
,而所有重载的 navigation()
方法最终都会走向 _ARouter#_navigation()
方法,其源码如下:
final class _ARouter {
private Object _navigation(final Postcard postcard, final int requestCode, final NavigationCallback callback) {
final Context currentContext = postcard.getContext();
switch (postcard.getType()) {
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
// Set flags.
int flags = postcard.getFlags();
if (0 != flags) {
intent.setFlags(flags);
}
// Non activity, need FLAG_ACTIVITY_NEW_TASK
if (!(currentContext instanceof Activity)) {
intent.addFlags(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);
}
});
break;
case PROVIDER:
return postcard.getProvider();
case BOARDCAST:
case CONTENT_PROVIDER:
case FRAGMENT:
Class<?> fragmentMeta = postcard.getDestination();
try {
Object instance = fragmentMeta.getConstructor().newInstance();
if (instance instanceof Fragment) {
((Fragment) instance).setArguments(postcard.getExtras());
} else if (instance instanceof android.support.v4.app.Fragment) {
((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
}
return instance;
} catch (Exception ex) {
logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
}
case METHOD:
case SERVICE:
default:
return null;
}
return null;
}
}
这里主要看 switch-case
中 ACTIVITY
分支部分,在创建好 Intent 之后,就会调用 _ARouter#startActivity()
来启动 Activity,其源码如下:
final class _ARouter {
/**
* Start activity
*
* @see ActivityCompat
*/
private void startActivity(int requestCode, Context currentContext, Intent intent, Postcard postcard, NavigationCallback callback) {
if (requestCode >= 0) { // Need start for result
if (currentContext instanceof Activity) {
ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
} else {
logger.warning(Consts.TAG, "Must use [navigation(activity, ...)] to support [startActivityForResult]");
}
} else {
ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
}
if ((-1 != postcard.getEnterAnim() && -1 != postcard.getExitAnim()) && currentContext instanceof Activity) { // Old version.
((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
}
if (null != callback) { // Navigation over.
callback.onArrival(postcard);
}
}
}
可以看到 _ARouter#startActivity()
是通过 ActivityCompat.startActivity()
来启动 Activity,有一点很关键,插件中的 Activity,必须由插件的 context 来启动,那么这里的 currentContext
是谁?通过 Postcard#setContext()
的调用可以定位到以下源码:
final class _ARouter {
private static Context mContext;
protected static synchronized boolean init(Application application) {
mContext = application;
...
return true;
}
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
// Set context to postcard.
postcard.setContext(null == context ? mContext : context);
...
}
}
也就是说,在调用 _ARouter#navigation()
时,如果外部有传入 context 就使用外部的 context,否则使用 application,而 _ARouter
的 application 来自框架初始化时 ARouter#init(application)
传入的 application,好了,现在的问题就是,这个 application 是宿主的,还是插件的?
下面只考虑作为插件的情况,因为作为单品没有宿主插件之分。
# 情景 1:在自定义 Application 中初始化 ARouter
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ARouter.init(this); // this是插件的application
}
}
- 自定义 Application 中的 this 就是【插件】的 application。
# 情景 2:在 Activity 中初始化 ARouter
public class MyActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ARouter.init(this.getApplication()); // 宿主application
ARouter.init(((Application) this.getApplicationContext())); // 插件application
}
}
activity.getApplication()
:拿到【宿主】的 applicationactivity.getApplicationContext()
:拿到【插件】的 application
相关 issue:
- https://github.com/Qihoo360/RePlugin/issues/550 (opens new window)
- https://github.com/Qihoo360/RePlugin/issues/335 (opens new window)
综上,出现 Activity 跳转失败的本质原因就是使用了宿主的 context,主要发生在非自定义 Application 中初始化 ARouter 的场景,现在解决这个问题就很简单了,有 2 种解决方案:
如果你是在自定义 Application 中初始化 ARouter 的话,下面就不用看了。
# 方案 1:在非自定义 Application 中使用 applicationContext
初始化 ARouter:
public class MyActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ARouter.init(((Application) this.getApplicationContext()));
}
}
# 方案 2:使用带 context 参数的 navigation(Context)
方法进行路由操作:
public class MyActivity extends AppCompatActivity implements View.OnClickListener {
private Button button;
@Override
public void onClick(View view) {
ARouter.getInstance().build("/module2/other").navigation(this);
}
}
# 三、LiteARouter
通过以上分析得出,为了让 ARouter 能够在 RePlugin 中正常初始化,有两种方案:
- 反射:通过反射的方式给 ARouter 的追加路由配置信息
Warehouse.groupsIndex
,拦截器Warehouse.interceptorsIndex
以及 IProvider 服务Warehouse.providersIndex
。 - 改造:对 ARouter 进行代码改造,在
LogisticsCenter.init(mContext, executor)
中插入获取当前插件 dex 文件中所有 class 的代码。
以上两种方案各有利弊,个人觉得改造的方式应该比较好一点,不过呢,我并没有完全按上面的方式来处理,一方面是因为 ARouter 的设计思路是比较复杂的,除了路由,还有拦截器,依赖注入,以及 IProvider 等服务,并不能完全保证除了路由以外的其他功能是否能正常使用,另一方面是因为项目时间紧、任务重,开发时间严重不足,而且我们只需要用到路由功能。于是,我的做法是改造 ARouter,只保留路由功能,并命名为 LiteARouter (opens new window)。
- LiteARouter 的 Git 仓库:https://github.com/GitLqr/LiteARouter (opens new window)
# 1、arouter-compiler 分析
通过分析 arouter-compiler
代码,结合 jadx 反编译,可以知道最终会在 com.alibaba.android.arouter.routes
包下生成 ARouter$$Root$$XXX
的类,比如:
package com.alibaba.android.arouter.routes;
import com.alibaba.android.arouter.facade.template.IRouteGroup;
import com.alibaba.android.arouter.facade.template.IRouteRoot;
import java.util.Map;
public class ARouter$$Root$$substance implements IRouteRoot {
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("substance", ARouter$$Group$$substance.class);
}
}
substance 是我的主程序 Module 名,一般是 app,也就是 gradle 文件中
AROUTER_MODULE_NAME
参数对应的值。
可以知道 ARouter$$Root$$XXX
的 loadInto(Map<String, Class<? extends IRouteGroup>> routes)
方法会将路由的【分组信息】保存到 routes
中,而这个 route
正是 Warehouse.groupsIndex
。另外,真正的路由【映射信息】则是生成在了 ARouter$$Group$$XXX
类中,比如:
package com.alibaba.android.arouter.routes;
import com.alibaba.android.arouter.facade.enums.RouteType;
import com.alibaba.android.arouter.facade.model.RouteMeta;
import com.alibaba.android.arouter.facade.template.IRouteGroup;
import com.charylin.substance.screen.main.MainActivity;
import java.util.Map;
public class ARouter$$Group$$substance implements IRouteGroup {
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/substance/main", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/substance/main", "substance", (Map<String, Integer>) null, -1, Integer.MIN_VALUE));
}
}
ARouter$$Group$$XXX
的 loadInto(Map<String, RouteMeta> atlas)
会将路由【映射信息】保存到 atlas
中,而这个 atlas
正是 Warehouse.routes
。
# 2、LogisticsCenter 分析
现在回过头再来看 LogisticsCenter.init(mContext, executor)
中最后的那个 for 循环,就比较清楚它是怎么加载路由配置信息的了:
public class LogisticsCenter {
/**
* LogisticsCenter init, load all metas in memory. Demand initialization
*/
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
...
// Step2. 加载 路由配置、拦截器 相关类
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);
}
}
...
}
}
其核心就是反射拿到 ARouter$$Root$$XXX
、 ARouter$$Interceptors$$XXX
、 ARouter$$Providers$$XXX
类并创建实例,最终丢到 Warehouse
中,细心的你可能发现了,这里怎么没有 Warehouse.routes
?其实 LogisticsCenter
有一个很重要的 completion(Postcard)
方法,主要是对 Postcard 中的信息进行完善填充,以下是 completion(Postcard)
方法源码:
public class LogisticsCenter {
/**
* Completion the postcard by route metas
*
* @param postcard Incomplete postcard, should complete by this method.
*/
public synchronized static void completion(Postcard postcard) {
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
if (null == routeMeta) {
// Maybe its does't exist, or didn't load.
if (!Warehouse.groupsIndex.containsKey(postcard.getGroup())) {
throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
} else {
// Load route and cache it into memory, then delete from metas.
try {
addRouteGroupDynamic(postcard.getGroup(), null);
} catch (Exception e) {
throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
}
completion(postcard); // Reload
}
}
...
}
public synchronized static void addRouteGroupDynamic(String groupName, IRouteGroup group) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
if (Warehouse.groupsIndex.containsKey(groupName)){
// If this group is included, but it has not been loaded
// load this group first, because dynamic route has high priority.
Warehouse.groupsIndex.get(groupName).getConstructor().newInstance().loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(groupName);
}
// cover old group.
if (null != group) {
group.loadInto(Warehouse.routes);
}
}
}
LogisticsCenter#completion(Postcard)
源码的大致逻辑是,当根据 Postcard 中的路由 path 从 Warehouse.routes
中获取不到 routeMeta 时,即为路由信息缺失,这时,会根据 Postcard 中的路由 group (分组信息)动态加载到具体的路由配置,并保存到 Warehouse.routes
中,所以说,其实 Warehouse.routes
中具体的 routeMeta 是按需加载的。
# 3、LogisticsCenter 改造
综上,因为 Warehouse.routes
会在 LogisticsCenter#completion(Postcard)
方法中动态加载填充,所以,我只需要通过反射把路由【分组信息】加载进 Warehouse.groupsIndex
即可,于是我改造了 LogisticsCenter#loadRouterMap()
方法:
public class LogisticsCenter {
private static Context sContext;
private static ThreadPoolExecutor sExecutor;
private LogisticsCenter() {
}
public synchronized static void init(Context context, ThreadPoolExecutor tpe) {
loadRouterMap();
}
private static void loadRouterMap() {
try {
String nameOfRouteRootClass = Consts.NAME_OF_ROUTE_ROOT_CLASS; // com.charylin.litearouter.routes.LiteARouter$$Root
Class<?> routeRootClz = Class.forName(nameOfRouteRootClass);
if (routeRootClz != null) {
IRouteRoot routeGroup = routeRootClz.asSubclass(IRouteRoot.class).newInstance();
if (routeGroup != null) {
routeGroup.loadInto(Warehouse.groupsIndex);
}
}
} catch (Exception e) {
LiteARouter.logger.error(Consts.TAG, "load router map error.", e);
}
}
}
public final class Consts {
public static final String SEPARATOR = "$$";
public static final String PROJECT = "LiteARouter";
public static final String TAG = PROJECT + "::";
public static final String NAME_OF_ROOT = PROJECT + SEPARATOR + "Root";
public static final String PACKAGE_OF_GENERATE_FILE = "com.charylin.litearouter.routes";
public static final String NAME_OF_ROUTE_ROOT_CLASS = PACKAGE_OF_GENERATE_FILE + '.' + NAME_OF_ROOT;
}
为了反射足够简单,我把 ARouter$$Root$$XXX
类名改为 LiteARouter$$Root
,也就是说类名后面不追加模块名。即 gradle 文件中不需要配置 AROUTER_MODULE_NAME
参数了。
# 4、arouter-compiler 改造
既然记载了路由【分组信息】的类名( ARouter$$Root$$XXX
)设计变化了,那么在 compiler 中也需要对生成的类名规则进行修改:
@AutoService(Processor.class)
@SupportedAnnotationTypes({ANNOTATION_TYPE_ROUTE})
public class RouteProcessor extends BaseProcessor {
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
...
// Write root meta into disk.
String rootFileName = NAME_OF_ROOT; // 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);
}
}
因为只需要保留了路由功能,所以,把拦截器、依赖注入等不需要用到的功能对应的生成类逻辑一并精简掉了。
至此,便是我在处理 RePlugin+ARouter 搭配时发现的问题思考及解决方案,以上只是此次改造 ARouter 中最重要的部分,更多细节可通过对比 LiteARouter (opens new window) 与 ARouter 各个文件的差异来了解。
- 01
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21
- 02
- Flutter - 轻松实现PageView卡片偏移效果09-08
- 03
- Flutter - 升级到3.24后页面还会多次rebuild吗?🧐08-11