随着Android技术发展越来越成熟,随着热更新技术的火热,组件化和插件化的技术也得到了一定的关注。特别是对于比较大的项目,人员的增多,代码越来越臃肿,这时候就必须进行模块化的拆分。模块化在Android工程中目前有两种实现方式,一种是组件化,另一种是插件化,其实质都是降低耦合。作为Android开发者,有必要对这两种技术做一个了解,在项目需要重构时或在构建大项目初期时,选择合适的模块化方案,下面就对组件化技术学习做一个总结。目前比较流行的组件化方案是阿里的Atlas,得到app的AndroidComponent,插件化比较流行的方案是滴滴的virtual apk,360的replugin。
得到团队的AndroidComponent的学习成本较低,对于初学者方便入手,以下记录该框架的学习情况:
每个组件都可以看成一个单独的整体,可以按需的和其他组件(包括主项目)整合在一起,从而完成的形成一个app,整体app缺少任何一个组件都是可以正常运行的,并且每个组件可以单独运行。
件化和插件化的最大区别(应该也是唯一区别)就是组件化在运行时不具备动态添加和修改组件的功能,但是插件化是可以的。
依赖库与Component的区别
依赖库library
代码被其他组件直接引用。比如网络库module可以认为是一个library。
Component
这种module是一个完整的功能模块。比如分享module就是一个Component。
统一把library称之为依赖库,把Component称之为组件。组件化也主要是针对Component这种类型。主项目、主module或者Host负责拼装这些组件以形成一个完成app的module,统一称为主项目。
项目结构
app是主项目,负责集成众多组件,控制组件的生命周期
xxxComponent 是我们拆分的两个组件,比如shareComponent
componentservice 定义了所有的组件提供的服务
basicres 定义了全局通用的theme和color等公共资源
basiclib 公共的基础库,统一在这里引入,比如一些第三方的库okhttp
componentlib 组件化的基础库 Router/UIRouter等都定义在这里
build-gradle 组件化编译的gradle插件
单独调试和发布
- 在组件工程下的gradle.properties文件中设置一个isRunAlone的变量来区分不同的场景,build.gradle需要引入一个插件,插件中会判断apply com.android.library还是com.android.application
- 在src/main/runalone下面定义单独调试所必须的AndroidManifest.xml、application、入口activity等类
- 组件开发并测试完成时,需要发布一个release版本的aar文件到中央仓库,只需要把isRunAlone修改为false,执行module:assembleRelease命令。只有发布组件时才需要修改isRunAlone=false,即使后面将组件集成到app中,也不需要修改isRunAlone的值。所以在Androidstudio中,是可以看到三个application工程的,随便点击一个都是可以独立运行的,并且可以根据配置引入其他需要依赖的组件,这些工作都由插件来完成。
组件交互
组件间数据传输,通过接口+实现的方式,组件之间完全面向接口编程。
下面是share提供一个fragment给app使用的例子。
share组件在componentservice中定义自己的服务
1 | public interface ShareService { |
在自己的组件工程中,提供对应的实现类ShareServiceImpl:
1 | public class ShareServiceImpl implements ShareService { |
在ShareAppLike中在组件加载的时候把实现类注册到Router中,ShareAppLike相当于组件的application类需要实现IApplicationLike的接口,在IApplicationLike接口中定义onCreate和onStop两个生命周期方法,对应组件的加载和卸载。
1 | public class ShareAppLike implements IApplicationLike { |
在app中面向ShareService来编程
1 | Router router = Router.getInstance(); |
由于组件是动态加载和卸载的,在使用ShareService的需要进行判空处理。我们看到数据的传输是通过一个中央路由Router来实现的,这个Router就是一个HashMap
UI跳转
页面(activity)的跳转也是通过一个中央路由UIRouter来实现,这里增加了一个优先级的概念。
页面的跳转通过短链的方式,跳转到share的activity
1 | UIRouter.getInstance().openUri(getActivity(), "componentdemo://share", null); |
share组件在自己实现的ShareUIRouter中声明了自己处理这个短链(scheme和host)
1 | private static final String SCHEME = "componentdemo"; |
如果已经组件已经响应了这个短链,就返回true,这样更低优先级的组件就不会接收到这个短链。注解生成根据scheme和host跳转的逻辑,如ARouter的开源框架已实现。
集成调试
由app或者其他组件充当host的角色,引入其他相关的组件一起参与编译,从而测试整个交互流程。app和组件都可以充当host的角色。在这里我们以app为例。
- 在根项目的gradle.properties中增加一个变量mainmodulename,其值就是工程中的主项目,这里是app。设置为mainmodulename的module,isRunAlone永远是true。
- 在app项目的gradle.properties文件中增加两个变量:其中debugComponent是debug的时候引入的组件,compileComponent是release下引入的组件。
1
2debugComponent=com.mrzhang.share:sharecomponent
compileComponent=sharecomponent
debugComponent引入的组件写法是不同的,组件引入支持两种语法,module直接引用module工程,modulePackage:module,使用componentrelease中已经发布的aar。
在集成调试中,要引入的share组件是不需要把自己的isRunAlone修改为false的。一个application工程是不能直接引用(compile)另一个application工程的,所以app和组件都是isRunAlone=true在正常情况下是编译不过的。
插件会自动识别当前要调试的具体是哪个组件,然后把其他组件修改为library工程,而且这个修改只在当次编译生效。
通过task来判断判断当前要运行的是app还是哪个组件,判断的规则如下:
assembleRelease → app
app:assembleRelease或者 :app:assembleRelease → app
sharecomponent:assembleRelease 或者:sharecomponent:assembleRelease→ sharecomponent
这样每个组件可以直接在Androidstudio中run,也可以使用命令进行打包,不需要修改任何配置,却可以自动引入依赖的组件,在开发中可以提高工作效率。
代码边界
依赖的组件集成到host中,本质还是使用compile project(…)或者compile modulePackage:module@aar。不直接在build.gradle中直接引入是为了组件之间的完全隔离,可以称之为代码边界。为了避免直接引入实现类来编程,绕过了面向接口编程的约束。
从task入手,只有在assemble任务的时候才进行compile引入。这样在代码的开发期间,组件是完全不可见的。具体的代码如下:
1 | /** |
生命周期
集成调试,可以在打包的时候把依赖的组件参与编译,此时反编译apk的代码会看到各个组件的代码和资源都已经包含在包里面。但是由于每个组件的唯一入口ApplicationLike还没有执行oncreate()方法,所以组件并没有把自己的服务注册到中央路由,因此组件实际上是不可达的。
加载组件目前插件提供了两种方式,字节码插入和反射调用。
字节码插入模式
在dex生成之前,扫描所有的ApplicationLike类,它有一个共同的父类,然后通过javassist在主项目的Application.onCreate()中插入调用ApplicationLike.onCreate()的代码。这样就相当于每个组件在application启动的时候就加载起来了。
反射调用的方式
手动在Application.onCreate()中或者在其他合适的时机手动通过反射的方式来调用ApplicationLike.onCreate()。比起字节码插入,这种方式有两个好处,一是字节码插入对代码进行扫描和插入会增加编译的时间,在debug的时候会影响效率,并且这种模式对Instant Run支持不好;二是可以更灵活的控制加载或者卸载时机。
这两种模式的配置是通过配置插件的Extension来实现的,下面是字节码插入的模式下的配置格式,添加applicatonName的目的是加快定位Application的速度。
1 | combuild { |
组件化集成步骤
以官方的demo为例子:
- 将项目中的所有第三方库引用添加到basiclib的build.gradle
1
2compile 'com.squareup.okhttp3:okhttp:3.4.1'
compile 'com.squareup.picasso:picasso:2.5.2' - 将项目中的公共资源如颜色,字符串,风格 ,主要是values里面的东西迁移到basicres
- 导入gradle插件
ComExtension 是否自动注册管理
ComBuild gradle task 对组件进行打包
ComCodeTransform ConvertUtils 工具类 - componentlib 组件化的基础库 Router/UIRouter的实现
- componentservice 定义组件提供的服务接口声明,这里是阅读组件提供了一个fragment
1
2
3public interface ReadBookService {
Fragment getReadBookFragment();
} - readerComponent
ui路由的注册,实现IApplicationLike接口,此处使用的是默认的ui路由1
2
3
4
5
6
7
8
9
10
11
12
13
14public class ReaderAppLike implements IApplicationLike {
Router router = Router.getInstance();
public void onCreate() {
router.addService(ReadBookService.class.getSimpleName(), new ReadBookServiceImpl());
}
public void onStop() {
router.removeService(ReadBookService.class.getSimpleName());
}
}
实现提供的ReadBookService接口
1 | public class ReadBookServiceImpl implements ReadBookService { |
需要提供的fragment实现,点击后跳转到share的activity
1 | public class ReaderFragment extends Fragment { |
- shareComponent
ui路由的注册,实现IApplicationLike接口,使用的是自定义的ui路由自定义ui路由,实现IComponentRouter接口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class ShareApplike implements IApplicationLike {
UIRouter uiRouter = UIRouter.getInstance();
ShareUIRouter shareUIRouter = ShareUIRouter.getInstance();
public void onCreate() {
uiRouter.registerUI(shareUIRouter);
}
public void onStop() {
uiRouter.unregisterUI(shareUIRouter);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55public class ShareUIRouter implements IComponentRouter {
private static final String SCHME = "componentdemo";
private static final String SHAREHOST = "share";
private static String[] HOSTS = new String[]{SHAREHOST};
private static ShareUIRouter instance = new ShareUIRouter();
private ShareUIRouter() {
}
public static ShareUIRouter getInstance() {
return instance;
}
public boolean openUri(Context context, String url, Bundle bundle) {
if (TextUtils.isEmpty(url) || context == null) {
return true;
}
return openUri(context, Uri.parse(url), bundle);
}
public boolean openUri(Context context, Uri uri, Bundle bundle) {
if (uri == null || context == null) {
return true;
}
String host = uri.getHost();
if (SHAREHOST.equals(host)) {
Intent intent = new Intent(context, ShareActivity.class);
intent.putExtras(bundle == null ? new Bundle() : bundle);
context.startActivity(intent);
return true;
}
return false;
}
public boolean verifyUri(Uri uri) {
String scheme = uri.getScheme();
String host = uri.getHost();
if (SCHME.equals(scheme)) {
for (String str : HOSTS) {
if (str.equals(host)) {
return true;
}
}
}
return false;
}
}
activity的具体实现
1 | public class ShareActivity extends AppCompatActivity { |
最后在主app中调用:
使用shareComponent提供的fragment
1 | Router router = Router.getInstance(); |
注册activity
1 | Router.registerComponent("com.mrzhang.share.applike.ShareApplike"); |
Router实现解析
不管是哪个组件化方案,路由技术都是必不可少的,得到团队的这个方案路由未采用第三方,且实现了动态加载和卸载,确实不错。
- 定义组件ui路由接口,组件自定义ui路由需实现这个接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public interface IComponentRouter {
/**
* 打开一个链接
*
* @param url
* 目标url可以是http 或者 自定义scheme
* @param bundle
* 打开目录activity时要传入的参数。建议只传基本类型参数。
* @return 是否正常打开
*/
public boolean openUri(Context context, String url, Bundle bundle);
public boolean openUri(Context context,Uri uri, Bundle bundle);
public boolean verifyUri(Uri uri);
} - 实现一个统一管理ui路由的接口,继承IComponentRouter
设置优先级和提供注册方法1
2
3
4
5
6
7
8
9
10
11
12public interface IUIRouter extends IComponentRouter {
int PRIORITY_NORMAL = 0;
int PRIORITY_LOW = -1000;
int PRIORITY_HEIGHT = 1000;
void registerUI(IComponentRouter router, int priority);
void registerUI(IComponentRouter router);
void unregisterUI(IComponentRouter router);
} - 实现一个统一管理的ui路由
定义了一个路由列表和优先级map提供单例对象的接口:1
2List<IComponentRouter> uiRouters = new ArrayList<IComponentRouter>();
HashMap<IComponentRouter, Integer> priorities = new HashMap<IComponentRouter, Integer>();注册一个ui路由并设置优先级,如果当前列表存在这个路由且优先级相同则返回,移除已存在的路由对象,遍历路由列表,当优先级小于当前路由时,退出遍历并插入列表,同时插入优先级到优先级map1
2
3
4
5
6
7
8
9
10
11
12private UIRouter() {
}
public static UIRouter getInstance() {
if (instance == null) {
synchronized (UIRouter.class) {
if (instance == null) {
instance = new UIRouter();
}
}
}
return instance;
}反注册执行移除操作1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void registerUI(IComponentRouter router, int priority) {
if (priorities.containsKey(router) && priority == priorities.get(router)) {
return;
}
removeOldUIRouter(router);
int i = 0;
for (IComponentRouter temp : uiRouters) {
Integer tp = priorities.get(temp);
if (tp == null || tp <= priority) {
break;
}
i++;
}
uiRouters.add(i, router);
priorities.put(router, Integer.valueOf(priority));
}统一对ui理由的url进行处理,没有特殊前缀加上http头返回1
2
3
4
5
6
7
8
9
10
public void unregisterUI(IComponentRouter router) {
for (int i = 0; i < uiRouters.size(); i++) {
if (router == uiRouters.get(i)) {
uiRouters.remove(i);
priorities.remove(router);
break;
}
}
}遍历路由列表,调用当前ui路由的检查方法和跳转方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean openUri(Context context, String url, Bundle bundle) {
url = url.trim();
if (!TextUtils.isEmpty(url)) {
if (!url.contains("://") &&
(!url.startsWith("tel:") ||
!url.startsWith("smsto:") ||
!url.startsWith("file:"))) {
url = "http://" + url;
}
Uri uri = Uri.parse(url);
return openUri(context, uri, bundle);
}
return true;
}1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean openUri(Context context, Uri uri, Bundle bundle) {
for (IComponentRouter temp : uiRouters) {
try {
if (temp.verifyUri(uri) && temp.openUri(context, uri, bundle)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
} - 路由实现类Router,负责组件的服务管理和组件注册,同时调用applicationLike的onCreate和onStop,相当于Application的初始化与销毁的生命周期
组件的服务map和注册组件map提供单例对象的接口:1
2
3private HashMap<String, Object> services = new HashMap<>();
//注册组件的集合
private static HashMap<String, IApplicationLike> components = new HashMap<>();组件服务的添加移除和获取1
2
3
4
5
6
7
8
9
10
11
12
13private Router() {
}
public static Router getInstance() {
if (instance == null) {
synchronized (Router.class) {
if (instance == null) {
instance = new Router();
}
}
}
return instance;
}组件的注册与反注册,调用此处可实现动态的加载和卸载组件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public synchronized void addService(String serviceName, Object serviceImpl) {
if (serviceName == null || serviceImpl == null) {
return;
}
services.put(serviceName, serviceImpl);
}
public synchronized Object getService(String serviceName) {
if (serviceName == null) {
return null;
}
return services.get(serviceName);
}
public synchronized void removeService(String serviceName) {
if (serviceName == null) {
return;
}
services.remove(serviceName);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45/**
* 注册组件
*
* @param classname 组件名
*/
public static void registerComponent(@Nullable String classname) {
if (TextUtils.isEmpty(classname)) {
return;
}
if (components.keySet().contains(classname)) {
return;
}
try {
Class clazz = Class.forName(classname);
IApplicationLike applicationLike = (IApplicationLike) clazz.newInstance();
applicationLike.onCreate();
components.put(classname, applicationLike);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 反注册组件
*
* @param classname 组件名
*/
public static void unregisterComponent(@Nullable String classname) {
if (TextUtils.isEmpty(classname)) {
return;
}
if (components.keySet().contains(classname)) {
components.get(classname).onStop();
components.remove(classname);
return;
}
try {
Class clazz = Class.forName(classname);
IApplicationLike applicationLike = (IApplicationLike) clazz.newInstance();
applicationLike.onStop();
components.remove(classname);
} catch (Exception e) {
e.printStackTrace();
}
}