百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

RePlugin中初始化不了ARouter怎么办

toyiye 2024-06-21 12:09 7 浏览 0 评论

一、功能需求

1、组件解耦 (ARouter)

阿里开源的 ARouter 在组件化开发中这个库相当有用,其核心功能就是组件解耦,比如以往要跳转另一个 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

2、插件化 (RePlugin)

项目的架构设计涉及到插件化,我们使用的是 RePlugin,当然,每个公司对 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
  • https://github.com/alibaba/ARouter/issues/281
  • https://github.com/alibaba/ARouter/issues/172

以下是 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/插件的管理#插件的目录结构

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():拿到【宿主】的 application
  • activity.getApplicationContext():拿到【插件】的 application

相关 issue:

  • https://github.com/Qihoo360/RePlugin/issues/550
  • https://github.com/Qihoo360/RePlugin/issues/335

综上,出现 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 。


  • LiteARouter 的 Git 仓库:https://github.com/GitLqr/LiteARouter


通过分析 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 与 ARouter 各个文件的差异来了解。


  • LiteARouter 的 Git 仓库:https://github.com/GitLqr/LiteARouter


相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码