zoukankan      html  css  js  c++  java
  • 模块化(1):基本思路

    一.什么是模块化

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

    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'
          }
      }
    }

    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. 模块通信

    四.模块职责明确

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

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

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

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

    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>
  • 相关阅读:
    mysql联合主键,也就是两个数据字段一起做主键的情况
    PHP细节,empty,is_null,isset,if()
    PHP细节,PHP手册中常见的一句话:该函数是二进制安全的
    git和github的学习
    用WPS查看两篇word文档异同之处
    js全角字符转为半角字符
    坑(十七)—— Linux无法挂载NTFS格式的U盘
    subprocess模块
    吴裕雄--天生自然--Go 语言学习笔记--Go 语言数组
    吴裕雄--天生自然--Go 语言学习笔记--Go 语言变量作用域
  • 原文地址:https://www.cnblogs.com/ganchuanpu/p/9493397.html
Copyright © 2011-2022 走看看