Potato icon indicating copy to clipboard operation
Potato copied to clipboard

Arouter: Main Process

Open yunshuipiao opened this issue 5 years ago • 0 comments

Arouter

[TOC]

需求背景

当一个 App 开发随着迭代而变得复杂时,多模块开发就显的很有必要,multimodule。

Android 原生方案的不足

原生路由方案一般是通过显式 intent 和隐式 intent 两种方式实现,均存在不足:

  1. 显式需要直接持有对应 class,从而导致强依赖关系,提高了耦合度。
  2. 隐式由于 action 定义在 Manifest,扩展较差;另一方面不适合协作。
  3. 由于 startActivity() 失败后无法降级,直接异常,不可取。

自定义路由框架的适用场景

  1. 支持动态跳转:比如电商 app,页面跳转需要较强的灵活性。
  2. 多模块化:随着业务量的不断增长,app也会不断的膨胀,开发团队的规模和工作量也会逐渐增大,面对所衍生的64K问题、协作开发问题等,app一般都会走向组件化。组件化就是将APP按照一定的功能和业务拆分成多个组件module,不同的组件独立开发,组件化不仅能够提供团队的工作效率,还能够提高应用性能。而组件化的前提就是解耦,那么我们首先要做的就是解耦页面之间的依赖关系
  3. Native 和 H5 的问题:无法从 H5 页面直接跳转

Arouter 概述

ARouter不仅实现了对基础组件的路由,而且其具有的Ioc功能也实现了对动作的路由,也就是跨模块API调用!

可以理解为“通过URL找Class能实现的业务,ARouter都能实现”,这个概念要理解透彻!

优点请参考官方文档:https://github.com/alibaba/ARouter/blob/master/README_CN.md

源码分析

直接从 github 官方仓库下载源码编译即可。

其中 app, module_java, module_kotlin, 是 demo 演示;arouter_annotation 包含注解数据和携带数据的bean;arouter_compiler 是注解处理相关,后面会专门介绍编译时代码生成相关的内容。比如该 demo 就在 build 目录下生成了 Arouter 相关的代码。

arouter-api 提供了给我们使用的api,以实现路由功能。

init

// 打印日志
ARouter.openLog();
// 调试模式不是必须开启,但是为了防止有用户开启了InstantRun,但是
// 忘了开调试模式,导致无法使用Demo,如果使用了InstantRun,必须在
// 初始化之前开启调试模式,但是上线前需要关闭,InstantRun仅用于开
// 发阶段,线上开启调试模式有安全风险,可以使用BuildConfig.DEBUG
// 来区分环境
ARouter.openDebug();
// 初始化,上面两行必须在 init 之前才起作用。
ARouter.init(getApplication());
/**
 * Init, it must be call before used router.
 */
public static void init(Application application) {
    if (!hasInit) {
      	// 外观模式或者装饰模式
        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.");
    }
}
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
    mContext = context;
  
  	// 线程池
    executor = tpe;

    try {
        long startInit = System.currentTimeMillis();
        //billy.qi modified at 2017-12-06
        //load by plugin first
        loadRouterMap();
      	// 路由表
        Set<String> routerMap;
				// 扫描包下面的文件,得到注解编译时生成的路由表    
        routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
            for (String className : routerMap) {
              		
                if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                    // This one of root elements, load root.
                  	// com.alibaba.android.arouter.routes.Arouter$$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
                  	// com.alibaba.android.arouter.routes.Arouter$$interceptor 拦截器前缀
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                    // Load providerIndex
                  	// com.alibaba.android.arouter.routes.Arouter$$Provider 前缀
                    ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                }
            }
        }
}
public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
    final Set<String> classNames = new HashSet<>();
		// 获取所有 分包dex 的路径
    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);
                    }	
                  	// 提取 其中的 class 类名
                    Enumeration<String> dexEntries = dexfile.entries();
                    while (dexEntries.hasMoreElements()) {
                        String className = dexEntries.nextElement();
                      	// 以 arouter 为前缀
                        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;
}

上述函数是将注解生成的路由表信息,拦截器信息和 Provider 信息存到缓存中。

// Cache route and metas
static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
static Map<String, RouteMeta> routes = new HashMap<>();

// Cache provider
static Map<Class, IProvider> providers = new HashMap<>();
static Map<String, RouteMeta> providersIndex = new HashMap<>();

// Cache interceptor
static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]");
static List<IInterceptor> interceptors = new ArrayList<>();

下面接着看一下生成的实现类做了什么工作:

public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    // key 为分组的名字,即路径的第一段,value 为分组中所有的映射关系
    routes.put("test", ARouter$$Group$$test.class);
    routes.put("yourservicegroupname", ARouter$$Group$$yourservicegroupname.class);
  }
}

//将module中使用@Route注解的activity或Fragment添加到集合中,这里的方法会在之后调用
public class ARouter$$Group$$test implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/test/activity1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/activity1", "test", new java.util.HashMap<String, Integer>(){{put("ser", 9); put("ch", 5); put("fl", 6); put("dou", 7); put("boy", 0); put("url", 8); put("pac", 10); put("obj", 11); put("name", 8); put("objList", 11); put("map", 11); put("age", 3); put("height", 3); }}, -1, -2147483648));
    atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(){{put("key1", 8); }}, -1, -2147483648));
    atlas.put("/test/activity3", RouteMeta.build(RouteType.ACTIVITY, Test3Activity.class, "/test/activity3", "test", new java.util.HashMap<String, Integer>(){{put("name", 8); put("boy", 0); put("age", 3); }}, -1, -2147483648));
    atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648));
    atlas.put("/test/fragment", RouteMeta.build(RouteType.FRAGMENT, BlankFragment.class, "/test/fragment", "test", null, -1, -2147483648));
    atlas.put("/test/webview", RouteMeta.build(RouteType.ACTIVITY, TestWebview.class, "/test/webview", "test", null, -1, -2147483648));
  }
}

IRouteRoot的实现将有@Route注解的module名添加到参数集合中,也就是groupsIndex。

官方的建议是路径分组与模块名相同,并且不同模块不要使用相同的分组。

上面的 RouteMeta 是一个数据 bean,封装了注解的相关内容;

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
    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;

    private Map<String, Autowired> injectConfig;  // Cache inject config.

Init 方法的内容大概如下, 还有一个 initAfter 方法。

static void afterInit() {
    // Trigger interceptor init, use byName.
    interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation();
}

根据官方的说明,IProvider接口是用来暴露服务,并且在初始化的时候会被调用init(Context context)方法。具体的服务有其实现提供,那我们就来看下它的实现做了些什么:

public void init(final Context context) {
    LogisticsCenter.executor.execute(new Runnable() {
        @Override
        public void run() {
            if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {
                for (Map.Entry<Integer, Class<? extends IInterceptor>> entry : Warehouse.interceptorsIndex.entrySet()) {
                    Class<? extends IInterceptor> interceptorClass = entry.getValue();
                    try {
                        IInterceptor iInterceptor = interceptorClass.getConstructor().newInstance();
                        iInterceptor.init(context);
                        Warehouse.interceptors.add(iInterceptor);
                    } catch (Exception ex) {
                        throw new HandlerException(TAG + "ARouter init interceptor error! name = [" + interceptorClass.getName() + "], reason = [" + ex.getMessage() + "]");
                    }
                }

                interceptorHasInit = true;

                logger.info(TAG, "ARouter interceptors init over.");

                synchronized (interceptorInitLock) {
                    interceptorInitLock.notifyAll();
                }
            }
        }
    });
}

还记的在上一步初始化的时候将所有注解了Interceptor的类的信息存入了Warehouse.interceptorsIndex,这里就将这些类实例化,并调用iInterceptor.init(context);完成自定义的初始化内容,最后放入Warehouse.interceptors集合中。

总结来说,init过程就是把所有注解的信息加载内存中,并且完成所有拦截器的初始化。

navagation

ARouter.getInstance()
        .build("/test/activity2")
        .navigation();
// 包含路由表的数据结构, 附加一些跳转信息,如参数之类
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;
protected Postcard build(String path) {
   			// 继承 IProvider 的接口,它是预留给用户实现路径动态变化功能,
        PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
        if (null != pService) {
            path = pService.forString(path);
        }
  			// 路径和分组
        return build(path, extractGroup(path));
}

        protected Postcard build(String path, String group) {
        if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                path = pService.forString(path);
            }
            return new Postcard(path, group);
        }
    }
// _Arouter.java
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    ...
    try {
       //试图找到跳转的目标,如果找不到则让callback回调onLost,或者交给全局降级策略处理。找到则回调callback的onFound方法。
        LogisticsCenter.completion(postcard);
    } catch (NoRouteFoundException ex) {
        logger.warning(Consts.TAG, ex.getMessage());

        if (debuggable()) {
            // Show friendly tips for user.
            runInMainThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(mContext, "There's no route matched!\n" +
                            " Path = [" + postcard.getPath() + "]\n" +
                            " Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show();
                }
            });
        }

        if (null != callback) {
            callback.onLost(postcard);
        } else {
            // No callback for this invoke, then we use the global degrade service.
          	// 交给全局降级策略
            DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
            if (null != degradeService) {
                degradeService.onLost(context, postcard);
            }
        }

        return null;
    }

    if (null != callback) {
        callback.onFound(postcard);
    }
		
  	//不是绿色通道,即经过拦截器处理
    if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.
        interceptorService.doInterceptions(postcard, new InterceptorCallback() {
            /**
             * Continue process
             *
             * @param postcard route meta
             */
            @Override
            public void onContinue(Postcard postcard) {
                _navigation(context, postcard, requestCode, callback);
            }

            /**
             * Interrupt process, pipeline will be destory when this method called.
             *
             * @param exception Reson of interrupt.
             */
            @Override
            public void onInterrupt(Throwable exception) {
                if (null != callback) {
                    callback.onInterrupt(postcard);
                }

                logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
            }
        });
    } else {
        return _navigation(context, postcard, requestCode, callback);
    }

    return null;
}

最后的 _navigation 过程很简单。

整个路由跳转的过程大致完成,整个过程可以分为:封装Postcard -> 查找信息集合,实例化目标类 -> 返回实例或者跳转。

上述的过程加遗漏了一个方法,这里分析一下:

// 通过路由基本信息完善 PostCard
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.
        Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  // Load route meta.
        if (null == groupMeta) {
            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 {
                if (ARouter.debuggable()) {
                    logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                }

                IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                iGroupInstance.loadInto(Warehouse.routes);
                Warehouse.groupsIndex.remove(postcard.getGroup());

                if (ARouter.debuggable()) {
                    logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                }
            } catch (Exception e) {
                throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
            }

            completion(postcard);   // Reload
        }
    } else {
        postcard.setDestination(routeMeta.getDestination());
        postcard.setType(routeMeta.getType());
        postcard.setPriority(routeMeta.getPriority());
        postcard.setExtra(routeMeta.getExtra());

        Uri rawUri = postcard.getUri();
        if (null != rawUri) {   // Try to set params into bundle.
            Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);
            Map<String, Integer> paramsType = routeMeta.getParamsType();

            if (MapUtils.isNotEmpty(paramsType)) {
                // Set value by its type, just for params which annotation by @Param
                for (Map.Entry<String, Integer> params : paramsType.entrySet()) {
                    setValue(postcard,
                            params.getValue(),
                            params.getKey(),
                            resultMap.get(params.getKey()));
                }

                // Save params name which need auto inject.
                postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));
            }

            // Save raw uri
            postcard.withString(ARouter.RAW_URI, rawUri.toString());
        }

        switch (routeMeta.getType()) {
            case PROVIDER:  // if the route is provider, should find its instance
                // Its provider, so it must implement IProvider
                Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                IProvider instance = Warehouse.providers.get(providerMeta);
                if (null == instance) { // There's no instance of this provider
                    IProvider provider;
                    try {
                        provider = providerMeta.getConstructor().newInstance();
                        provider.init(mContext);
                        Warehouse.providers.put(providerMeta, provider);
                        instance = provider;
                    } catch (Exception e) {
                        throw new HandlerException("Init provider failed! " + e.getMessage());
                    }
                }
                postcard.setProvider(instance);
                postcard.greenChannel();    // Provider should skip all of interceptors
                break;
            case FRAGMENT:
                postcard.greenChannel();    // Fragment needn't interceptors
            default:
                break;
        }
    }

总结

基本的流程分析结束,后续会继续分析 apt 的相关使用。

yunshuipiao avatar May 14 '19 09:05 yunshuipiao