谈谈我理解的Android组件化

Android组件化分享

  • 为什么要做组件化
  • 认识一下组件化
  • 组件化流程与问题
    • 1、组件模式与集成模式的切换
    • 2、AndroidManifest清单文件合并问题
    • 3、全局Context的获取
    • 4、lib第三方库的依赖
    • 5、组件之间的通信
    • 6、资源文件命名问题与规范
    • 7、组件化三种工程类型的(build.gralde)
  • BuildConfig.DEBUG始终为true
  • 关于组件化混淆
  • 总结

为什么要做组件化

一个App总归是要迭代更新的,这个过程中,业务逻辑也会慢慢增加或者修改的越来越复杂,这样业务模块也就是对应的package继续增加是不可避免的
,相应的每个模块的代码只会变多,所以单一工程下的APP或者说单一业务组件的架构极有可能会影响开发效率
,站在新员工的角度来看,每个伙伴着手前需要熟悉如此多的代码,较难上手,而且编译代码时间会非常卡,开发过程中,出现问题需要跑整个项目,所以必须要有更灵活的架构代替过去单一的工程架构。

认识一下组件化

先来解释一下组件化两种模式

  • 集成模式:所有的业务组件(module)都是被空壳(app module)依赖,合成一个完整的项目.
  • 组件模式:可以单独运行编译出独立的项目,简单的说就是一个组件一个app

再来看看切割的业务组件和功能组件

  • app module:原本单一工程的主角,大部分的业务都写在其中,甚至功能工具,现在他是一个空壳,用来整合各个业务组件(a module……),负责打包apk等,没有具体的业务功能
  • launch module 也算半个业务组件,负责制定APP启动界面。
  • a module 根据a业务组件独立形成的一个工程
  • b module 根据b业务组件独立形成的一个工程
  • c module 根据c业务组件独立形成的一个工程
  • common module 一个功能组件,为业务组件提供对应的功能(可细拆分功能)

其实已经很清晰了,简单一点说就是,组件化就是将从前的模块化的东西,拆成了组件形式,common组件问题不大,一般app架构里都会有这么一个功能组件,组件模式后单独运行代码量,少之又少,
可以提高速度,方便测试。
这里有一点是需要考虑的,就是并不是所有模块都是适合拆出来成为组件,成为一个特立独行的工程,拆成组件需要对业务有比较深的理解,哪些业务是紧密连接的,哪些业务是可切割的。
不是组件越多越好,而应该以组件切割得清晰来衡量这个架构的水平。
我的理解是,其实在上面已经说到过,工程这个词,如果拆出来的模块能构成一个小工程来运行,或者说可以帮助项目解耦,方便单元测试,甚至是编译速度,那么它都是可拆的。

组件化流程与问题

组件模式与集成模式的切换
apply plugin: ‘com.android.application’ 对应的是Android应用程序,也就是我们的App,可以独立运行
apply plugin: ‘com.android.library’ 对应的是Android 库文件,可以理解为本地库,不可独立运行

每个组件的属性都放在build.gradle文件中,其中控制这两个模式的属性,一般就在文件第一行。
业务组件处在application属性时,这个组件就是一个工程,独立运行,开发和调试,当处在library时,他才可以被app空壳工程依赖,与其他业务组件合成一个完整的app。
那么要如何切换这个属性呢?
肯定是不能每次都修改build.gradle文件的属性的,必须需要一个开关来决定这个组件的模式,这时候就需要一个常量来判断,我所知道的有两种方式创建这个常量。

1、其实在项目根目录下有一个**gradle.properties**文件,在Android项目中的任何一个**build.gradle**文件中都可以把**gradle.properties**中的常量读取出来。
2、或者你定义一个全局配置**config.gradle**,在系统级别的**build.gradle**把**config.gradle**apply进去,在**config.gradle**文件中定义常量

定义一个常量值isAModuleApplication*(true为集成模式,false为组件模式),操作如下:

需要注意的是,取出来的值,它是String类型,这时候需要以下写法
if (isAModuleApplication.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

改完之后,同步一下就可以看到效果了

AndroidManifest清单文件合并问题
  • 一个组件当它是组件模式的时候,他的AndroidManifest需要几个作为application应用(也就是App工程)的东西,特别是声明一个application和设置一个入口(启动界面)。
  • 一个组件当它是集成模式的时候,它的AndroidManifest会被合并到app空壳工程里,那么一个工程不应该要有多个入口或者多个application。。

那么问题来了,怎么才能让它是组件模式的时候有对应的东西,集成的时候又抹除不该有的?

答案很简单,需要有两个AndroidManifest清单文件,一份作为组件模式独立运行使用,一份作为集成模式被app空壳依赖使用,还要两份对应的各自的application对象。

现在就是要让程序知道在不同模式下使用不同的AndroidManifest清单文件和application。

在main文件夹下面创建一个runalong文件夹,new一个清单文件,文件夹名字可以随便取,意思要到位,独立运行!
在java文件夹下面创建一个runalong文件夹,new一个自定义的application对象,文件名字可以随便取。

这时候有2个清单文件和application,需要程序自己取了,在业务组件下的**build.gradle**中指定清单文件的路径,操作如下
 sourceSets {
        main {
            if (isAModuleApplication.toBoolean()) {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/runalong/AndroidManifest.xml'
                java {
                      exclude 'runalong/**'
                }
            }
        }
    }
再来看看2个清单文件的内容:
    组件模式
    
          
              
                  
  
                  
              
          
  
          ....
    
    
    集成模式
    
           
                
                
           
    
           ....
     
     
可以看到,组件模式的时候,一个app需要的东西一个都不能少,集成模式的时候,基本上是一个都不能要。

因为处在组件模式,不需要空壳做任何操作,那么可以如下操作

if(isAModuleApplication.toBoolean()){
    java {
          exclude 'com/xxx/xxx/**'
    }
}
全局Context的获取

开发过程中,一般我们会自定义一个继承Application的对象,来获取全局Context。
现在要做的是,不管处在什么模式下都能获得全局Context
上面提到过,当我们在组件模式开发中,每一个组件都要有application,所以我们在java文件夹下面创建一个runalong文件夹,同时声明一个application来支持组件特立独行。。一切看似都很美好
当我们切换到集成模式的时候,会发现runalong中的application没有执行,因为main文件夹下runalong下的清单文件被排除了,所以只有app空壳工程中的application才有全局Context。
现在我们就需要用到common module(公用功能组件)了,定义一个BaseApplication,继承Application,因为app空壳工程依赖common组件,所以将app空壳工程中的自定义的application
对象继承BaseApplication,并且,在app空壳工程中的清单文件中声明这个自定义的application对象,以确保集成模式启动时,common组件中的BaseApplicaition被执行,至此,保证集成模式下
其他业务组件都可以获取的到全局的Context对象。
需要注意的是,其他业务组件在独立运行的时候,需要将runalong文件夹下的自定义application对象继承common组件中的BaseApplication,并在其runalong文件夹下的清单文件中声明,保证组件模式下
的common组件中的BaseApplication被执行。
所以不管是组件模式独立运行还是集成模式都可以获取全局Context对象。

lib第三方库的依赖

项目中多少都会使用到一些实用的库,当多人协作开发时,每个人基本上是管好自己的项目,这样会造成第三方库重复甚至泛滥。
所以

  • 首先需要对第三方库进行评估,尽量排除不稳定或者不更新的lib
  • 为了统一管理,我们将第三方库放在common组件中,提供给业务组件
  • 在common组件中,我们需要使用api(这里效果是和compile是一样的),不能使用implementation来加载,implementation只会在自身组件中使用,不能对外提供。
组件之间的通信

因为组件之间没有相互依赖,所以不存在直接调用,那么需要如何调用呢??
首先想一下,我们每个组件都有依赖一个叫做common的组件,我们依然还是需要它作为中间的一个桥梁,帮助我们让海峡两岸进行沟通,开始做桥梁吧

  • 我们需要一个桥梁管理器,BridgeManager,用来管理无数个桥梁,为每个actitvity制定一个易于管理的名字,用功能/包名+类名,如vip/com.xxx.xxx.VipActivity,来命名。
  • BridgeManager注册这些名字,存在Map中,以便提取。
  • 提取过程中,将制定的名字切割,用反射获取到指定包下的activity,就可以进行组件通信了。
public static final String VIP_VIP = "vip/com.xxx.xxx.VipActivity";

public class BridgeManager {

    private static final String TAG = "BridgeManager";

    private static HashMap> hashMap = new HashMap<>();

    public static Class findBridgeObj(String bizName) {
        String className = parseBizName(bizName);
        if (TextUtils.isEmpty(className)) {
            return null;
        }
        Class bridgeObject = hashMap.get(className);
        if (bridgeObject == null) {
            bridgeObject = createBridgeObject(className);
        }
        return bridgeObject;
    }

    private static boolean register(Class activityClass) {
        if (activityClass == null) {
            return false;
        }

        String classNameKey = activityClass.getName();
        if (hashMap.containsKey(classNameKey)) {
            Log.e(TAG, "请勿重复注册 key" + classNameKey);
        }
        hashMap.put(classNameKey, activityClass);
        return true;
    }

    private static Class createBridgeObject(String className) {
        if (TextUtils.isEmpty(className)) {
            return null;
        }

        //反射
        Class activityClass = null;
        try {
            Class clazz = (Class) Class.forName(className);
            if (register(clazz)) {
                activityClass = clazz;
            }
        } catch (Exception e) {
            Log.e(TAG, e.getMessage());
        }
        return activityClass;
    }

    private static String parseBizName(String bizName) {
        if (TextUtils.isEmpty(bizName)) {
            return null;
        }
        int index = bizName.indexOf("/");
        if (index != -1) {
            return bizName.substring(index + 1);
        } else {
            throw new IllegalArgumentException("not found the bizName :" + bizName);
        }
    }
}

public static void startAct(Context context, String bizName) {
        Class activityClass = BridgeManager.findBridgeObj(bizName);
        context.startActivity(new Intent(context, activityClass));
    }

过程很简单,就是利用反射获取包名进行调用,怎么封装也有很多花样,这里只是提供一个思路,还是极力推荐使用ARouter进行组件通信,方便快捷,可以了解一下。

资源文件命名问题与规范

单多个协同开发时,难免存在一些资源文件上的命名冲突,比如都有一个drawable_background的drawable文件,两个命名如果是一样的,在集成模式下会导致编译不通过。
最直接的办法就是组内人员规定某些命名,但是不可估计和预判的资源文件是没法说明哪个文件用哪个命名,所以只能在资源文件名的头部,加上我们的组件名,如,a_drawable_background,b_drawable_background
这里还存在一个问题,因为人做事总会疏忽,不是这次就是下次,所以有没有办法约束一下命名,答案是有!

android{
    ......
    
    resourcePrefix vip_
    
    .....
}

这样每次创建新的资源文件,都会强制要求你文件名必须以vip_开始,否则就会报红,虽然并不影响编译和运行,但是会有一个强烈的错误警告,起到很好的提示作用
值得一提的是图片也是属于资源文件,但是并不会对图片命名有约束,这个一点还是要开发人员手动修改,或者根据使用场景规范命名。
BuildConfig.DEBUG始终为true

开发中一般会通过 BuildConfig.DEBUG 判断是否是 Debug 模式,从而做一些在 Debug 模式才开启的特殊操作,比如打印日志。这样好处是不用在发布前去主动修改,因为这个值在 Debug 模式下为 true,Release 模式下为 false。
如果应用只有一个 Module 没有问题,Debug 模式下BuildConfig.DEBUG 会始终为 true。如果现在有两个Module,会有问题。
比如一个A module和common module,common module中的日志工具中使用了BuildConfig.DEBUG来判断是否输出日志,那么永远都是false。
BuildConfig.java 是编译时自动生成的,并且每个Module都会生成一份,所以如果你的应用有多个 Module 就会有多个 BuildConfig.java 生成。
而上面的common module import 的是自己的BuildConfig.java,编译时被依赖的 Module 默认会提供 Release 版给其他 Module 或工程使用,这就导致该 BuildConfig.DEBUG 会始终为 false。
解决方案,我有两种:

  • 始终调用最终运行的Module的BuildConfig,因为它没有被任何其他Module依赖,所以BuildConfig.DEBUG 值会准确。
public class AppUtils {
 
    private static Boolean isDebug = null;
 
    public static boolean isDebug() {
        return isDebug == null ? false : isDebug.booleanValue();
    }
 
    /**
     * Sync lib debug with app's debug value. Should be called in module Application
     *
     * @param context
     */
    public static void syncIsDebug(Context context) {
        if (isDebug == null) {
            isDebug = context.getApplicationInfo() != null &&
                    (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
        }
    }
}
  • 让被依赖的Module提供除Release版以外的其他版本
android {
    publishNonDefault true
}
表示该Module打包时会同时打包其他版本,包括Debug版。并且需要在App空壳中将其依赖的common如下逐个添加:
dependencies {
    releaseImplementation project(path: ':common', configuration: 'release')
    debugImplementation project(path: ':common', configuration: 'debug')
}
表示依赖不同版本的common Module。
组件化三种工程类型的build.gralde
  • app空壳工程
  • common功能组件
  • 业务组件

app空壳工程

与单一工程的**build.gradle**并没有什么不同,需要注意的是根据isModuleApplication来选择引入不同的依赖,和排除不同模式下不需要的文件夹,以下是一份app空壳工程的简单build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion

    defaultConfig {
        applicationId rootProject.ext.android.applicationId
        minSdkVersion rootProject.ext.android.minSdkVersion
        targetSdkVersion rootProject.ext.android.targetSdkVersion
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName
        testInstrumentationRunner rootProject.ext.android.AndroidJUnitRunner
        multiDexEnabled rootProject.ext.android.multiDexEnabled
        
        ....
    }
    
    ....

    sourceSets {
        main {
            if (isAModuleAppliction.toBoolean()) {
                java {
                    exclude 'com/xxx/xxx/**'
                }
            }
            
            ....
        }
    }

    ....
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation project(':common')
    if (!isAModuleCashAppliction.toBoolean()) {
        implementation project(':a_module')
    }
    
    ....
}

common功能组件

不管是什么模式下,common module永远都是apply 'com.android.library',本身也不存在什么独立运行,直接贴伪代码
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'

android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion

    defaultConfig {
        minSdkVersion rootProject.ext.android.minSdkVersion
        targetSdkVersion rootProject.ext.android.targetSdkVersion

        testInstrumentationRunner rootProject.ext.android.AndroidJUnitRunner
        
        ....
    }

    buildTypes {
        debug {
            ....
        }
        release {
            ....
        }
    }

    compileOptions {
        sourceCompatibility rootProject.ext.android.compileOptions.sourceCompatibility
        targetCompatibility rootProject.ext.android.compileOptions.targetCompatibility
    }

    resourcePrefix rootProject.ext.module_common.resourcePrefix_name
    sourceSets {
        main {
            ....
        }
    }

    publishNonDefault true
    
    ....
}

dependencies {
    api fileTree(include: ['*.jar'], dir: 'libs')
    api rootProject.ext.dependencies.appcompat_v7
    api rootProject.ext.dependencies.design
    api rootProject.ext.dependencies.butterknife
    annotationProcessor rootProject.ext.dependencies.butterknife_compiler
    
    ....
}

业务组件

业务组件需要根据不同情况切换模式,代码
if (isAModuleAppliction.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
apply plugin: 'com.jakewharton.butterknife'

android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion



    defaultConfig {

        if (isAModuleAppliction.toBoolean()) {
            applicationId rootProject.ext.android.AModuleapplicationId
            multiDexEnabled rootProject.ext.android.multiDexEnabled
        }

        minSdkVersion rootProject.ext.android.minSdkVersion
        targetSdkVersion rootProject.ext.android.targetSdkVersion

        testInstrumentationRunner rootProject.ext.android.AndroidJUnitRunner

    }
    
    resourcePrefix rootProject.ext.module_a.resourcePrefix_name
    sourceSets {
        main {
            if (isAModuleAppliction.toBoolean()) {
                manifest.srcFile 'src/main/runalong/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                //集成模式下排除runalong文件夹中的所有Java文件
                java {
                    exclude 'runalong/**'
                }
            }
        }
    }

    compileOptions {
        sourceCompatibility rootProject.ext.android.compileOptions.sourceCompatibility
        targetCompatibility rootProject.ext.android.compileOptions.targetCompatibility
    }

    publishNonDefault true
    
    ....
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')

    implementation project(':common')

    annotationProcessor rootProject.ext.dependencies.butterknife_compiler
    annotationProcessor rootProject.ext.dependencies.arouter_compiler
    ....
}

关于组件化混淆

一般关于组件化混淆有两种做法

  • 直接使用app空壳工程中的混淆规则,集成模式下一旦app空壳开始混淆,其他依赖的组件都会默认开启混淆。
  • 各自组件使用各自的混淆规则,需要有比较好的管理
选择第二种,需要在**build.gradle**中添加如下
release{
        consumerProguardFiles   'proguard-rules.pro'
}
业务组件中的混淆规则对app空壳工程是不构成影响的,所以就只存在该组件相关的混淆规则,共有的可以选择放在common组件或者app空壳中

总结

  • 组件化相较于单一工程,在组件模式下可以提高编译速度,方便单元测试,提高开发效率。
  • 开发人员分工更加明确,基本上做到互不干扰。
  • 业务组件的架构也可以自由选择,不影响同伴之间的协作。
  • 降低维护成本,代码结构更加清晰。

组件化其实并不复杂,复杂的是,我们开发者为了更加容易区分功能业务,把它解耦得更彻底,导致某些地方和以往的有所偏差,需要深入浅出的了解后才能处理,
这个个人认为跟mvc到mvp再到mvvm的发展历程道理是一样的,一样是为了解耦,写更多的东西,慢慢完善趋于稳定,所以离开舒适区,当然是要复出代价的。
组件化每个人的理解可能都会不同,我这边也需要慢慢完善,毕竟步子大了扯到蛋,当然这也不是组件化的最终形态,比如,你可以将组件上传私有maven,然后引用到项目上等等。。

你可能感兴趣的:(谈谈我理解的Android组件化)