组件化构想以及ARouter的使用分析

组件化

模块化、组件化与插件化

在项目发展到一定程度,随着人员的增多,代码越来越臃肿,这时候就必须进行模块化的拆分。在我看来,模块化是一种指导理念,其核心思想就是分而治之、降低耦合。而在Android工程中如何实施,目前有两种途径,也是两大流派,一个是组件化,一个是插件化。

既然组件化和插件化都是为了模块化而生的,那么他们有什么区别,我觉得最大的区别应该就是动态修改的能力,这里的动态修改指的是运行期的动态修改,插件化显然是可以支持的,但是组件化却不行,它只允许编译期的动态修改。

所以为什么要做的是组件化而不是插件化,作为RD我觉得理由大概如下吧

插件化有很多坑要躺——插件化框架本身的不稳定让开发者前赴后继的躺坑

发不完的版本——插件化可以运行时修改,PM表示非常完美,RD变身成真业务搬砖工

组件化没有黑科技,稳定——原生能力支持这种灵活配置的方式

组件化工作

代码解耦

一个比较理想的解耦状态应当是使用AndroidStudio提供的multiple module能力将主项目中的已有模块进行拆分,这里的module我们分为两种

一种是基础库library,这些代码可以直接被其他模块直接引用,比如网络库,我们称之为library。另一种是一个完整的功能模块,比如会员中心,我们称之为Component。拆分后的结果应该是类似于如下样式

模块化

那么解耦到什么样的效果,才是我们需要的呢,显然主模块以及各个Component之间不允许有直接的引用,我们解耦的主要目标就是要做到完全隔离的效果,不能直接使用其他Component内的类并且最好不了解其中的实现细节。

组件的单独调试

其实单独调试比较简单,只需要把apply plugin: ‘com.android.library’切换成apply plugin: ‘com.android.application’就可以,但是我们还需要修改一下AndroidManifest文件,因为一个单独调试需要有一个入口的Actiivity。具体如下

在gradle.properities配置中放入如下参数

##### 是否单独调试A模块 #####
DEBUG_MODULE_A=false
##### 主模块是否需要引入A模块 #####
NEED_MODULE_A=true

在业务module的build.gradle中添加如下代码

if (DEBUG_MODULE_A.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

android {
    // ...
    
    sourceSets {
        main {
            if (DEBUG_MODULE_A.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }

}

在主module的build.gradle中添加如下代码

// a模块非debug且需要打包a模块能力时,才包含a模块
if (!DEBUG_MODULE_A.toBoolean() && NEED_MODULE_A.toBoolean()) {
    implementation project(':module-a')
}

在业务module的src文件夹下添加对应的debug时需要使用的AndroidMainifest文件

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.baidu.input.module_a" >

    <application>
        <activity android:name="com.baidu.input.module_a.ModuleAMainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name="com.baidu.input.module_a.ModuleATestActivity"></activity>
    </application>

</manifest>

在业务module中添加入口Activity类

// 虚拟Activity,用于测试业务内功能
public class ModuleAMainActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.modulea_activity_main);
    }

    public void testActivity(View v) {
        // ...
    }

    public void testService(View v) {
        // ...
    }
}

通过上面的步骤,其实我们已经给组件搭了一个测试的环境,从而让组件的代码能够在单独的环境里运行,结构如下图所示

组件化

目前这种做法的缺点在于需要手动配置,同时对于manifest文件维护两份的成本也比较大,后期希望能够通过插件的方式自动进行配置,对于manifest则采用Android Studio支持的多flavor的manifest自动合并来去做。

组件的通信

上面说到解耦的时候提到了,主项目与各组件之间不允许直接进行引用,那么要实现跨模块的功能,就必然涉及到了通信的过程,这个过程应该如何进行呢。

比较通用的方式是使用路由来进行这部分的工作。具体后面会以ARouter的使用为例来进行说明。

集成调试

在组件的单独调试环节我们增加了下面的配置

##### 是否单独调试A模块 #####
DEBUG_MODULE_A=false
##### 主模块是否需要引入A模块 #####
NEED_MODULE_A=true

其中NEED_MODULE_A的配置就是用于后期集成调试准备的,当A模块的功能完成时,该配置应当被值为true,便于主模块对A模块进行依赖并将A模块的功能打包到整体APK中。

实际上,比较合适的做法是,在整个开发阶段(debug),主模块中都不应当包含类似于下面的配置

implementation project(':module-a')

这种依赖方式带来的缺点是主模块的开发人员会有意无意的直接引用到A模块中的类,这对于解耦工作来说是一个退化过程,因此可能也需要一个整体的开关用于控制开发阶段的依赖问题,比如

if (!DEBUG_MODULE_A.toBoolean() && NEED_MODULE_A.toBoolean() && !DEBUG.toBoolean()) {
    implementation project(':module-a')
}

但是正如单独调试中提到的,这种手动修改的方式毕竟非常的不友好,而且对于我们目前的项目而言不易于操作,因此考虑使用自定义插件的方式来进行,目前对Gradle插件还不是很熟悉,所以没有具体去尝试,大致的想法应该是希望能够判断当前build的类型并且根据配置文件的参数来决定是否
implementation对应的模块。

组件化规划

实际上组件化是一个比较长期而且耗时的过程,特别是将一个大工程进行组件化,要考虑的内容可以说是非常多的,具体体现在下面几点

路由库选择

正如前面所说的,目前组件化的工作需要进行组件间的通信,因此必须要有一个负责这部分工作的路由模块,这个模块应该如何选择,是自行实现还是选择第三方等等。

调试环境

目前调试环境的切分方式还不够自动化,可能需要开发额外的插件

组件化拆分

对于组件化工作而言,大部分时间可能都是消耗在拆分工作上,现在让我去想这个过程我都可以感觉到是很麻烦,但是细细考虑这部分的工作,还是有法可循的

  • 从产品需求到开发阶段到运营阶段都有清晰边界的功能开始拆分
  • 拆分过程中依赖项目的模块继续进行拆分,比如埋点、网络
  • 最终将主模块变成空壳,仅仅包含一些简单的拼接逻辑

路由

这里的路由就是根据路由表将请求发送到制定的位置,可以是一个页面也可以是一个服务抑或是其他形式的内容。目前Android平台的路由库还是比较丰富的,那么为什么要有这么一个路由组件主要有下面几点原因

开发与协作

根据我们对路由的定义,Android原生的路由方案一般是通过显式intent和隐式intent两种方式实现的,而在显式intent的情况下,因为会存在直接的类依赖的问题,导致耦合非常严重;而在隐式intent情况下,则会出现规则集中式管理,导致协作变得非常困难。

组件化

组件化是开发和协作中作为开发者所需要面对的问题,而一旦一款APP达到一定体量的时候,业务就会膨胀得比较严重,而开发团队的规模也会越来越大,这时候一般都会提出组件化的概念。组件化就是将APP按照一定的功能和业务拆分成多个小组件,不同的组件由不同的开发小组来负责,这样就可以解决大型APP开发过程中的开发与协作的问题,将这些问题分散到小的APP中。目前而言组件化已经有非常多比较成熟的方案了,而自定义路由框架也可以非常好地解决整个APP完成组件化之后模块之间没有耦合的问题,因为没有耦合时使用原生的路由方案肯定是不可以的。

Native和H5问题

Native与H5的问题主要是由于现在的APP很少是纯Native或者纯H5的,一般是将两者进行结合,那么他们之间需要一个统一负责处理页面跳转的管理模块,使用路由模块实现的中间跳转页就非常适合处理这种问题。
根据之前组件化的工作中的描述,路由是必须使用的一项工具,这里我使用ARouter库来介绍。

ARouter介绍

ARouter是阿里开源的一个Android平台中对页面及服务提供路由功能的中间件,

他有如下特点

  1. 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
  2. 支持多模块工程使用
  3. 支持添加多个拦截器,自定义拦截顺序
  4. 支持依赖注入,可单独作为依赖注入框架使用
  5. 支持InstantRun
  6. 支持MultiDex(Google方案)
  7. 映射关系按组分类、多级管理,按需初始化
  8. 支持用户指定全局降级与局部降级策略
  9. 页面、拦截器、服务等组件均自动注册到框架
  10. 支持多种方式配置转场动画
  11. 支持获取Fragment
  12. 完全支持Kotlin以及混编

简单的说ARouter的原理就是在编译阶段根据注解解释器对路由注解拦截器注解以及自动装配注解注解进行解释并生成辅助代码,待运行期与API接口一起提供给宿主APP使用,其中

路由注解——@Route

路由注解生成的路由表,是核心路由功能,之所以使用注解来实现主要考虑的是大型项目中的界面数量非常多,如果进行手动注册映射关系会非常麻烦,需要写很多重复冗余的代码,并且需要调用很多接口,因此ARouter使用了注解的方式进行帮我们自动注册。

拦截器注解——@Interceptor

拦截器注解用于对路由过程进行拦截,主要考虑的是原生路由能力无法在页面跳转的过程中添加自定义逻辑,而这一能力有时候有非常有必要可以避免许多重复逻辑的实现。ARouter中的拦截器也是通过注解的方式自动注册的。

自动装配——@Autowired

编译期对Autowired注解的字段进行扫描并注册到映射文件中,如果需要路由的目标界面调用了ARouter.inject(this),那么待运行时ARouter会查找到编译期为调用方生成的辅助类进行参数注入。

ARouter使用

以一个实际的例子来描述ARouter的使用,项目希望的简要结构如下
image.png

配置

路由跳转各个模块都需要使用,因此在ModuleRouter模块中引入ARouter所需要的库,上面一个是api接口,下面一个是注解解释器

dependencies {
    // 替换成最新版本, 需要注意的是api
    // 要与compiler匹配使用,均使用最新版可以保证兼容
    // 最新版本参考github的链接
    api "com.alibaba:arouter-api:${AROUTER_API}"
    annotationProcessor "com.alibaba:arouter-compiler:${AROUTER_COMPILER}"
    ...
}

由于ModuleRouter模块可能会使用到一些ARouter的注解,因此还需要添加下面的配置代码

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ moduleName : project.getName() ]
            }
        }
    }
}

ModuleA、ModuleB以及APP模块都需要依赖ModuleRouter,而且需要使用到注解,因此配置如下

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ moduleName : project.getName() ]
            }
        }
    }
}

// ...
dependencies {
    annotationProcessor "com.alibaba:arouter-compiler:${AROUTER_COMPILER}"
    implementation project(':modulerouter')
    // ...
}

初始化

ARouter初始化工作推荐尽早进行,因此放在Application的onCreate中

if (isDebug()) {           // 这两行必须写在init之前,否则这些配置在init过程中将无效
    ARouter.openLog();     // 打印日志
    ARouter.openDebug();   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化

注解

ARouter的路由功能所需要的路由表是在编译阶段根据注解生成辅助类中包含的,这里路由主要包含了路由界面和路由服务两部分。注解解释器已经在配置阶段在相应模块中通过配置添加了,但是想使用路由能力,就需要在特定地方加上注解。除此之外还有拦截器注解和自动装配注解

路由界面

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/modulea/test")
public class ModuleATestActivity extends Activity {
    ...
}

这里对@Route这个注解做个简单的解释,path这个字段里最前面的两个『/』中间的部分是路由表中『组』的标识,后面的内容是具体表示。『组』这个概念用于ARouter的分组加载的管理,避免一次性加载所有节点导致路由表瞬间增大。可以使用group字段进行自定义分组,其余字段部分可以参考源码中的注释。

@Route(path = "/com/test" , group = "wangchen")

一旦主动指定分组之后,应用内路由需要使用 ARouter.getInstance().build(path, group) 进行跳转,手动指定分组,否则无法找到

路由服务

对于需要路由的服务,需要实现IProvider接口

@Route(path = "/modulea/service")
public class ModuleAServiceImpl implements IProvider {
    ...
}

特殊服务

这里提两个特殊服务

对象解析服务

ARouter中如果要传递自定义对象,则需要使用该服务,实现SerializationService,并且使用@Route注解

@Route(path = "/service/json")
public class JsonServiceImpl implements SerializationService {
    @Override
    public void init(Context context) {

    }

    @Override
    public <T> T json2Object(String text, Class<T> clazz) {
        return JSON.parseObject(text, clazz);
    }

    @Override
    public String object2Json(Object instance) {
        return JSON.toJSONString(instance);
    }
}

降级服务

降级服务表示对路由失败的情况的处理,是全局生效的

// 实现DegradeService接口,并加上一个Path内容任意的注解即可
@Route(path = "/xxx/xxx")
public class DegradeServiceImpl implements DegradeService {
  @Override
  public void onLost(Context context, Postcard postcard) {
    // do something.
  }

  @Override
  public void init(Context context) {

  }
}

拦截器

拦截器全局生效,需要实现IInterceptor接口,priority表示拦截器优先级,优先级高的拦截器优先执行

@Interceptor(priority = 7)
public class Test1Interceptor implements IInterceptor {
    ...
}

自动装配

自动装配需要在对应成员变量处加上@Autowired注解

// 为每一个参数声明一个字段,并使用 @Autowired 标注
// URL中不能传递Parcelable类型数据,通过ARouter api可以传递Parcelable对象
@Route(path = "/test/activity")
public class Test1Activity extends Activity {
    @Autowired
    public String name;
    @Autowired
    int age;
    @Autowired(name = "girl") // 通过name来映射URL中的不同参数
    boolean boy;
    @Autowired
    TestObj obj;    // 支持解析自定义对象,路由表中需要存在SerializationService服务

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ARouter.getInstance().inject(this);

    // ARouter会自动对字段进行赋值,无需主动获取
    Log.d("param", name + age + boy);
    }
}

注意要尽可能早的调用

ARouter.getInstance().inject()

如果不需要自动装配,那么可以不调用ARouter.getInstance().inject(),但是如果希望可以通过URL跳转的方式进入该界面,则依然需要保留@Autowired注解,否则ARouter不知道应该如何从URL中提取参数类型放入Intent内。

发起路由

路由界面和服务略有不同

路由界面

针对上面定义的ModuleATestActivity,我们需要路由该界面时只需要下面的代码即可

ARouter.getInstance().build("/modulea/test").navigation();

对于手动指定的分组,则需要这样

ARouter.getInstance().build("/modulea/test", "module").navigation();

在实际项目开发中,为了协调路路径的问题,考虑将这部分的静态代码下沉至路由模块,因此在路由模块中添加如下代码

package com.baidu.input.imerouter;

/**
 * Created by wangchen on 02/03/18.
 */
public class RouterPath implements IModuleAPath, IModuleBPath {
}

interface IModuleAPath {

    String MODULE_A_TEST = "/modulea/test";

    String MODULE_A_SERVICE = "/modulea/service";

}

interface IModuleBPath {

    String MODULE_B_TEST = "/moduleb/test";
}

因此注解代码可以修改成

@Route(path = RouterPath.MODULE_A_TEST)
public class ModuleATestActivity extends Activity {
}

@Route(path = RouterPath.MODULE_A_SERVICE)
public class ModuleAServiceImpl implements ModuleAService {
}

发起路由代码可以修改成

ARouter.getInstance().build(RouterPath.MODULE_A_TEST).navigation();

在ARouter官方给的最佳实践中描述了这一点,对于所有界面跳转都建议使用ARouter的方式来进行统一管理,但是对于模块内的跳转似乎这么写又有些难受,因此给出如下的代码建议

@Route(path = RouterPath.MODULE_A_TEST)
public class ModuleATestActivity extends Activity {

    public static void launch(Activity c) {
        ARouter.getInstance().build(RouterPath.MODULE_A_TEST).navigation(c);
    }
}

这样对于所有可以直接引用ModuleATestActivity这个类的地方(本模块)就可以以一个非ARouter的方式进行界面跳转。

路由服务

首先要说的是,相对于上面的路由服务注册的代码,实际上ARouter建议以如下的方式进行路由服务的声明

定义服务接口

public interface ModuleAService extends IProvider {

    String callModuleAService(String msg);
}

实现服务接口

@Route(path = RouterPath.MODULE_A_SERVICE)
public class ModuleAServiceImpl implements ModuleAService {

    @Override
    public String callModuleAService(String msg) {
        Log.i("ModuleA", msg);
        return "ModuleA receive " + msg;
    }

    @Override
    public void init(Context context) {

    }
}

这样做的好处是分离了接口和实现,对于后面的模块解耦有比较大的帮助。

回过头来看路由服务的使用,ARouter中提供了两种路由服务的方式——byType和byName,这跟它的路由表实现有关,两种方式都可以在路由表中找到对应的服务。

byType

ARouter.getInstance().navigation(ModuleAService.class).callModuleAService("msg");

byName

((ModuleAService) ARouter.getInstance().build(RouterPath.MODULE_A_SERVICE).navigation()).callModuleAService("msg");

这两种方式在ModuleAService接口只有一个实现的时候没有问题,但是如果出现多实现时会有问题,由于路由表加载是一个map,因此此时实际使用的服务接口实现是自动生成的路由表加载代码中顺序靠后的一个,这种情况建议使用byName的方式来规避冲突。

再来看看为什么要进行接口和实现分离,很多时候我们需要跨模块进行服务调用,如果不进行分离直接使用实现类,那么根据上面两种方式,在发起路由的模块会产生一个对服务提供模块的直接依赖,这回对模块解耦产生影响。

但是换成接口和实现分离的形式来做的话,依然会有一个接口类的依赖,为了避免这种直接依赖问题,我们需要将接口类下沉到基础模块中,这里就是ModuleRouter,注意下面的package

接口位于路由模块

package com.baidu.input.imerouter;

import com.alibaba.android.arouter.facade.template.IProvider;

/**
 * Created by wangchen on 02/03/18.
 */
public interface ModuleAService extends IProvider {

    String callModuleAService(String msg);
}

实现位于业务模块

package com.baidu.input.module_a;

import android.content.Context;
import android.util.Log;

import com.alibaba.android.arouter.facade.annotation.Route;
import com.baidu.input.imerouter.ModuleAService;
import com.baidu.input.imerouter.RouterPath;

/**
 * Created by wangchen on 02/03/18.
 */
@Route(path = RouterPath.MODULE_A_SERVICE)
public class ModuleAServiceImpl implements ModuleAService {

    @Override
    public String callModuleAService(String msg) {
        Log.i("ModuleA", msg);
        return "ModuleA receive " + msg;
    }

    @Override
    public void init(Context context) {

    }
}

值得注意的是,希望一个模块最多对外提供一个服务接口,以确保路由模块的接口数量不会膨胀,同时该服务接口必须满足开闭原则

特殊服务

因为之前讨论的byType和byName问题,ARouter的实现对于特殊服务都是使用byType的形式来处理的,因此如果出现多服务实现可能会出现问题。此时建议全局仅使用一个自定义对象加载服务全局降级服务,这两个服务可以放在路由模块,便于统一处理。

其他

对于拦截器和自动装配以及其他路由发起方式的使用,可以参考官方demo,这里不做更多介绍了。

ARouter分析

ARouter注解

首先我们知道ARouter的自动注册的实现是利用了编译期自定义注解的处理来完成的。ARouter定义的注解的部分源码位于arouter-annotation,具体的实现这里不做源码分析了,只需要知道编译期ARouter会通过注解解释器生成帮助类,比如这样

APT

Routes

我们看下routes这个包下的内容,这个包下的类都是用于生成路由表的

先看Root的内容,可以看出这里做的事情是将路由分组放入map中,这里涉及到两个分组——service和test

/**
 * 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("service", ARouter$$Group$$service.class);
    routes.put("test", ARouter$$Group$$test.class);
  }
}

以其中一个分组test为例看代码,这里做的事情是将这个test分组内的所有具体路由项添加到一个map中,路由项的具体信息会包装成RouteMeta类的对象

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
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("pac", 9); put("ch", 5); put("fl", 6); put("obj", 10); put("name", 8); put("dou", 7); put("boy", 0); put("objList", 10); put("map", 10); put("age", 3); put("url", 8); 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));
  }
}

除此之外Interceptor中是拦截器路由项加载的类,而Provider中是服务路由项加载的类

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Interceptors$$app implements IInterceptorGroup {
  @Override
  public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
    interceptors.put(7, Test1Interceptor.class);
  }
}
/**
 * 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.alibaba.android.arouter.demo.testservice.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/service/hello", "service", null, -1, -2147483648));
    providers.put("com.alibaba.android.arouter.facade.service.SerializationService", RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl.class, "/service/json", "service", null, 10, -2147483648));
    providers.put("com.alibaba.android.arouter.demo.testservice.SingleService", RouteMeta.build(RouteType.PROVIDER, SingleService.class, "/service/single", "service", null, -1, -2147483648));
    providers.put("com.alibaba.android.arouter.facade.service.SerializationService", RouteMeta.build(RouteType.PROVIDER, TestService.class, "/service/test", "service", null, 50, -2147483648));
  }
}

从Provider这个自动生成类我们还可以发现一个问题,对于服务而言它可以通过Provider自动生成类的loadInto方法加载到路由表中,也可以通过具体所属组所提供的loadInto方法加载到路由表中,这也是ARouter对服务能提供byTypebyName两种路由方式的原因

Autowired

对于剩下的自动生成的类,都是以Autowired这个关键词结尾的,表明他们是负责自动装配的代码,以其中一个为例

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class Test1Activity$$ARouter$$Autowired implements ISyringe {
  private SerializationService serializationService;

  @Override
  public void inject(Object target) {
    serializationService = ARouter.getInstance().navigation(SerializationService.class);
    Test1Activity substitute = (Test1Activity)target;
    substitute.name = substitute.getIntent().getStringExtra("name");
    substitute.age = substitute.getIntent().getIntExtra("age", substitute.age);
    substitute.height = substitute.getIntent().getIntExtra("height", substitute.height);
    substitute.girl = substitute.getIntent().getBooleanExtra("boy", substitute.girl);
    substitute.ch = substitute.getIntent().getCharExtra("ch", substitute.ch);
    substitute.fl = substitute.getIntent().getFloatExtra("fl", substitute.fl);
    substitute.dou = substitute.getIntent().getDoubleExtra("dou", substitute.dou);
    substitute.pac = substitute.getIntent().getParcelableExtra("pac");
    if (null != serializationService) {
      substitute.obj = serializationService.parseObject(substitute.getIntent().getStringExtra("obj"), new com.alibaba.android.arouter.facade.model.TypeWrapper<TestObj>(){}.getType());
    } else {
      Log.e("ARouter::", "You want automatic inject the field 'obj' in class 'Test1Activity' , then you should implement 'SerializationService' to support object auto inject!");
    }
    if (null != serializationService) {
      substitute.objList = serializationService.parseObject(substitute.getIntent().getStringExtra("objList"), new com.alibaba.android.arouter.facade.model.TypeWrapper<List<TestObj>>(){}.getType());
    } else {
      Log.e("ARouter::", "You want automatic inject the field 'objList' in class 'Test1Activity' , then you should implement 'SerializationService' to support object auto inject!");
    }
    if (null != serializationService) {
      substitute.map = serializationService.parseObject(substitute.getIntent().getStringExtra("map"), new com.alibaba.android.arouter.facade.model.TypeWrapper<Map<String, List<TestObj>>>(){}.getType());
    } else {
      Log.e("ARouter::", "You want automatic inject the field 'map' in class 'Test1Activity' , then you should implement 'SerializationService' to support object auto inject!");
    }
    substitute.url = substitute.getIntent().getStringExtra("url");
    substitute.helloService = ARouter.getInstance().navigation(HelloService.class);
  }
}

对于这部分的代码,大部分比较清晰,总体逻辑是从Activity接收到的intent中提取内容并赋值给对应的属性。需要注意的是如下几点

  1. @Autowired修饰的属性不能为private
  2. @Autowired如果修饰的是自定义对象,那么需要有一个SerializationService服务实现
  3. @Autowired可以用来修饰服务,自动装配的时候会找到对应的服务实现赋值

总结

综合上面的内容,我们可以得出下面的结论

  1. ARouter 的自动注册机制一定是通过这些路由清单类来实现的
  2. 我们可以通过两种方式来找到定义的 PROVIDER 类型的路由节点
  3. 自动赋值功能的实现,一定是在页面被路由打开时调用了生成的帮助类(ISyringe接口的 inject(Object target) 方法)

初始化

LogisticsCenter.init

ARouter的初始化是通过ARouter.init方法来实现的,这个方法最终是通过LogisticsCenter.init来实现具体的逻辑的

    /**
     * LogisticsCenter init, load all metas in memory. Demand initialization
     */
    public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        mContext = context;
        executor = tpe;

        try {
            long startInit = System.currentTimeMillis();
            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 generate by arouter-compiler.
                // 扫描对应包名下(实际上就是routes包)的所有类
                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 finish.
            } 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();

            // 对所有包下的类,分情况进行加载,加载到Warehouse的不同的属性中
            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);
                }
            }

            logger.info(TAG, "Load root element finished, cost " + (System.currentTimeMillis() - startInit) + " ms.");

            if (Warehouse.groupsIndex.size() == 0) {
                logger.error(TAG, "No mapping files were found, check your configuration please!");
            }

            if (ARouter.debuggable()) {
                logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size()));
            }
        } catch (Exception e) {
            throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
        }
    }

根据这部分的逻辑,我们可以得出下面的结论

  1. loadInto这个方法,其实就是调用了刚刚我们分析的自动生成的那些类的loadInto方法,
  2. 初始化之后的路由表是保存在Warehouse这个类的一些成员变量里的。
  3. 初始化只加载了Root,Provider,Interceptor三个类的路由项

实际上对于第三点,正是之前说的,ARouter是分组懒加载的,所以初始化的时候并未做完全路由加载。

Warehouse

然后可以看看Warehouse里的内容

/**
 * Storage of route meta and other data.
 *
 * @author zhilong <a href="mailto:zhilong.lzl@alibaba-inc.com">Contact me.</a>
 * @version 1.0
 * @since 2017/2/23 下午1:39
 */
class Warehouse {
    // 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<>();

    static void clear() {
        routes.clear();
        groupsIndex.clear();
        providers.clear();
        providersIndex.clear();
        interceptors.clear();
        interceptorsIndex.clear();
    }
}

这里有6个成员变量,有三个是在刚刚的init过程中初始化的——groupsIndex、providersIndex、interceptorsIndex,另外三个分别描述如下

  1. routes——这个用于保存完整的路由表,是在路由表更新的方法中不断更新的,懒加载过程对应于LogisticsCenter.completion方法

  2. providers——这个用于缓存服务实现具体的类的对象,避免重复创建服务实现类的对象,懒加载过程对应于LogisticsCenter.completion方法

  3. interceptors——这个用于保存拦截器的优先级顺序,因为我们拦截器是按照优先级先后来处理的,因此必然需要一个列表来保存这个优先级,懒加载过程对应于InterceptorServiceImpl.init方法

LogisticsCenter.completion

刚刚提到了LogisticsCenter.completion这个方法,ARouter的初始化过程严格来说应该也包含了这个『完善路由』的方法

    /**
     * Completion the postcard by route metas
     *
     * @param postcard Incomplete postcard, should completion by this method.
     */
    public synchronized static void completion(Postcard postcard) {
        if (null == postcard) {
            throw new NoRouteFoundException(TAG + "No 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 be implememt 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;
            }
        }
    }

这里的PostCard保存的是路由过程需要的一些全部信息,从上面的代码里我们也可以看出来下面几点

  1. 对于不在Warehouse.routes路由表中的路由项,需要加载并放在Warehouse.routes中
  2. 对于已经在路由表中的项,会将相应信息放入PostCard中
  3. 如果路由项是Provider(服务)且不存在于服务路由表缓存中时,会实例化服务并放入缓存

InterceptorServiceImpl.init

InterceptorServiceImpl实际上也是一个服务的具体实现,用于管理所有的拦截器的初始化,它在ARouter初始化(LogisticsCenter.init)之后执行,具体看下InterceptorServiceImpl.init的实现代码

    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();
                    }
                }
            }
        });
    }

可以看到拦截器是异步加载的,而且是从interceptorsIndex中提取所有的一次性完全加载到Warehouse管理的内存队列中的。

综合上面所有我们可以大概知道ARouter的路由表初始化的整个过程。

发起路由

这里只分析byType和byName两种发起路由的方式,这两种方式最终会执行到_ARouter类内的两个方法

// byType
protected <T> T navigation(Class<? extends T> service) {
    // .............
}
// byName
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, NavigationCallback callback) {
    // .............
}

byType

byType内部实现如下

protected <T> T navigation(Class<? extends T> service) {
        try {
            Postcard postcard = LogisticsCenter.buildProvider(service.getName());

            // Compatible 1.0.5 compiler sdk.
            if (null == postcard) { // No service, or this service in old version.
                postcard = LogisticsCenter.buildProvider(service.getSimpleName());
            }

            LogisticsCenter.completion(postcard);
            return (T) postcard.getProvider();
        } catch (NoRouteFoundException ex) {
            logger.warning(Consts.TAG, ex.getMessage());
            return null;
        }
    }

第一个方法用于构造路由信息PostCard对象,可以看到是从路由表中根据服务接口名找到路由基本信息

    /**
     * Build postcard by serviceName
     *
     * @param serviceName interfaceName
     * @return postcard
     */
    public static Postcard buildProvider(String serviceName) {
        RouteMeta meta = Warehouse.providersIndex.get(serviceName);

        if (null == meta) {
            return null;
        } else {
            return new Postcard(meta.getPath(), meta.getGroup());
        }
    }

然后使用上面分析过的completion方法完善PostCard对象信息,因为byType只用于路由服务,因此最后将Provider实例对象返回

byName

byName内部实现如下

/**
     * Use router navigation.
     *
     * @param context     Activity or null.
     * @param postcard    Route metas
     * @param requestCode RequestCode
     * @param callback    cb
     */
    protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        try {
            LogisticsCenter.completion(postcard);
        } catch (NoRouteFoundException ex) {
            logger.warning(Consts.TAG, ex.getMessage());

            if (debuggable()) { // Show friendly tips for user.
                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;
    }

byName的方式,方法本身包含PostCard对象参数,该参数是根据路由项的Group和Path构造的,从上面的代码里看到主要做了几件事

  1. 调用completion方法完善路由表和postcard对象信息
  2. 回调告知路由状态
  3. 拦截器工作
  4. 执行路由跳转

我们看下最后执行路由跳转的方法_navigation(context, postcard, requestCode, callback);

private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        final Context currentContext = null == context ? mContext : context;

        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 (-1 != flags) {
                    intent.setFlags(flags);
                } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                // Navigation in main looper.
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        if (requestCode > 0) {  // Need start for result
                            ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
                        } else {
                            ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
                        }

                        if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) {    // Old version.
                            ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
                        }

                        if (null != callback) { // Navigation over.
                            callback.onArrival(postcard);
                        }
                    }
                });

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

上面的代码主要也是分路由类别做了不同的事情

  1. Activity——创建intent,塞入flag,extras等内容并执行界面跳转
  2. Provider——返回Provider实现对象,供byName调用者调用服务接口
  3. Fragment——返回Fragment实例

以上就是对ARouter实现的一个简单的分析,如果有兴趣的话可以参考源码阅读更多的内容

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