Android模块化探索与实践

前言

万维网发明人 Tim Berners-Lee 谈到设计原理时说过:“简单性和模块化是软件工程的基石;分布式和容错性是互联网的生命。” 由此可见模块化之于软件工程领域的重要性。

从 2016 年开始,模块化在 Android 社区越来越多的被提及。随着移动平台的不断发展,移动平台上的软件慢慢走向复杂化,体积也变得臃肿庞大;为了降低大型软件复杂性和耦合度,同时也为了适应模块重用、多团队并行开发测试等等需求,模块化在 Android 平台上变得势在必行。阿里 Android 团队在年初开源了他们的容器化框架 Atlas 就很大程度说明了当前 Android 平台开发大型商业项目所面临的问题。

什么是模块化

那么什么是模块化呢?《 Java 应用架构设计:模块化模式与 OSGi 》一书中对它的定义是:模块化是一种处理复杂系统分解为更好的可管理模块的方式。

上面这种描述太过生涩难懂,不够直观。下面这种类比的方式则可能加容易理解。

我们可以把软件看做是一辆汽车,开发一款软件的过程就是生产一辆汽车的过程。一辆汽车由车架、发动机、变数箱、车轮等一系列模块组成;同样,一款大型商业软件也是由各个不同的模块组成的。

汽车的这些模块是由不同的工厂生产的,一辆 BMW 的发动机可能是由位于德国的工厂生产的,它的自动变数箱可能是 Jatco(世界三大变速箱厂商之一)位于日本的工厂生产的,车轮可能是中国的工厂生产的,***交给华晨宝马的工厂统一组装成一辆完整的汽车。这就类似于我们在软件工程领域里说的多团队并行开发,***将各个团队开发的模块统一打包成我们可使用的 App 。

一款发动机、一款变数箱都不可能只应用于一个车型,比如同一款 Jatco 的 6AT 自动变速箱既可能被安装在 BMW 的车型上,也可能被安装在 Mazda 的车型上。这就如同软件开发领域里的模块重用。

到了冬天,特别是在北方我们可能需要开着车走雪路,为了安全起见往往我们会将汽车的公路胎升级为雪地胎;轮胎可以很轻易的更换,这就是我们在软件开发领域谈到的低耦合。一个模块的升级替换不会影响到其它模块,也不会受其它模块的限制;同时这也类似于我们在软件开发领域提到的可插拔。

模块化分层设计

上面的类比很清晰的说明的模块化带来的好处:

  • 多团队并行开发测试;
  • 模块间解耦、重用;
  • 可单独编译打包某一模块,提升开发效率。

在《安居客 Android 项目架构演进》这篇文章中,我介绍了安居客 Android 端的模块化设计方案,这里我还是拿它来举例。但首先要对本文中的组件和模块做个区别定义

  • 组件:指的是单一的功能组件,如地图组件(MapSDK)、支付组件(AnjukePay)、路由组件(Router)等等;
  • 模块:指的是独立的业务模块,如新房模块(NewHouseModule)、二手房模块(SecondHouseModule)、即时通讯模块(InstantMessagingModule)等等;模块相对于组件来说粒度更大。

具体设计方案如下图:

整个项目分为三层,从下至上分别是:

  • Basic Component Layer: 基础组件层,顾名思义就是一些基础组件,包含了各种开源库以及和业务无关的各种自研工具库;
  • Business Component Layer: 业务组件层,这一层的所有组件都是业务相关的,例如上图中的支付组件 AnjukePay、数据模拟组件 DataSimulator 等等;
  • Business Module Layer: 业务 Module 层,在 Android Studio 中每块业务对应一个单独的 Module。例如安居客用户 App 我们就可以拆分成新房 Module、二手房 Module、IM Module 等等,每个单独的 Business Module 都必须准遵守我们自己的 MVP 架构。

我们在谈模块化的时候,其实就是将业务模块层的各个功能业务拆分层独立的业务模块。所以我们进行模块化的***步就是业务模块划分,但是模块划分并没有一个业界通用的标准,因此划分的粒度需要根据项目情况进行合理把控,这就需要对业务和项目有较为透彻的理解。拿安居客来举例,我们会将项目划分为新房模块、二手房模块、IM 模块等等。

每个业务模块在 Android Studio 中的都是一个 Module ,因此在命名方面我们要求每个业务模块都以 Module 为后缀。如下图所示:

对于模块化项目,每个单独的 Business Module 都可以单独编译成 APK。在开发阶段需要单独打包编译,项目发布的时候又需要它作为项目的一个 Module 来整体编译打包。简单的说就是开发时是 Application,发布时是 Library。因此需要在 Business Module 的 build.gradle 中加入如下代码:

 
 
 
 
  1. if(isBuildModule.toBoolean()){
  2.     apply plugin: 'com.android.application'
  3. }else{
  4.     apply plugin: 'com.android.library'
  5. }

isBuildModule 在项目根目录的 gradle.properties 中定义:

 
 
 
 
  1. > isBuildModule=false
  2. >

同样 Manifest.xml 也需要有两套:

 
 
 
 
  1. sourceSets {
  2.    main {
  3.        if (isBuildModule.toBoolean()) {
  4.            manifest.srcFile 'src/main/debug/AndroidManifest.xml'
  5.        } else {
  6.            manifest.srcFile 'src/main/release/AndroidManifest.xml'
  7.        }
  8.    }
  9. }

如图:

debug 模式下的 AndroidManifest.xml :

 
 
 
 
  1.    ...
  2.    >
  3.    
  4.        android:name="com.baronzhang.android.newhouse.NewHouseMainActivity"
  5.        android:label="@string/new_house_label_home_page">
  6.        
  7.            
  8.            
  9.        
  10.    

realease 模式下的 AndroidManifest.xml :

 
 
 
 
  1.    ...
  2.    >
  3.    
  4.        android:name="com.baronzhang.android.newhouse.NewHouseMainActivity"
  5.        android:label="@string/new_house_label_home_page">
  6.        
  7.            
  8.            
  9.            
  10.            
  11.                android:scheme="router" />
  12.        
  13.    

同时针对模块化我们也定义了一些自己的游戏规则:

  • 对于 Business Module Layer,各业务模块之间不允许存在相互依赖关系,它们之间的跳转通讯采用路由框架 Router 来实现(后面会介绍 Router 框架的实现);
  • 对于 Business Component Layer,单一业务组件只能对应某一项具体的业务,个性化需求对外部提供接口让调用方定制;
  • 合理控制各组件和各业务模块的拆分粒度,太小的公有模块不足以构成单独组件或者模块的,我们先放到类似于 CommonBusiness 的组件中,在后期不断的重构迭代中视情况进行进一步的拆分;
  • 上层的公有业务或者功能模块可以逐步下放到下层,合理把握好度就好;
  • 各 Layer 间严禁反向依赖,横向依赖关系由各业务 Leader 和技术小组商讨决定。

模块间跳转通讯(Router)

对业务进行模块化拆分后,为了使各业务模块间解耦,因此各个 Bussiness Module 都是独立的模块,它们之间是没有依赖关系。那么各个模块间的跳转通讯如何实现呢?

比如业务上要求从新房的列表页跳转到二手房的列表页,那么由于是 NewHouseModule 和 SecondHouseModule 之间并不相互依赖,我们通过想如下这种显式跳转的方式来实现 Activity 跳转显然是不可能的实现的。

 
 
 
 
  1. Intent intent = new Intent(NewHouseListActivity.this, SecondHouseListActivity.class);
  2. startActivity(intent);

有的同学可能会想到用隐式跳转,通过 Intent 匹配规则来实现:

 
 
 
 
  1. Intent intent = new Intent(Intent.ACTION_VIEW, "://:/"); 
  2. startActivity(intent);

但是这种代码写起来比较繁琐,且容易出错,出错也不太容易定位问题。因此一个简单易用、解放开发的路由框架是必须的了。

我自己实现的路由框架分为路由(Router) 和参数注入器(Injector) 两部分:

Router 提供 Activity 跳转传参的功能;Injector 提供参数注入功能,通过编译时生成代码的方式在 Activity 获取获取传递过来的参数,简化开发。

Router

路由(Router)部分通过 Java 注解结合动态代理来实现,这一点和 Retrofit 的实现原理是一样的。

首先需要定义我们自己的注解(篇幅有限,这里只列出少部分源码)。

用于定义跳转 URI 的注解 FullUri:

 
 
 
 
  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface FullUri {
  4.     String value();
  5. }

用于定义跳转传参的 UriParam( UriParam 注解的参数用于拼接到 URI 后面):

 
 
 
 
  1. @Target(ElementType.PARAMETER)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface UriParam {
  4.     String value();
  5. }

用于定义跳转传参的 IntentExtrasParam( IntentExtrasParam 注解的参数最终通过 Intent 来传递):

 
 
 
 
  1. @Target(ElementType.PARAMETER)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface IntentExtrasParam {
  4.     String value();
  5. }

然后实现 Router ,内部通过动态代理的方式来实现 Activity 跳转:

 
 
 
 
  1. public final class Router {
  2.     ...
  3.     public  T create(final Class service) {
  4.         return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[]{service}, new InvocationHandler() {
  5.             @Override
  6.             public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  7.                 FullUri fullUri = method.getAnnotation(FullUri.class);
  8.                 StringBuilder urlBuilder = new StringBuilder();
  9.                 urlBuilder.append(fullUri.value());
  10.                 //获取注解参数
  11.                 Annotation[][] parameterAnnotations = method.getParameterAnnotations();
  12.                 HashMap serializedParams = new HashMap<>();
  13.                 //拼接跳转 URI
  14.                 int position = 0;
  15.                 for (int i = 0; i < parameterAnnotations.length; i++) {
  16.                     Annotation[] annotations = parameterAnnotations[i];
  17.                     if (annotations == null || annotations.length == 0)
  18.                         break;
  19.                     Annotation annotation = annotations[0];
  20.                     if (annotation instanceof UriParam) {
  21.                         //拼接 URI 后的参数
  22.                         ...
  23.                     } else if (annotation instanceof IntentExtrasParam) {
  24.                         //Intent 传参处理
  25.                         ...
  26.                     }
  27.                 }
  28.                 //执行Activity跳转操作
  29.                 performJump(urlBuilder.toString(), serializedParams);
  30.                 return null;
  31.             }
  32.         });
  33.     }
  34.     ...
  35. }

上面是 Router 实现的部分代码,在使用 Router 来跳转的时候,首先需要定义一个 Interface(类似于 Retrofit 的使用方式):

 
 
 
 
  1. public interface RouterService { 
  2. @FullUri("router://com.baronzhang.android.router.FourthActivity") 
  3. void startUserActivity(@UriParam("cityName") 
  4. String cityName, @IntentExtrasParam("user") User user); 
  5. }

接下来我们就可以通过如下方式实现 Activity 的跳转传参了:

 
 
 
 
  1. RouterService routerService = new Router(this).create(RouterService.class);
  2. User user = new User("张三", 17, 165, 88);
  3. routerService.startUserActivity("上海", user);

Injector

通过 Router 跳转到目标 Activity 后,我们需要在目标 Activity 中获取通过 Intent 传过来的参数:

 
 
 
 
  1. getIntent().getIntExtra("intParam", 0);
  2. getIntent().getData().getQueryParameter("preActivity");

为了简化这部分工作,路由框架 Router 中提供了 Injector 模块在编译时生成上述代码。参数注入器(Injector

当前题目:Android模块化探索与实践
浏览路径:http://www.hantingmc.com/qtweb/news20/310670.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联