组件化是什么?
就是将一个app分成多个Module,每个Module都是一个组件(也可以是一个基础库供组件依赖),开发的过程中我们可以单独调试部分组件,组件间不需要互相依赖,但可以相互调用,最终发布的时候所有组件以lib的形式被主app工程依赖并打包成1个apk。
组件化的优点:
加快编译速度:每个业务功能都是一个单独的工程,可独立编译运行,拆分后代码量较少,编译自然变快。
提高协作效率:解耦使得组件之间此互不打扰,实现代码隔离;团队中每个人负责自己的组件,降低团队成员熟悉项目的成本,只需熟悉责任组件即可;对测试来说,只需重点测试改动的组件,而不是全盘回归测试。
功能复用:组件类似我们引用的第三方库,只需维护好每个组件,一建引用集成即可。
除此之外,还能实现一些组件间的AOP:比如借助拦截器,可以动态给组件调用添加不同的中间处理逻辑(登录态,埋点等)
组件化方案:
总的来讲可以分为两类:
基于 Uri 设计的路由(代表性的有阿里的ARoute,美团的WMRoute)
工作原理类似于仓库管理员:大家先把类全部放到仓库中,有人需要的时候,仓库管理员就根据所提供的字符串找出存放在仓库中的类。
基于总线方案(CC)
工作原理类似于电话接线员(中介者模式),调用组件时,总线根据字符串找到对应的组件类并将调用信息转发给该组件类,组件执行完成后再通过组件总线将结果返回给调用方。就像调用后台接口一样
优缺点:
路由的优点:配置简单,侵入性低,使用者多,多端兼容性好(指的是对URI的兼容)。
路由的缺点:接口下沉,默认生成映射表的性能稍低(通过注解+扫描所有dex文件+反射)推荐使用插件的方式(Transform API+ASM)来加快初始化的速度。
CC的优点:生成映射表的效率高(Transform API+ASM),跨进程,改造旧工程较容易。
CC的缺点:配置复杂(好在文档很详细),侵入性高,多端兼容性一般(指的是对URI的兼容),不更新了。
方案选择
为了更好地扩展、兼容,决定采用路由方案。
组件化框架图
具体实现
要解决的问题如下:
业务组件如何实现单独运行调试?
业务组件间如何实现页面的跳转?
业务组件间如何实现组件间通信/方法调用?
业务组件间如何获取fragment实例?
业务组件如何获取Application实例、如何获取Application onCreate()回调(用于任务初始化)?
业务组件间资源命名冲突问题?
业务组件单独运行调试
众所周知的两种gradle插件:
Application 插件,id: com.android.application
Library 插件,id: com.android.library
第一个用于生成apk,第二个用于生成aar。
那么我们可以在gradle.properties中自定义一个参数来控制在单独调试的时候用application插件,在集成的时候用library插件
//gradle.properties
#组件独立调试开关, 每次更改值后要同步工程
isModule = false
//build.gradle
//注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换
if (isModule.toBoolean()){
apply plugin: 'com.android.application'
}else {
apply plugin: 'com.android.library'
}
作为一个APP肯定少不了APPID以及启动activity,我们一样可以通过isModule来控制:
//build.gradle (module_A)
android {
...
defaultConfig {
...
if (isModule.toBoolean()) {
// 独立调试时添加 applicationId ,集成调试时移除
applicationId "com.codemao.grow.A"
}
...
}
sourceSets {
main {
// 独立调试与集成调试时使用不同的 AndroidManifest.xml 文件
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
...
}
其中debug/AndroidManifest.xml大概长这样:
//moduleManifest/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.codemao.grow.A" >
<application android:name=".ModuleApplication"
android:allowBackup="true"
android:label="A"
android:theme="@style/Theme.AppCompat">
<activity android:name=".ModuleActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
AndroidManifest.xml大概是这样的:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.codemao.grow.A">
<application>
<activity android:name=".ModuleActivity"></activity>
</application>
</manifest>
业务组件间的页面跳转
通过路由(跳转路由)来实现,这里以Aroute来举例:
// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
...
}
ARouter.getInstance().build("/test/activity").navigation();
更详细的使用快戳我
业务组件间的通信
一样是通过路由(服务路由)来实现:
// 声明接口,其他组件通过接口来调用服务
public interface HelloService extends IProvider {
String sayHello(String name);
}
// 实现接口
@Route(path = "/yourservicegroupname/hello", name = "测试服务")
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name) {
return "hello, " + name;
}
@Override
public void init(Context context) {
}
}
HelloService helloService = ARouter.getInstance().navigation(HelloService.class);
或者
HelloService helloService=(HelloService)ARouter.getInstance().build("/yourservicegroupname/hello").navigation();
更详细的使用快戳我
业务组件间获取Fragment
还是通过路由
// 获取Fragment
Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();
更详细的使用快戳我
业务组件获取Application实例并感应其生命周期
通常我们会在Application的onCreate中做一些初始化工作,我们可以将所有初始化都放在APP壳工程里进行,可我们希望是壳工程和组件间实现代码隔离,由组件各自实现自己的初始化工作。
这里我们有三种种方法:
方法一:通过服务路由
例如:
@Route(path = "/MainModule/MainModule", name = "模块初始化")
public class MainModule implements IProvider {
private static final String TAG = "MainModule";
@Override
public void init(Context context) {
Log.e(TAG, "MainModule调用了初始化" );
}
}
方法二:通过注册一个ContentProvider
以Lifecycle为例:
<provider
android:name="android.arch.lifecycle.LifecycleRuntimeTrojanProvider"
android:authorities="xx.xx.xxxx.lifecycle-trojan"
android:exported="false"
android:multiprocess="true" />
public class LifecycleRuntimeTrojanProvider extends ContentProvider {
@Override
public boolean onCreate() {
LifecycleDispatcher.init(getContext());
ProcessLifecycleOwner.init(getContext());
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, String[] strings, String s, String[] strings1,
String s1) {
return null;
}
...
//return null or 0;
}
我们都知道ContentProvider的onCreate的调用时机介于Application的attachBaseContext和onCreate之间,Provider的onCreate优先于Application的onCreate执行,并且此时的Application已经创建成功,而Provider里的context正是Application的对象
方法三:通过AppLifecycle插件
使用详情戳链接
业务组件间资源命名冲突
通过在gradle中设置:
android {
resourcePrefix "moduleA_"
}
注意:resourcePrefix 只能限定Xml资源,并不能限定图片资源,图片资源仍需要自身注意设置前缀名。
最后
为了不使用接口下沉来暴露组件提供的服务,我们为每个组件创建一个公开的export组件,供其它组件调用。
就像:
这样做会使组件更加内聚,而不是将所有组件的服务接口都下沉到同一个export组件中。
组件化后的工程结构大概是这样: