欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

模块化(1):基本思路

程序员文章站 2022-08-08 11:15:08
一.什么是模块化 什么是模块化呢?有一种定义是:模块化是一种处理复杂系统分解为更好的可管理模块的方式。由此可见,模块化思路下构成的复杂系统是由各个可管理的子模块构成的,每个子模块之前相互独立,并通过某种特定的方式进行通信。在工业上面,有模块化汽车的概念,也有模块化手机的概念,各个模块根据一定的标准进 ......

一.什么是模块化

  什么是模块化呢?有一种定义是:模块化是一种处理复杂系统分解为更好的可管理模块的方式。由此可见,模块化思路下构成的复杂系统是由各个可管理的子模块构成的,每个子模块之前相互独立,并通过某种特定的方式进行通信。
在工业上面,有模块化汽车的概念,也有模块化手机的概念,各个模块根据一定的标准进行生产,生产之后可以直接进行各个模块的组装,某个模块出现问题之后,可以单独对这个模块进行替换。举个例子,同样一款汽车,有各中配置不同的版本,比如发动机不同。这些发动机都按照一定的标准生产,但是发送的输出和能耗并不同。重要的是其接口标准一样。从可替换这一点来讲,和软件开发中的可插拔是异曲同工的。

android 开发中有两个比较相似的概念:组件化和模块化,这里需要进行区分的。

组件化:指的是单一的功能组件,如地图组件、支付组件、路由组件(router)等等;
模块化:独立的业务模块,模块相对于组件来讲粒度更大。

模块化的好处是显而易见的。

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

android 插件化 ——指将一个程序划分为不同的部分,比如一般 app的皮肤样式就可以看成一个插件

android 组件化 ——这个概念实际跟上面相差不那么明显,组件和插件较大的区别就是:组件是指通用及复用性较高的构件,比如图片缓存就可以看成一个组件被多个 app共用

插件的方式只有三种:1,apk安装,2,apk不安装,3,dex包

 

二.模块debug和release处理

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

if(isbuildmodule.toboolean()){
   apply plugin: 'com.android.application'
}else{
   apply plugin: 'com.android.library'
}

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

isbuildmodule=false

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

sourcesets {
  main {
      if (isbuildmodule.toboolean()) {
          manifest.srcfile 'src/main/debug/androidmanifest.xml'
      } else {
          manifest.srcfile 'src/main/release/androidmanifest.xml'
      }
  }
}

模块化(1):基本思路

debug 模式下的 androidmanifest.xml :

 1 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 2    package="com.dajiazhongyi.dajia.pedumodule">
 3 
 4    <uses-permission android:name="android.permission.internet" />
 5    <uses-permission android:name="android.permission.read_phone_state" />
 6    <uses-permission android:name="android.permission.access_network_state" />
 7    <uses-permission android:name="android.permission.access_wifi_state" />
 8    <uses-permission android:name="android.permission.change_wifi_state" />
 9 
10    <application
11        ...
12        >
13 
14        <activity
15            android:name="com.dajiazhongyi.dajia.loginmodule.ui.dajialauncher"
16            android:exported="true"
17            android:screenorientation="portrait">
18            <intent-filter>
19                <action android:name="android.intent.action.main" />
20                <category android:name="android.intent.category.launcher" />
21            </intent-filter>
22            <intent-filter>
23                <action android:name="android.intent.action.view" />
24 
25                <category android:name="android.intent.category.default" />
26                <category android:name="android.intent.category.browsable" />
27 
28                <data android:scheme="dajia" />
29            </intent-filter>
30        </activity>
31 
32        <activity
33            android:name=".ui.mainactivity"
34            android:screenorientation="portrait"/>
35 
36    </application>
37 
38 </manifest>

realease 模式下的 androidmanifest.xml :

 1 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 2    package="com.dajiazhongyi.dajia.pedumodule">
 3 
 4    <application
 5        android:allowbackup="true"
 6        android:supportsrtl="true">
 7 
 8        <activity
 9            android:name="com.dajiazhongyi.dajia.pedumodule.ui.peducationlistactivity"
10            android:screenorientation="portrait"/>
11 
12        <activity
13            android:name="com.dajiazhongyi.dajia.pedumodule.ui.syslib.systemedudetaillistactivity"
14            android:screenorientation="portrait"/>
15 
16        <activity
17            android:name="com.dajiazhongyi.dajia.pedumodule.ui.syslib.systemedulistactivity"
18            android:screenorientation="portrait"/>
19 
20    </application>
21 
22 </manifest>

三.模块化分层设计

合理的模块化分层设计是非常重要的,就像一个房子一样,合理的框架设计是成功的保证。
模块化分层设计需要达到以下几个目标:

  1. 模块职责明确;

  2. 模块代码边界清晰;

  3. 模块通信

四.模块职责明确

根据职责进行分层设计是合理有效的,以下是在项目实践中采用的分层设计。

模块化(1):基本思路

①.sdk
sdk层包括的内容如图所示,需要强调的是并不是所有的第三方libraries都放到sdk,必须是通用的基础级别的。

②.组件库
我们将各个业务模块公用的组件整合到组件库中,组件库并不一定是一个module,它也可以是多个module,实际使用的时候更多的被业务模块依赖。

③.basecore
这是最重要的一个层级,app核心的部分就是它,basecore可以用通用的定义以下几个部分:

模块化(1):基本思路

coreaccount: app账号管理,账号登录、注销、profile信息获取等;
corenetwork: 以retrofit2为例,corenetwork并不提供业务模块的api,只是提供基础的网络状态管理、网络错误管理;
corestorage: 处理sqlite、preferences;
corecommunication:模块之间的通信主要有三种:事件通知、页面跳转(activity、service)、接口调用。模块通信是最重要的层次,后面会重点讲

此外,这个层次是最容易代码越界的层次,随着业务的不断复杂,业务模块中的代码是极有可能下沉到basecore的,从而导致core层代码越来越冗余。清晰合理的代码边界规范是重要的。

④业务模块
业务模块的拆分粒度需要把控,太小的粒度并不是很合理。其中app(release)是最终发布出去的版本,它是对其他模块1…n 的整合。各个业务模块在debug’阶段,可以独立打包成apk进行调试,在release阶段,则作为app的module被引用。各个业务模块之间不进行相互调用,它们之间的通信通过basecore层来实现。

五.代码边界

合理的代码边界约定可以保证层次的清晰、避免架构变得冗余,虽然没法完全保证,毕竟定期的重构是无法避免的。

①各个业务模块之间无依赖关系,模块之间页面的跳转通过arouter等页面路由协议进行;

②模块之间的事件通信采用eventbus,并依赖于basecore层的事件manager进行管理;

③模块之间的功能暴露全部通过接口,接口需要下沉到basecore层,接口使用前必须先注册,调用方式形如下,后续文章会详细介绍:

servicemanager.regist(pluginservice.class); 
servicemanager.get(pluginservice.class).execute();

④组件库组件必须提供个性化定制,方便业务模块使用;

⑤合理控制各组件和各业务模块的拆分粒度,太小的公有模块不足以构成单独组件或者模块的,我们先放到类似于 commonmodule 的组件中,在后期不断的重构迭代中视情况进行进一步的拆分;

⑥上层的公有业务或者功能模块可以逐步下放到下层,下放过程中按照层次职责归类下放;

⑦各个模块之间的横向依赖关系,比如在使用pluginservice2之前,需要先注册pluginservice1,这种依赖管理后续会详细介绍

 

六.模块通信

模块通信需要解决三大问题:

  1. 页面跳转

  2. 事件通知

  3. 接口调用

页面跳转

这里介绍一款页面路由神器:arouter https://github.com/alibaba/arouter

本着能用、够用、好用的原则,这款神器支持以下功能:

  1. 支持直接解析标准url进行跳转,并自动注入参数到目标页面中
  2. 支持多模块工程使用
  3. 支持添加多个拦截器,自定义拦截顺序
  4. 支持依赖注入,可单独作为依赖注入框架使用
  5. 支持instantrun
  6. 支持multidex(google方案)
  7. 映射关系按组分类、多级管理,按需初始化
  8. 支持用户指定全局降级与局部降级策略
  9. 页面、拦截器、服务等组件均自动注册到框架
  10. 支持多种方式配置转场动画
  11. 支持获取fragment
  12. 完全支持kotlin以及混编(配置见文末 其他#5)

其调用方式如下:

1. 添加注解
@route(path = "/test/activity")
public class youractivity extend activity {
   ...
}

2. 初始化sdk
if (isdebug()) {           // 这两行必须写在init之前,否则这些配置在init过程中将无效
   arouter.openlog();     // 打印日志
   arouter.opendebug();   // 开启调试模式(如果在instantrun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
arouter.init(mapplication); // 尽可能早,推荐在application中初始化

3. 发起路由操作
// 1\. 应用内简单的跳转(通过url跳转在'进阶用法'中)
arouter.getinstance().build("/test/activity").navigation();

// 2\. 跳转并携带参数
arouter.getinstance().build("/test/1")
  .withlong("key1", 666l)
  .withstring("key3", "888")
  .withobject("key4", new test("jack", "rose"))
  .navigation();

实际应用中,在basecore中实现一个routermanager,管理路由初始化,跳转等事宜:

public class routermanager {

   /**
    * router path
    */
   public static final string url_welcome = "/loginmodule/welcome";
   public static final string url_login = "/loginmodule/login";

   public static final string url_main_login = "/loginmodule/main";
   public static final string url_main_pedu = "/pedumodule/main";

   ...

   /**
    * module application name
    */
   public static final string module_login = "loginmodule";
   public static final string module_pedu = "pedumodule";

   public static void initrouter(application application) {
       if (buildconfig.debug) {
           arouter.openlog();     // 打印日志
           arouter.opendebug();   // 开启调试模式(如果在instantrun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
       }
       arouter.init(application);
   }

   public static void gotonewpage(context context, string pageurl) {
       arouter.getinstance().build(pageurl).navigation();
   }

   public static void gowelcome(context context) {
       arouter.getinstance().build(url_welcome).navigation();
   }

   public static void gologin(context context) {
       arouter.getinstance().build(url_login).navigation();
   }

   public static void gohome(context context) {
       string packagename = context.getapplicationinfo().packagename;
       logutils.logd(packagename);
       string suffix = packagename.substring(packagename.lastindexof(".") + 1);
       switch (suffix) {
           case module_login:
               arouter.getinstance().build(url_main_login).navigation();
               break;
           case module_pedu:
               arouter.getinstance().build(url_main_pedu).navigation();
               break;
       }
   }

   ...
}

更多使用方法可以参考github该库的详细介绍

由于篇幅原因,事件通知、接口调用将在后续文章中介绍!!

其他问题

资源名冲突

对于多个 bussines module 中资源名冲突的问题,可以通过在 build.gradle 定义前缀的方式解决:

defaultconfig {
  ...
  resourceprefix "module_name_"
  ...
}

而对于 module 中有些资源不想被外部访问的,我们可以创建 res/values/public.xml,添加到 public.xml 中的 resource 则可被外部访问,未添加的则视为私有:

<resources>
   <public name="module1_str" type="string"/>
</resources>