背景
三国演义里开篇就说:天下大势,分久必合,合久必分。这话套在软件开发上,也特别贴切。我记得我刚入门时做Android应用程序开发,刚开始都是采用中心化管理的思想,将相同的资源集中进行管理,但是做着做着,发现集中管理的资源太多了,多人开发时牵一发而动全身,进而又要对原本的项目进行拆分,演变出模块化开发,以及我这里要讲的Android组件化实践。
现状
一种是项目组件化开发,一种是单一工程开发模式。
1.对工程的任意修改调试都要编译整个工程,效率十分低下;
2.不利于多人团队协同开发;
3.无法做到功能复用
4.业务模块间耦合严重
在项目的开发过程中,随着开发人员的增多及功能的增加,如果提前没有使用合理的开发架构,那么代码会越来臃肿,功能间代码耦合也会越来越严重,这时候为了保证项目代码的质量,我们就必须进行重构。
比较简单的开发架构是按照功能模块进行拆分,也就是用 Android 开发中的 module 这个概念,每个功能都是一个 module
上面说到了从普通的无架构到模块化,再由模块化到组件化,那么其中的界限是什么,模块化和组件化的本质区别又是什么?为了解决这些问题,我们就要先了解 “模块” 和 “组件” 的区别。
组件化和模块化的区别
- 模块
模块指的是独立的业务模块,比如刚才提到的 [首页模块]、[圈子模块]、[直播间模块] 等 - 组件
组件指的是单一的功能组件,如 [视频组件]、[支付组件]、[登陆组件] 等,每个组件都可以以一个单独的 module 开发,并且可以单独抽出来作为 SDK 对外发布使用。
最明显的区别就是模块相对与组件来说粒度更大,一个模块中可能包含多个组件。并且两种方式的本质思想是一样的,都是为了代码重用和业务解耦。在划分的时候,模块化是业务导向,组件化是功能导向。
问题: 当多个模块中涉及到相同功能时代码的耦合又会增加。
例子:首页模块和论坛圈子间模块中都可能涉及到了视频播放的功能,
这时候不管将播放控制的代码放到首页还是直播间,开发过程中都会发现,我们想要解决的代码耦合情况又又出现了
理想结构
要解决的问题
- 每个组件都是一个完整的整体,所以组件开发过程中要满足单独运行及调试的要求,这样还可以提升开发过程中项目的编译速度。
- 组件间的数据传递与方法调用
- 组件间界面跳转,在组件化开发过程中如何在不相互依赖的情况下实现互相跳转?
组件单独调试-两种方式
- 单独工程运行
- 动态配置组件的工程类型
组件单独运行
- 组件的工程类型
Android Gradle 中提供了三种插件,在开发中可以通过配置不同的插件来配置不同的工程
- App 插件,id: com.android.application
- Library 插件,id: com.android.libraay
- Test 插件,id: com.android.test
在 module 中添加一个 gradle.properties 配置文件,在配置文件中添加一个布尔类型的变量 isRunAlone,在 build.gradle 中通过 isRunAlone 的值来使用不同的插件从而配置不同的工程类型,在单独调试和集成调试时直接修改 isRunAlone 的值即可
组件间页面跳转
说到组件间通信,目前算是已经有了很成熟的方案,ARouter(阿里)、WMRouter(美团)等
工作原理可以分为三个部分:
- 编译期通过注解处理器生成相关的中间类;中间类保存路由Url和具体实现的类的路径关系, build/generated/ap_generated_source/out
- 程序启动时初始化路由表;
- 路由调用的时候通过路由表找到实现类路径,使用startActivity实现页面跳转
技术点
- 编译期通过注解APT
- 动态注解和静态注解:
- 编译时获取注解信息,并据此产生java代码文件,无性能损失
- Java Poet生成源代码
a. 可以自动生成Java文件的第三方依赖
b. 简洁易懂的API,上手快
组件间的数据传递与方法调用
- 暴露服务
// 声明接口,其他组件通过接口来调用服务, 接口要放在commom底层
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) {
}
}
- 发现服务
// 2. 使用依赖查找的方式发现服务,主动去发现服务并使用,下面两种方式分别是byName和byType
helloService3 = ARouter.getInstance().navigation(HelloService.class);
helloService3.sayHello("Vergil");
3. 问题
上面的处理可以看到 接口类HelloService必须放在底层。但是这样会导致不好扩展。有变化都需要修改底层接口。
4. 解决
底层提供一个通用的CommonService,来分发方法调用
然后根据传入的Method 找到对应的具体实现
基类
public interface BaseCommonService extends IProvider {
/**
* 分发方法调用
*
* @param method 方法path
* @param params 方法所需的参数
* @param args 额外参数
* @return
*/
Response call(String method, Bundle params, Object... args);
}
具体实现类
@Route(path = BBRouterKeys.BB_COMMON_METHOD_PATH)
public class CommonRouterService implements BaseCommonService {
private static final String TAG = "CommonRouterService";
private Context mContext;
@Override
public void init(Context context) {
mContext = context;
}
@Override
public Response call(String method, Bundle bundle, Object... objects) {
if (BBRouterKeys.BB_COMMON_INVITE_TEAM_MEMBER.equals(method)) {
ArrayList<String> members = bundle.getStringArrayList(BBRouterKeys.BB_ENCRYPT_KEY_IS_SECRET);
return Response.generateSuccess(inviteTeamMember(members));
} else if (BBRouterKeys.BB_COMMON_KEY_NIM_GET_NIM_TOKEN.equals(method)) {
String uid = bundle.getString(BBRouterKeys.BB_COMMON_KEY_NIM_UID);
return getUserNimToken(uid);
} else if (BBRouterKeys.BB_CHAT_UPDATE_NIM_TOKEN.equals(method)) {
return updateNimToken();
}
return Response.generateFail();
}
服务获取
/**
* 从路由路径path对服务的方法中获取数据
*
* @param path
* @param requestBundle
* @param args
* @return
*/
public static Bundle getResponseBundle(String path, Bundle requestBundle, Object... args) {
Response response = BAFRouter.call(path, requestBundle, args);
if (response != null && response.result != null && Response.SUCCESS.equals(response.status)) {
return response.result;
} else {
return null;
}
}
仍然存在的问题
使用commonService改造后,是可以实现发现服务,但是返回值被封装到Response的Bundle里面,还要解析去拿,而且不太友好,已经不是面向接口编程
面向接口编程-理想的情况
- 模块A,定义接口
@JInterface("/service")
public interface TestInterface {
//提供的方法
public void test(String msg);
}
- 模块A调用接口
Router.getInstance(). create(TestInterface.class) .test("Implement By Module B");
- 模块B提供服务
@JImplement("/service")
public class TestImplement {
public void test(String msg) {
//方法的具体实现
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
}
实现原理
跟上面路由查找的一样的,还是使用APT来生成中间代码,然后根据路由表,最有对应的实现类,反射出实例;在调用具体的方法
多进程架构模块通信
- 解决方案是单独提取一个Wide Router模块,所有的Local Route与Wide Router通过进程间通信的方式建立连接
- 请求如果在Local Router中找不到时,则通过WideRouter与其它进程建立连接,WideRouter充当局域网之间的路由。
- 类似: 局域网- 广域网的处理
总结
- 提高工程编译速度
- 业务模块解耦,有利于多人团队协作开发
- 组件化是功能重用的基石