谈谈我理解的Android组件化

作者: SSSuperYe | 来源:发表于2019-05-09 14:01 被阅读31次

    Android组件化分享

    为什么要做组件化

    一个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个清单文件的内容:
        组件模式
        <application
              android:name="runalong.XxxApp"
              android:icon="@mipmap/ic_launcher"
              android:label="@string/app_name"
              android:persistent="true"
              android:supportsRtl="true"
              android:theme="@style/AppTheme"
              tools:replace="android:label"
              tools:ignore="GoogleAppIndexingWarning">
              <activity
                  android:name=".main.MainActivity"
                  android:screenOrientation="landscape"
                  android:windowSoftInputMode="stateAlwaysHidden">
                  <intent-filter>
                      <action android:name="android.intent.action.MAIN"/>
      
                      <category android:name="android.intent.category.LAUNCHER"/>
                  </intent-filter>
              </activity>
      
              ....
        </application>
        
        集成模式
        <application android:theme="@style/AppTheme">
               <activity
                    android:name=".main.MainActivity"
                    android:screenOrientation="landscape"
                    android:windowSoftInputMode="stateAlwaysHidden">
                    
                    
               </activity>
        
               ....
         </application>
         
    可以看到,组件模式的时候,一个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<String,Class>中,以便提取。
    • 提取过程中,将制定的名字切割,用反射获取到指定包下的activity,就可以进行组件通信了。
    public static final String VIP_VIP = "vip/com.xxx.xxx.VipActivity";
    
    public class BridgeManager {
    
        private static final String TAG = "BridgeManager";
    
        private static HashMap<String, Class<Activity>> hashMap = new HashMap<>();
    
        public static Class<Activity> findBridgeObj(String bizName) {
            String className = parseBizName(bizName);
            if (TextUtils.isEmpty(className)) {
                return null;
            }
            Class<Activity> bridgeObject = hashMap.get(className);
            if (bridgeObject == null) {
                bridgeObject = createBridgeObject(className);
            }
            return bridgeObject;
        }
    
        private static boolean register(Class<Activity> 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<Activity> createBridgeObject(String className) {
            if (TextUtils.isEmpty(className)) {
                return null;
            }
    
            //反射
            Class<Activity> activityClass = null;
            try {
                Class<Activity> clazz = (Class<Activity>) 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<Activity> 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组件化

        本文链接:https://www.haomeiwen.com/subject/injwoqtx.html