组件化相关

2018-12-05  本文已影响0人  _Sisyphus

单项目结构:

将程序的所有功能以及依赖库都集中在一个项目下进行管理,不同业务或非业务通过包名区分,使得项目结构清晰,比如常见的登录、反馈、上报功能等。

适合小项目和需要快速迭代开发的项目。

插件化:

把 App 拆分成一个宿主和多个插件,插件可以在运行期动态加载

出现背景:

特点:重、黑科技;对团队和技术要求高。每个组负责单独插件开发,适合航母级应用。

组件化:

将一个 App 按照功能或者业务拆分为多个模块,每个模块作为单独的组件(module),可以独立开发和调试,最终在发布时,再将这些组件合并成完整的 Apk。

为什么需要组件化?
  1. 高内聚代码的强制解耦

  2. 各组件相互独立,便于开发和调试

    功能点比较少的项目,通过单项目工程就足以应付开发场景,当功能比较多的时候,比如集成一个直播、社交功能..功能点独立,完全可以由其它开发团队来开发、或根据模块由不同开发人员进行开发 (节省时间,提高开发效率)

  3. 便于项目的集成和更新

    比如这个组件并不在这个版本上使用,但是要先开发出来,等到下一个版本需要上的时候这个组件能够非常快集成进项目中去。

  4. 易于复用和扩展

    UI库样式一致、意见反馈这些模块,随时可以扔到另外的项目中去用。

  5. 有损服务,动态加载

    Android 生态复杂,手机性能差异巨大,有损服务出现在插件化概念中,可以通过判断用户手机性能,如果性能高就加载所有组件,如果比较差就先加载核心的,其他一些组件可能需要用户去开启或者去下载的 。(插件化范畴了)

    ...

组件化主流方案
  1. 冯森林 MDCC 2016 中国移动开发者大会

     多module,debug时候是apk,发布时作为library
    
  2. 大众点评 (AAR 独立 repo)

     组件放到单独的 git 仓库中,打包生成 aar ,供宿主或其他组件调用 
    
  3. 淘宝 Atlas 动态组件化(Dynamic Bundle)框架

     类似 OSGI ,其实是一种插件化方式
    
组件化模型
组件化模型

组件化具体实践

1. 组件依赖方式
  1. AAR 依赖,

    子module全部打包生成aar,由一个壳工程去组装构建 App,各业务模块独立拆分Git仓库,提高项目隔离性。

    优点:隔离性好
    缺点:上层依赖底层,发布顺序必须等到依赖的底层 AAR 开发完毕才能发布,有一定依赖性

  2. Compile project 依赖

    Module 在 debug 模式下作为Application,在release模式下作为library,各组件在调试时可以独立运行,在 App 发布时作为lib嵌入主项目。(简单、常用)

2. 组件独立编译

开发模式下,子模块可以单独调试,生成独立的 App,而在主程序发布时,则作为 library嵌入主程序,在子模块的 build.gradle 配置中,可根据常量判断是否处于开发模式

  1. 根目录配置文件中配置各个模块组件是否处于 App 还是 Module。

    • 项目根目录gradle.properties配置:

        # 是否需要单独编译 true表示不需要,false表示需要
      
        #isApp_Home=false
        #isApp_Chat=false
        #isApp_Video=false
        #isApp_Me=false
      
    • 在各个子模块中配置(例如Home Module):

        if (isApp_Home.toBoolean()) {
            apply plugin: 'com.android.application'
        } else {
            apply plugin: 'com.android.library'
        }
      
  2. applicationId,只有在 application 的情况下才需要声明。

     defaultConfig {
         if (isApp_Home.toBoolean()) {
              //单独运行时候需要 applicationId 
             applicationId "tsou.cn.module_me"
         }
     }
    
  3. sourceSets:在debug模式下生成自己的清单文件(需要入口函数或测试类作为一个独立app来运行的时候)

     sourceSets {
         main {
             if (isDebug.toBoolean()) {
                 manifest.srcFile 'src/main/debug/AndroidManifest.xml'
             } else {
                 manifest.srcFile 'src/main/release/AndroidManifest.xml'
                 java {
                     exclude 'debug/**'
                 }
             }
         }
     }
    
  4. 在app主模块中:

     if (isNeedHomeModule.toBoolean()) {
         compile project (':module_home')
     }
     if (isNeedChatModule.toBoolean()) {
         compile project (':module_chat')
     }
     if (isNeedRecomModule.toBoolean()) {
         compile project (':module_recom')
     }
     if (isNeedMeModule.toBoolean()) {
         compile project (':module_me')
     }
    
3. 组件 SDK 版本一致性

根目录定义ext统一版本,子module引用。避免版本冲突

  1. project 的 build.gradle 中定义常量

     ext {
         minSdkVersion = 16
         supportVersion='27.1.1'
     }
    
  2. 组件的 build.gradle 引用常量

     defaultConfig {
         minSdkVersion rootProject.ext.minSdkVersion
     }
     
     dependencies {
         api("com.android.support:appcompat-v7:${supportVersion}")
         ...
     }
    
4. 资源合并冲突(*)

参考:AndroidManifest合并原理

合并冲突: 是指多个Manifest文件中含有同一属性但值不同时,默认合并规则解决不了从而导致的冲突。
当冲突发生时,高优先级的Manifest属性值会覆盖低优先级属性值。这个优先级规则由高到低依次是:

buildType下的Manifest设置 > productFlavor下的Manifest设置 > 主工程src/main > dependency&library

不同 module 资源id合并:

  1. 如果两个模块中定义了相同资源id,将使用应用中的资源id
  2. 如果多个 AAR 库之间发生冲突,将使用依赖项列表首先列出(位于dependencies块顶部)的库中的资源
  3. 为避免资源id冲突,请使用在模块中具有唯一性前缀或其他一致性的命名方案
    • 3.1:Gradle 设置资源前缀: resourcePrefix "user_"
    • 3.2:修改 aapt(入侵了源码和app编译过程,存在风险)
  1. 模块私有资源:
    私有资源描述可参考

    如果不想让其他 module 访问我当前 module 的资源,可以申明私有属性
    1)资源库中所有资源默认处于公开状态
    2)要将所有资源隐私设为私有,至少将一个特定的属性定义为公开
    3)在res/value目录下,创建public.xml文件,定义公开资源

     <resources>
         <public name="mylib_main_layout" type="layout"/>
         <public name="mylib_public_string" type="string"/>
     </resources>
     除了以上定义的共有资源以外的都是私有资源
    
  2. AndroidManifest.xml 合并冲突

    Android Studio工程通常包含多个AndroidManifest文件,最终构建成APK时,会合并成一个AndroidManifest文件。

    合并冲突: 是指多个Manifest文件中含有同一属性但值不同时,默认合并规则解决不了从而导致的冲突。
    当冲突发生时,高优先级的Manifest属性值会覆盖低优先级属性值。这个优先级规则由高到低依次是:
    buildType下的Manifest设置 > productFlavor下的Manifest设置 > 主工程src/main > dependency&library

    清单文件合并规则.png

    合并规则标记:
    1. 节点标记:
    tools:node="replace" :在高优先级 Manifest 中添加,完全替换低优先级
    2. 属性标记:
    tools:remove="attr"
    3. 标记选择器:
    selector

5. 组件间解耦(页面跳转解耦+组件间通信解耦):
Splash(壳工程)--> 首页(module)-->意见反馈(module)  

以上三个需要交互的页面处于三个不同的Module,组件不可能完全拆分的干净,势必会有交互和通信;

  1. Android 原生支持 URL Scheme/AIDL(Service)

     protocol://host:port/path?params
     
     <data
         android:scheme="protocol"
         android:host="host"
         android:port="8080"
         android:path="/path"
     />
    
     Intent intent = new Intent(Intent.ACTION_VIEW,Uri.parse("url"));
    

Scheme 跳转解耦每个页面都需要在Manifest配置,扩展性差,跳转过程无法控制,参数传递困难 。 适合H5跳转到原生应用,如果原生跳转使用这个的话,规则太多,清单文件都需要去定义,非常不灵活。
AIDL(Service) 解耦,各组件都需要维护AIDL文件,相对比较复杂。

  1. 基于注解的路由框架(Arouter)

    特点:

    • 直接URL路由&参数解析赋值
    • 支持多模块项目
    • 支持InstantRun
    • 允许自定义拦截器(AOP)
    • 提供IOC容器(控制反转)
    • 隐射关系自动注册
    • 灵活的降级策略
ARouter

ARouter特点:
1)APT编译期生成关系映射表
2)运行期通过路径查找对应页面进行跳转
3)页面跳转完全解耦,不需要访问你那个类
4)缺点是 path、params非常量,可能会写错(需要自己管理)

ARouter不同组件接口调用:

提供IProvider:
面向接口编程:通过接口进行通信。
通信双方必须共同依赖该接口(缺点)。
如果我没有这个接口根本就调不到。
可以写到CommonLib中,但是不是好的方式。

组件间解耦思考?
  1. 组件间提供的页面以及接口路径,是以字符串还是常量的方式?如果是常量形式,常量放在哪?
  2. 组件间数据传递,非普通类型如何传递?Bean 应该放在哪个模块?
    需要传递的Bean位置:依赖包或CopyTask拷贝到CommomLib(公共的地方其实也就是)

解决方案:

创建组件的时候,同时创建该组件的依赖包。
组件通过Service依赖包提供自身可以被访问的页面路径常量,接口常量,以及需要传递参数的所有的Bean。 Module 之间依赖对方的 依赖包,通过依赖包中的公共部分提供通信基础。

共享依赖包方式
缺点是组件多的时候 Module 会变 Double,解决方案是通过 Gradle Copy Task 拷贝到CommonLib 目录下,具体:
依赖包不用单独建立Module,直接写在自己Module中,按照一定规则的路径存放。然后通过Gradle的脚本拷贝到 CommonLib目录下。(其实最后还是放到了公共的CommonLib目录下)
Gradle Copy Task复制到CommonLib目录下
6. 组件初始化

组件何时何地初始化:每个module实现一个入口类,用于统一调度(类似Application)。

7. 组件化可能遇到的坑
  1. R文件:
    App 项目生成的 R 文件是 static final(常量)的, ADT14后 library 项目中生成的并不是final类型。
    影响:

    • switch...case...
    • ButterKnife 注解
      其依赖于常量,所以当R文件不是常量时会失效。(R2或放弃使用..)
  2. 库发布
    默认情况下,library只发布release版本,当app时debug版本(未混淆),而 library 是已经混淆过的,而会导致编译问题。
    具体可参考

    一、在library module中的build.gradle中设置如下:

     android{
         publishNonDefault true  //不让发布默认release版本
     }
    

    二、在主 module 的build.gradle设置如下:

     dependencies {
         releaseCompile project(path: ':library', configuration: 'release')
         debugCompile project(path: ':library', configuration: 'debug')
         
         //flavor1Compile project(path:‘:lib_name’,configration:'release')
         //flavor2Compile project(path:‘:lib_name’,configration:'debug')
     }
    

    这样可以让app和library的debug和release保持一致

  3. 重复依赖:provided project 、exclude module

    • Project 重复依赖

        if(isApp_News.toBoolean()){
            compile project(':CommonLib')
        }else{
            provided project(': CommonLib')//去除依赖
        }
      

      依赖方式含义可参考

    • Duplicate entry(多个入口)

        //exclude 命令
        compile('com.jakewharton:butterknife:8.5.1'){
            exclude module:'support-compat'
        }
      

      support 包冲突

  1. 其他:
    DataBinding、Dagger、Retrolambda等第三方、多渠道打包、混淆和加固、Application Context

相关认识:

组件化的实施对开发人员和团队管理者提出了更高水平的要求.相对传统方式,在项目的管理和组织上难度加大,要求开发人员对业务有更深层次上的理解.

组件化首要做的事情就是划分组件.如何划分并没有一个确切的标准,建议早期实施组件化的时候,可以以一种”较粗”的粒度来进行,这样的好处在于后期随着对业务的理解和熟悉进行再次细分,而不会有太大的成本

这样的技术其实对于纯开发而言难度是不大的,真正的难度在于如何剥离现有的业务线。粒度大拆分比较容易,但是不利于今后的维护。粒度小需要对业务有很深的理解,但是能很好的解耦并且提高灵活度,所以具体的情况需要在具体的实际开发中进行分析。

组件化开发不是银弹,并不能完全解决当前业务复杂的情况,在进行项目实施和改进之前,一定要多加考量.

对当前项目实施组件化我的一些理解

组件化优点:

  1. 提高团队开发效率(并行开发)
  2. 项目结构清晰(多个业务 Module)
  3. 降低代码耦合性(各个业务组件相互隔离)
  4. 复用已有组件

一些组件化参考链接:

Android 开发:由模块化到组件化(一)

Android组件化方案

关于Android业务组件化的一些思考

Android-组件化改造项目之概述

终极组件化框架项目方案详解

Android-组件化如何处理多个ModuleApplication共存问题?

使用阿里ARouter路由实现组件化(模块化)开发流程+源码

Android彻底组件化demo发布-得到App方案


Android组件化框架设计与实践
我所理解的Android组件化之通信机制
Demo


多个维度对比一些有代表性的开源android组件化开发方案

上一篇 下一篇

猜你喜欢

热点阅读