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,然后引用到项目上等等。。
网友评论