《Android原理》ARouter

一、概述

一个支持模块间的路由、通信、解耦,帮助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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,188评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,464评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,562评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,893评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,917评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,708评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,430评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,342评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,801评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,976评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,115评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,804评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,458评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,008评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,135评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,365评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,055评论 2 355

推荐阅读更多精彩内容