谈谈我理解的Android组件化

栏目: IOS · Android · 发布时间: 5年前

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

一个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 ,然后引用到项目上等等。。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数字战争

数字战争

[英]查尔斯·亚瑟 / 余淼 / 中信出版社 / 2013-6-1 / 49

1998年,数码世界初具雏形。 至此以往,大浪淘沙。随着IT产业的迅猛发展,涌现出了以苹果、谷歌、微软为首的行业巨头。它们为争夺数码世界不同分支的霸主地位而争斗,包括搜索技术、移动音乐、智能手机和平板电脑市场。它们可利用的武器包括硬件、软件以及广告。同时,它们要赌上的则是公司的声望,当然,还有我们的未来。然而,无论在产品创新还是在战略优势上,这些企业彼此竞争、彼此砥砺,推动了行业的良性发展。......一起来看看 《数字战争》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码