Android 组件化之初探
这是一个APP臃肿的时代!所以为了告别APP臃肿的时代,让我们进入一个U盘时代,每个业务模块都是一个具备独立运行的U盘,插在哪里都可以完美运行,这就是推进业务组件化的初衷也是一个美好的愿景。
目前大部分app的单一项目结构原型。大致如下图所示:
app1.png
一眼望去这结构不是挺清晰的么?每个业务都放在单独的包名下,网络库、图片加载库等第三方库与上层业务都完美的剥离了,我们再来看下他们的直接的依赖关系图:
app2.png虽然上面的依赖关系举例有点太过于极端,但是真实场景中是存在的。各个业务之间代码互相引用,所以这种结构也是架构整改的根本动机,也是当务之急应该考虑的事情。为了更好的满足各个业务的迭代而彼此不受影响,更好的解决上面这种让人头疼的依赖关系,开始着手app架构整改。
从上面的分析我们可以得出适合业务组件化的几种情况:
- 业务较多、而且复杂
- 业务之间的依赖需要解耦
- 团队成员较多,需要各自开发相对独立的业务
业务组件化方向:
APP业务组件化架构整改的方向就是告别结构臃肿的时代,让各个业务变得相对独立。模块工程和类库工程之间遵循向下依赖关系,各个模块之间的遵循平级依赖关系。先看下整改后的各个独立业务模块与类库工程之间的向下依赖关系图:
app3.png整改的方向由一个项目工程拆分成若干个模块工程,由app壳工程提供统一的入口,每个业务独立的模块module共享项目的依赖库。由壳工程集成需要引入的业务模块,至于各个独立的业务模块之间的调用依赖关系,我们借助一个中间层充当路由功能,这个路由我们放在各个业务模块共同引用的依赖库那一层。由路由统一调度他们之间的依赖关系,路由调度解决平级依赖问题示意图:
app4.png通过APP壳工程提供的路由功能,各个模块之间调用不再采用传统的显式调用,而是采用隐式调用的方式。从而使各个模块之间不再存在依赖关系。
组件化的实现
- 子模块单独编译
- sdk和第三方库的版本一致性
- 资源重复定义
- 模块之间页面跳转
- 模块之间数据传递
- 模块初始化处理
APP业务组件化架构整改带来的好处:
- 加快迭代速度,各个业务模块组件更加独立,不再因为业务耦合情况,在发版时候,由于互相等待而迟迟不能发布版本。
- 稳定的公共模块采用依赖库方式,提供给各个业务线使用,减少重复开发和维护工作量。
- 迭代频繁的业务模块采用组件方式,各业务线研发可以互不干扰、提升协作效率,并控制产品质量。
- 为新业务随时集成提供了基础,所有业务可上可下,灵活多变。
- 降低团队成员熟悉项目的成本,降低项目的维护难度。
- 加快编译速度,提高开发效率
1、子模块如何单独编译
我们希望在开发模式下,能够单独调试自己的模块,编译成独立的apk。而在主程序发布时,成为一个library
嵌入主工程。
首先在子模块build.gradle
中定义常量,来标示模块目前是否处于开发模式def isDebug = true
在子模块的build.gradle
中进行模式配置。debug
模式下编译成独立app,release
模式下编译成library。
if (isDebug.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
两种模式下模块AndroidManifest.xml
文件是有差别的。作为独立运行的app,有自己的Application
,要加Launcher
的入口intent
,作为library
不需要。这个问题很好解决,写两个不同的AndroidManifest.xml
即可,并在gradle中进行配置。
sourceSets {
main {
if (isDebug.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
2、sdk和第三方库的版本一致性
不同module
依赖sdk版本不一致,会因兼容性问题导致编译问题。
不同module
引用了同一个第三方库的不同版本,并且这个库没有做到向前兼容,就有可能出现方法找不到、参数不对应等问题。
所以有必要统一整个project
的依赖版本。
在最外层build.gradle
中定义的常量能被整个project
的build.gradle
文件引用,统一的版本定义可以放在这里。
ext {
android_compileSdkVersion = 23
android_buildToolsVersion = '23.0.3'
android_minSdkVersion = 15
android_targetSdkVersion = 23
lib_appcompat = 'com.android.support:appcompat-v7:23.2.1'
lib_gson = 'com.google.code.gson:gson:2.6.1'
lib_butterknife = 'com.jakewharton:butterknife:8.4.0'
lib_butterknife_compiler = 'com.jakewharton:butterknife-compiler:8.4.0'
}
3、资源的重复定义
说到资源的重复定义,笔者趟过坑,如果主工程和子模块中重复定义了同名的资源。
主工程中
<string name="daddy">爸爸</string>
子工程中
<string name="daddy">干爹</string>
虽然编译不会出错,但是最后子模块中用到daddy的地方都会显示爸爸。
编译时子模块的资源会和主工程合并到同一个类中,所以资源重名会有问题。
但是资源也要模块化呀,总不能在底层找个统一的地方都扔在里面,gradle提供了一个解决方案来避免重复定义的问题。
resourcePrefix "a_"
强制模块中的资源名称带有a_
前缀,否则编译不过。
聊到这里,我们知道了如何使用gradle独立编译子模块,以及如何处理分模块导致的一些问题。但是除了主工程统一调度外,模块与模块之间也需要互相调起和访问,所以需要协议去统一,这个协议是模块间共同定义与使用的,所以写在底层。
4、模块之间页面跳转
首先想到的就是配置uri
去匹配模块AndroidManifest.xml
中的intentFilter
来启动相应Activity,这种方式是解耦的,但有缺点,要跳转其它模块,得先去看别的模块的AndroidManifest.xml
进行入口适配,还得研究具体Activity中的传参设置,虽然代码依赖上解耦了,但是实现逻辑上没有解耦,忍不了。需要在底层创建一个路由协议,让使用者通过协议方便地调用。
用注解把需要的参数写在路由协议的接口中。
public interface IRouterUri {
@RouterUri("test://host_liujc")
public Intent getIntentActivityA(@RounterParam("name") String name, @RounterParam("age") int age);
@RouterUri("test://host_b")
public Intent getIntentActivityB();
}
其中@RouterUri
表示跳转改页面需要匹配的uri,这个uri最终会拿去和moduleA中的AndroidManifest.xml
中对应activity的intentFilter去匹配。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RouterUri {
String value() default "";
}
@RounterParam用来表示目标activity需要的参数,最终会在目标activity中进行解析。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RounterParam {
String value() default "";
}
为什么用注解的方式写接口而不是直接定义跳转方法呢?
用注解的方式,可以把参数更直观地展现在最醒目的方法声明中。而写成实现的方法,参数会被写在方法内部,定义起来不方便,而且要带上少量逻辑,不够简洁。参考retrofit
框架,也是用注解方式去实现,简洁、方便。
为什么接口返回的是Intent,而不是直接进行页面跳转呢?
因为我们的项目中,实现这个跳转可能是activity
,可能是fragment
,也可能startActivityForResult
需要带入一个自定义的requestCode
。所以为了灵活性,直接返回Intent
。
写好了接口,还需要将接口中的参数组装成一个可进行跳转的Intent
。使用Proxy
生成类动态代理这个接口。
public class RounterBus {
//静态map存储代理接口的实例
private static HashMap<Class, Object> sRounterMap = new HashMap<Class, Object>();
/**
* 得到动态代理路由接口的实例
*
* @param c 接口类
* @return
*/
public static IRouterUri getRounter(Class<?> c) {
IRouterUri rounter = (IRouterUri) sRounterMap.get(c);
if (rounter == null) {
rounter = (IRouterUri) Proxy.newProxyInstance(c.getClassLoader(), new Class[] { c }, new InvocationHandler() {
@Override public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//从方法注解的获取uri
RouterUri routerUri = method.getAnnotation(RouterUri.class);
if (routerUri == null || TextUtils.isEmpty(routerUri.value())) {
throw new IllegalArgumentException(
"invoke a rounter method, bug not assign a rounterUri");
}
Uri.Builder uriBuilder = Uri.parse(routerUri.value()).buildUpon();
//从参数值和参数注解,获取信息,拼入uri的query
Annotation[][] annotations = method.getParameterAnnotations();
if (annotations != null && annotations.length > 0) {
for (int i = 0, n = annotations.length; i < n; i++) {
Annotation[] typeAnnotation = annotations[i];
if (typeAnnotation == null || typeAnnotation.length == 0) {
throw new IllegalArgumentException("method " + method.getName() + ", args at " + i + " lack of annotion RouterUri");
}
boolean findAnnotaion = false;
for (Annotation a : typeAnnotation) {
if (a != null && (a.annotationType() == RounterParam.class)) {
uriBuilder.appendQueryParameter(((RounterParam) a).value(), GsonInstance.getInstance().toJson(args[i]));
findAnnotaion = true;
break;
}
}
if (!findAnnotaion) {
throw new IllegalArgumentException("method " + method.getName() + " args at " + i + ", lack of annotion RouterUri");
}
}
}
return getIntentByRouterUri(uriBuilder.build());
}
});
sRounterMap.put(c, rounter);
}
return rounter;
}
private static Intent getIntentByRouterUri(Uri uriBuilder) {
Context context = AppContext.get();
PackageManager pm = context.getPackageManager();
Uri uri = uriBuilder;
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
//查询这个intent是否能被接收用来进行跳转
List<ResolveInfo> activities = pm.queryIntentActivities(intent, 0);
if (activities != null && !activities.isEmpty()) {
return intent;
} else {
if (BuildConfig.IS_DEBUG) {
Toast.makeText(context, "子模块作为独立程序启动时,跳不到其他模块", Toast.LENGTH_SHORT).show();
} else {
throw new IllegalArgumentException("can't resolve uri with " + uri.toString());
}
}
return null;
}
}
上面代码包装了一个路由总线,来获取并缓存路由接口的实例。
例如:需要调起moduleA中的ActivityA
Intent intent = RounterBus.getRounter(IRouterUri.class).getIntentActivityA("heihei", 23);
if (intent != null) {
MainActivity.this.startActivity(intent);
}
5、模块之间的数据传递
ri uri = getIntent().getData();
if (uri != null) {
// 完整的url信息
String url = uri.toString();
Log.e(TAG, "url: " + uri);
// scheme部分
String scheme = uri.getScheme();
Log.e(TAG, "scheme: " + scheme);
// host部分
String host = uri.getHost();
Log.e(TAG, "host: " + host);
// //port部分
// int port = uri.getPort();
// Log.e(TAG, "host: " + port);
// // 访问路径
// String path = uri.getPath();
// Log.e(TAG, "path: " + path);
List<String> pathSegments = uri.getPathSegments();
// Query部分
String query = uri.getQuery();
Log.e(TAG, "query: " + query);
//获取指定参数值
String name= uri.getQueryParameter("name");
Log.e(TAG, "name: " + goodsId);
6、application初始化
子模块作为application
时,有一些初始化的工作需要在Application.onCreate
时进行。而作为library时,调不到这个onCreate。所以自己写一个静态方法,供主工程的Application调用。
public class ApplicationA extends BaseChildApplication {
@Override public void onCreate() {
super.onCreate();
//给底层library设置context
AppContext.init(getApplicationContext());
}
/**
* 作为library时需要初始化的内容
*/
@Override public void onCreateAsLibrary(Application application) {
super.onCreateAsLibrary(application);
}
}
主工程的Application.onCreate时记得初始化子模块。
public class MainApplication extends Application {
@Override public void onCreate() {
super.onCreate();
AppContext.init(getApplicationContext());
ApplicationA.onCreateAsLibrary();
ApplicationB.onCreateAsLibrary();
}
}
想调试A模块,but某些功能需要依赖B这时只需要把B模块作为library引入A。并且记得在B模块Application.onCreate时初始化一下A模块。是不是很轻量级?常用的话在gradle中设置一个开关就更方便了。
def isDebugWithB = true
if (isDebugWithB.toBoolean()) {
compile project(':moduleB')
}
ApplicationB.onCreateAsLibrary();