ARouter 1.5.0

ARouter

帮助Android App进行组件化改造的框架,支持模块间路由、通信、解耦

支持功能:

  • 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
  • 支持多模块工程使用
  • 支持添加多个拦截器,自定义拦截顺序
  • 支持依赖注入,可单独作为依赖注入框架使用
  • 支持InstantRun
  • 支持MultiDex(Google方案)
  • 映射关系按组分类、多级管理,按需初始化
  • 用户指定全局降级与局部降级策略
  • 页面、拦截器、服务等组件均自动注册到框架
  • 多种方式配置跳转动画
  • 获取Fragment
  • 完全支持Kotlin以及混编
  • 第三方App加固(使用arouter-register实现自动注册)
  • 生成路由文档
  • IDE插件便捷关联路径和目标类

应用

  • 从外部URL映射到内部页面,参数传递和解析
  • 跨模块页面跳转,模块解耦
  • 拦截跳转过程,处理登陆、埋点
  • 跨模块API调用,控制反转来组件解耦

依赖(在app、子module等需要路由跳转的模块添加):

plugins {
    // module如下,如果是app(id 'com.android.application')
    id 'com.android.library'
   // kotlin 扩展
    id 'kotlin-android'
    id 'kotlin-android-extensions'
    id 'kotlin-kapt'
}

// kotlin模块(和javamodule模块不一样注意)
kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.getName())
    }
}

dependencies{
implementation 'com.alibaba:arouter-api:1.5.0'
kapt 'com.alibaba:arouter-compiler:1.2.2'
}

初始化:在Application中进行初始化

class BaseApplication :Application(){
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG){
            ARouter.openDebug()
            ARouter.openLog()
        }
        ARouter.init(this)
    }
}

一个包含多个module项目,名为user的module中存在一个UserHomeActivity,路由路径:/account/userHome。从其他module跳转该页面,指定path来跳转。

// 主app里的
@Route(path = "/account/userHome")
class UserHomeActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_home)
    }
}

// module里的
@Route(path = "/library1/test")
class Library1Activity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_library1)

        tvTest.setOnClickListener{
            // 跨模块跳转
            ARouter.getInstance().build("/account/userHome").navigation()
        }
    }
}

编译阶段生成辅助代码来实现path跳转。需要拿到Activity的Class对象才行。编译阶段,ARouter根据我们设定的路由跳转规则来自动生成映射文件,包含path和ActivityClass之间的对应关系。
UserHomeActivity编译阶段自动生成辅助java文件ARouter$$Group$$account类中就将path和ActivityClass作为键值对保存到了Map中,ARouter依靠此进行跳转。

public class ARouter$$Group$$account implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/account/userHome", RouteMeta.build(RouteType.ACTIVITY, UserHomeActivity.class, "/account/userhome", "account", null, -1, -2147483648));
  }
}

这类自动生成的文件包名路径都是com.alibaba.android.arouter.routes,类名前缀特定规则。虽然ARouter$$Group$$account类实现了对应关系保存到Map中,loadInto方法还是需要有ARouter运行时来调用,ARouter就需要拿到ARouter$$Group$$account这个类才行,ARouter通过扫描com.alibaba.android.arouter.routes这个包名路径来获取所有辅助文件。

1、程序员自己维护特定path和特定目标类之间对应关系,ARouter只要求开发者使用包含path的@Route 注解修饰目标类
2、ARouter在编译阶段通过注解处理器自动生成path和特定的目标类之间的对应关系,path作为key,将目标类Class对象为value之一存到Map中
3、在运行阶段,通过path来发起请求,ARouter根据path从map中取值,拿到目标类

ARouter类使用单例模式,暴露外部调用的API。实现逻辑交给_ARouter来完成。
_ARouter类包私有权限,使用了单例,通过init(Application)-->LogisticsCenter.init(mContext, executor)

LogisticsCenter实现扫描特定包名路径拿到所有自动生成的辅助文件的逻辑。在进行初始化的时候,加载到当前项目一共包含所有group,以及每个group对应的路由信息表。
1、如果开启debug模式或通过本地SP缓存判断出app的版本前后发生变化,需要重新获取路由信息,否则从使用之前缓存到SP中的数据。
2、获取全局路由信息是耗时操作,所以ARouter通过将全局路由信息缓存到SP中来实现。开发阶段可能随时添加路由表,每次发布新的版本正常都会加大应用版本号,所以ARouter只开启了debug模式或者版本号发生变化的时候才会重新获取路由信息
3、获取路由信息包含了com.alibaba.android.arouter.routes包下自动生成的辅助文件的全路径,判断路径名前缀字符串,知道该类什么类型文件,通过反射构建不同类型对象,调用对象的方法将路由信息存到Warehouse的Map中。

UserHomeActivity,其path为/account/userHome,ARouter默认会将path的第一个单词account作为group,而UserHomeActivity放到名为app(module名字)的module中

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("account", ARouter$$Group$$account.class);
  }
}

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$account implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/account/userHome", RouteMeta.build(RouteType.ACTIVITY, UserHomeActivity.class, "/account/userhome", "account", null, -1, -2147483648));
  }
}

LogisticsCenter的init文件名前缀ARouter$$Root$$定位到ARouter$$Root$$app这个类,然后通过反射构建出该对象,通过调用loadInto的页面时,反射调用ARouter$$Group$$account的loadInto方法,按需加载,等到需要的时候再获取详细的路由对应信息。

    ARouter.getInstance().build(RoutePath.USER_HOME).navigation()

build()通过ARouter中转调用_ARouter的build()返回Postcard对象,用于传入跳转配置参数,例如:mBundle、开启绿色通道greenChannel、跳转动画optionsCompat等

Postcard的navigation()方法会调用_ARouter完成Activity跳转。
navigation方法的重点在于LogisticsCenter.completion(postcard)。按需加载反射调用ARouterGroupaccount 的 loadInto 方法。completion方法就获取详细路由信息。通过postcard携带的path和group信息从Warehouse取值,如果不是null信息保存到postcard中,为null则抛出NoRouteFoundException

跳转到Activity并注入参数

            // 带参数跳转
            ARouter.getInstance()
                .build("/account/userHome")
                .withLong("userId",13)
                .withString("userName","小明")
                .navigation()

// 参数接收
@Route(path = "/account/userHome")
class UserHomeActivity : AppCompatActivity() {
    private val TAG = "UserHome-"
    @Autowired(name = "userId")
    @JvmField
    var userId:Long = 0

    @Autowired(name = "userName")
    @JvmField
    var userName = ""
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_home)
        // 需要注册,否则无法接收到参数传递
        ARouter.getInstance().inject(this)
        Log.d(TAG, "onCreate: userId:$userId userName:$userName")
    }
}

通过@Autowired注解修饰变量。声明name参数,传递键值对的key对应。通过ARouter.getInstance().inject(this)后,完成参数赋值。
生产辅助代码:

public class UserHomeActivity$$ARouter$$Autowired implements ISyringe {
  private SerializationService serializationService;

  @Override
  public void inject(Object target) {
    serializationService = ARouter.getInstance().navigation(SerializationService.class);
    UserHomeActivity substitute = (UserHomeActivity)target;
    substitute.userId = substitute.getIntent().getLongExtra("userId", substitute.userId);
    substitute.userName = substitute.getIntent().getStringExtra("userName");
  }
}

携带的参数放到Intent,inject方法实现了从Intent取值向变量赋值,要求变量是public的,kotlin需要同时向变量加上@JvmField注解的原因。参数注解:ARouter.getInstance().inject(this)
ARouter通过控制反转拿到AutowiredService对应实现类AutowiredServiceImpl的对象,调用autowire方法完成参数注入。
由于参数注入辅助类的类名具有固定的包名和类名,目标类类名+$$ARouter$$Autowired, 所以AutowiredServiceImpl传入的instance参数和反射过来生成的对象,最终调用inject方法完成参数注入。

控制反转

跳转Activity并自动注入参数属于依赖注入的一种,ARouter同时也支持控制反转:通过接口获取其实现类实例
存在一个ISayHelloService接口,需要拿到其实现类实例,但是不希望使用的时候和特定实现类SayHelloService绑定在一起造成耦合,使用ARouter控制反转功能,要求ISayHelloService接口继承了IProvider接口才行
注意:Route(path = "/account/sayhello")在实现的接口和调用的类path要一致,才能实现反转控制。

// 接口
interface ISayHelloService :IProvider {
    fun sayHello()
}

// 具体实现
@Route(path = "/account/sayhello")
class SayHelloService :ISayHelloService{
    private val TAG = "SayHello-"
    override fun sayHello() {
        Log.d(TAG, "sayHello: ")
    }

    override fun init(context: Context?) {
        Log.d(TAG, "init: ")
    }
}

// 具体使用
@Route(path = "/account/sayhello")
class MainActivity : AppCompatActivity() {
    private val TAG = "Main-网络测试"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        textNet.setOnClickListener {
        ARouter.getInstance().navigation(ISayHelloService::class.java).sayHello()
        }
    }
}

在使用的时候直接传递ISayHelloService的Class对象即可,ARouter会将SayHelloService以单例形式返回,无需开发者手动构建SayHelloService对象,从而达到解耦。

ARouter.getInstance().navigation(ISayHelloService::class.java).sayHello()

和实现Activity跳转一样,ARouter自动生成几个文件,包含路由表映射关系。

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$account implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/account/sayhello", RouteMeta.build(RouteType.PROVIDER, SayHelloService.class, "/account/sayhello", "account", null, -1, -2147483648));
  }
}

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Providers$$app implements IProviderGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> providers) {
    providers.put("com.george.learnretrofit.test.ISayHelloService", RouteMeta.build(RouteType.PROVIDER, SayHelloService.class, "/account/sayhello", "account", null, -1, -2147483648));
  }
}

LogisticsCenter实现扫描特定包名路径拿到所有自动生成的辅助文件逻辑。最终Warehouse中就会在初始化的时候拿到数据
Warehouse.groupsIndex:

  • account--->class com.alibaba.android.arouter.routes.ARouter$$Group$$account
    Warehouse.providersIndex:
    com.george.learnretrofit.test.ISayHelloService--->RouteMeta.build(RouteType.PROVIDER, SayHelloService.class, "/account/sayhello", "account", null, -1, -2147483648)

LogisticsCenter.completion(postcard)获取对象实例时候同时将实例缓存起来,以后复用。

拦截器

可以通过拦截器来判断用户是否处于登陆状态,还未登陆的话,拦截请求,打开登陆界面。
可以添加多个拦截器,每个拦截器设置不同优先级

@Interceptor(priority = 100,name="啥也不做的拦截器")
class NothingInterceptor :IInterceptor{
    private val TAG = "Nothing-1-"
    override fun process(postcard: Postcard?, callback: InterceptorCallback?) {
        // 不拦截,任其跳转
        Log.d(TAG, "process: ")
        callback?.onContinue(postcard)
    }

    override fun init(context: Context?) {

        Log.d(TAG, "init: ")
    }
}

@Interceptor(priority = 200, name = "登陆拦截器")
class LoginInterceptor: IInterceptor{
    private val TAG = "Nothing-2-"
    override fun process(postcard: Postcard?, callback: InterceptorCallback?) {

        if (postcard?.path == "/account/userHome"){
            // 拦截
            Log.d(TAG, "process: 拦截-到主页")
            callback?.onInterrupt(null)
            ARouter.getInstance().build("/account/sayhello").navigation()
        }else{
            Log.d(TAG, "process: 不拦截")
            // 不拦截,任其跳转
            callback?.onContinue(postcard)
        }
    }

    override fun init(context: Context?) {
        Log.d(TAG, "init: ")
    }

}

priority越小优先级越高(越先执行)。
_ARouter的navigation方法,转交给interceptorService,判断有没有开启绿色通道模式。InterceptorServiceImpl实现类,ARouter初始化过程中通过控制反转拿到interceptorService实例。
1、第一次获取InterceptorServiceImpl实例时候,其init方法会马上被调用,交由线程池来处理。通过反射生成每个拦截器对象,并调用每个拦截器的init方法完成拦截器初始化,将每个拦截器对象都存到Warehouse.interceptors中。如果初始化完成,则唤醒等待的interceptorInitLock上的线程。
2、doInterceptions方法被调用,如果第一个步骤未执行完,则通过checkInterceptorsInitStatus()方法等待第一次步骤完成。如果10s内都未完成,则走失败流程直接返回。
3、在线程池中遍历拦截器列表,如果某个拦截器拦截请求则调用callback.onInterrupt方法通知内部,否则调用callback.onContinue()方法继续跳转逻辑。

注解处理器

依靠注解器生成辅助文件,ARouter才能完成参数自动注入功能。
APT(Annotation Processing Tool)注解处理器,用在编译期扫描和处理注解,通过注解生成Java文件。即注解作为桥梁,预先设定好的代码生成规则,自动生成Java文件。例如:ButterKnife/Dragger2/EventBus等

Java API已经提供了扫描源码并解析注解的框架,通过继承AbstractProcessor类来实现自己的注解解析逻辑。APT的原理就是在注解了某些代码元素(如字段、函数、类等)后。在编译时编辑器会检查AbstractProcess的子类,并且自动调用process()方法,然后将指定注解的所有代码元素作为参数传递给该方法,开发者根据注解元素在编译期输出Java代码。

ARouter源码中和注解处理器相关的module

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