app壳工程是从名称来解释就是一个空壳工程,没有任何的业务代码,也不能有Activity,但它又必须被单独划分成一个组件,而不能融合到其他组件中,是因为它有如下几点重要功能:
1、app壳工程中声明了我们Android应用的 Application,这个 Application 必须继承自 Common组件中的 BaseApplication(如果你无需实现自己的Application可以直接在表单声明BaseApplication),因为只有这样,在打包应用后才能让BaseApplication中的Context生效,当然你还可以在这个 Application中初始化我们工程中使用到的库文件,还可以在这里解决Android引用方法数不能超过 65535 的限制,对崩溃事件的捕获和发送也可以在这里声明。
2、app壳工程的 AndroidManifest.xml 是我Android应用的根表单,应用的名称、图标以及是否支持备份等等属性都是在这份表单中配置的,其他组件中的表单最终在集成开发模式下都被合并到这份 AndroidManifest.xml 中。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="txkj.xian.com.zjdemo">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"></application>
</manifest>
3、app壳工程的 build.gradle 是比较特殊的,app壳不管是在集成开发模式还是组件开发模式,它的属性始终都是:com.android.application,因为最终其他的组件都要被app壳工程所依赖,被打包进app壳工程中,这一点从组件化工程模型图中就能体现出来,所以app壳工程是不需要单独调试单独开发的。另外Android应用的打包签名,混淆,以及buildTypes和defaultConfig都需要在这里配置,而它的dependencies则需要根据isModule的值分别依赖不同的组件,在组件开发模式下app壳工程只需要依赖Common组件,或者为了防止报错也可以根据实际情况依赖其他功能组件,而在集成模式下app壳工程必须依赖所有在应用Application中声明的业务组件,并且不需要再依赖任何功能组件。
下面是一份 app壳工程 的 build.gradle文件示例:
apply plugin: 'com.android.application'
static def buildTime() {
return new Date().format("yyyyMMdd");
}
android {
signingConfigs {
release {
keyAlias 'guiying712'
keyPassword 'guiying712'
storeFile file('/mykey.jks')
storePassword 'guiying712'
}
}
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId "com.guiying.androidmodulepattern"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
multiDexEnabled false
//打包时间
resValue "string", "build_time", buildTime()
}
buildTypes {
release {
//更改AndroidManifest.xml中预先定义好占位符信息
//manifestPlaceholders = [app_icon: "@drawable/icon"]
// 不显示Log
buildConfigField "boolean", "LEO_DEBUG", "false"
//是否zip对齐
zipAlignEnabled true
// 缩减resource文件
shrinkResources true
//Proguard
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
//签名
signingConfig signingConfigs.release
}
debug {
//给applicationId添加后缀“.debug”
applicationIdSuffix ".debug"
//manifestPlaceholders = [app_icon: "@drawable/launch_beta"]
buildConfigField "boolean", "LOG_DEBUG", "true"
zipAlignEnabled false
shrinkResources false
minifyEnabled false
debuggable true
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
if (isModule.toBoolean()) {
compile project(':lib_common')
} else {
compile project(':module_main')
compile project(':module_girls')
compile project(':module_news')
}
}
可以独立运行也可作为库依赖到壳app中,例如modulea和moduleb等,业务组件必须在自己的 build.gradle 中根据 isModule 值的不同改变自己的属性,在组件模式下是:com.android.application,而在集成模式下com.android.library;同时还需要在build.gradle配置资源文件,如 指定不同开发模式下的AndroidManifest.xml文件路径,排除debug文件夹等;业务组件还必须在dependencies中依赖基础组件baselib,并且引入路由的注解处理器annotationProcessor,以及依赖其他用到的功能组件,业务组件的build.gradle文件一般如下:
if (isModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
}
sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
//设置了resourcePrefix值后,所有的资源名必须以指定的字符串做前缀,否则会报错。
//但是resourcePrefix这个值只能限定xml里面的资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名。
//resourcePrefix "girls_"
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
// activityrouter路由注解,也可使用aroute
annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
compile project(':baselib')
}
一般我们还会单独创建一个main业务组件,Main组件集成模式(以library形式存在时)下的AndroidManifest.xml是跟其他业务组件不一样的,Main组件的表单中声明了我们整个Android应用的launch Activity,这就是Main组件的独特之处;所以我建议SplashActivity、登陆Activity以及主界面都应属于Main组件,也就是说Android应用启动后要调用的页面应置于Main组件。
<activity
android:name=".splash.SplashActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
功能组件可能会存在多个,如:画廊组件,分享组件,支付组件,评论反馈组件等,每一个功能组件都可以被一个或者多个不同的业务组件依赖。
功能组件的特征如下:
1,功能组件的 AndroidManifest.xml 是一张空表,这张表中只有功能组件的包名;
2、功能组件不管是在集成开发模式下还是组件开发模式下属性始终是: com.android.library,所以功能组件是不需要读取 gradle.properties 中的 isModule 值的;另外功能组件的 build.gradle 也无需设置 buildTypes ,只需要 dependencies 这个功能组件需要的jar包和开源库。
下面是一份 普通 的功能组件的 build.gradle文件:
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
不管是在集成开发模式下还是组件开发模式下属性始终是: com.android.library
和功能组件是一样的,且为同级关系,唯一的区别是功能组件是被某个或某几个特定的业务组件modele依赖的,而公共基础组件common是要被所有业务组件module都依赖的,common组件除了有功能组件的普遍属性外,还具有其他功能:
1、Common组件的 AndroidManifest.xml 不是一张空表,这张表中声明了我们 Android应用用到的所有使用权限 uses-permission 和 uses-feature,放到这里是因为在组件开发模式下,所有业务组件就无需在自己的 AndroidManifest.xm 声明自己要用到的权限了。
2、Common组件的 build.gradle 需要统一依赖业务组件中用到的 第三方依赖库和jar包,例如我们用到的ActivityRouter、Okhttp,官方的依赖如v4,v7,design,recyclerview等。
3、Common组件中封装了Android应用的 Base类和网络请求工具、图片加载工具等等,公用的 widget控件也应该放在Common 组件中;业务组件中都用到的数据也应放于Common组件中,例如保存到 SharedPreferences 和 DataBase 中的登陆数据;
4、Common组件的资源文件中需要放置项目公用的 Drawable、layout、sting、dimen、color和style 等等,另外项目中的 Activity 主题必须定义在 Common中,方便和 BaseActivity 配合保持整个Android应用的界面风格统一。
下面是一份 Common功能组件的 build.gradle文件:
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
//Android Support
compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion"
compile "com.android.support:design:$rootProject.supportLibraryVersion"
compile "com.android.support:percent:$rootProject.supportLibraryVersion"
//网络请求相关
compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"
compile "com.squareup.retrofit2:retrofit-mock:$rootProject.retrofitVersion"
compile "com.github.franmontiel:PersistentCookieJar:$rootProject.cookieVersion"
//稳定的
compile "com.github.bumptech.glide:glide:$rootProject.glideVersion"
compile "com.orhanobut:logger:$rootProject.loggerVersion"
compile "org.greenrobot:eventbus:$rootProject.eventbusVersion"
compile "com.google.code.gson:gson:$rootProject.gsonVersion"
compile "com.github.chrisbanes:PhotoView:$rootProject.photoViewVersion"
compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion"
compile "com.github.GrenderG:Toasty:$rootProject.toastyVersion"
//router
compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion"
}
每个组件占用一个Module,如果每个module所引用的库版本不一样,将导致app存在重复的库,增加apk包大小。所以,库统一问题需要重视。
1,在工程根目录下创建config.gradle配置文件,如图:
config.gradle文件内容如下,可按项目情况添加:
ext{
android = [
compileSdkVersion: 27,
buildToolsVersion: "27.0.1",
minSdkVersion : 16,
targetSdkVersion : 27
]
dependencies = [
dagger : "2.11",
arouter: "1.1.4",
butterknife : "8.6.0"
]
// Version
supportLibrary = "27.0.1"
}
2,在工程的build.gradle中最上面使用如下一句代码导入上面的配置文件:
apply from: "config.gradle"
buildscript {
...
}
3,然后在各个模块(包括app和baselibrary)的build.gradle文件中引用,
例如业务模块A中写法如下:
if (isModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
def config = rootProject.ext
android {
compileSdkVersion config.android.compileSdkVersion
buildToolsVersion config.android.buildToolsVersion
defaultConfig {
minSdkVersion config.android.minSdkVersion
targetSdkVersion config.android.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments = [ moduleName : project.getName() ]
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
annotationProcessor "com.alibaba:arouter-compiler:${config.dependencies.arouter}"
api project(':baselib')
}
注意:当在dependence中引用时,单引号需改为双引号。
其中baselib为各个模块公共的基础library,不需要独立运行。
AndroidStudio 3.0 版本以下需将 implementation 和 api 替换成 compile 。
若使用butterknife,需将R改成R2,比如R.id.test改成R2.id.test。
app壳模块build.gradle内容如下:
apply plugin: 'com.android.application'
def config = rootProject.ext
android {
compileSdkVersion config.android.compileSdkVersion
buildToolsVersion config.android.buildToolsVersion
defaultConfig {
minSdkVersion config.android.minSdkVersion
targetSdkVersion config.android.targetSdkVersion
applicationId "com.kun.componentjava"
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
if(!isModule.toBoolean()) {
implementation project(':modulea')
implementation project(':moduleb')
}else {
// 需要依赖基础组件,因为baselib中包含主app中初始化所需的BaseApplication
implementation project(':baselib')
}
}
基础组件baselib的build.gradle文件内容示例如下:
apply plugin: 'com.android.library'
//android配置
def config = rootProject.ext
android {
compileSdkVersion config.android.compileSdkVersion
buildToolsVersion config.android.buildToolsVersion
defaultConfig {
minSdkVersion config.android.minSdkVersion
targetSdkVersion config.android.targetSdkVersion
versionCode 1
versionName "1.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
implementation 'com.jakewharton.byteunits:byteunits:0.9.1'
api "com.android.support:appcompat-v7:${config.supportLibrary}"
api "com.android.support:recyclerview-v7:${config.supportLibrary}"
api "com.jakewharton:butterknife:${config.dependencies.butterknife}"
api ('com.alibaba:arouter-api:1.2.4') { exclude module: 'support-v4' }
api 'com.android.support.constraint:constraint-layout:1.0.2'
api 'com.squareup.retrofit2:retrofit:2.2.0'
api 'com.squareup.retrofit2:converter-gson:2.2.0'
api 'com.squareup.okhttp3:logging-interceptor:3.4.1'
api 'com.squareup.okhttp3:okhttp:3.8.0'
api 'io.reactivex.rxjava2:rxjava:2.0.5'
api 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
//permission
api 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
// RxLifecycle
api 'com.trello.rxlifecycle2:rxlifecycle:2.0.1'
api 'com.trello.rxlifecycle2:rxlifecycle-android:2.0.1'
api 'com.trello.rxlifecycle2:rxlifecycle-components:2.0.1'
api 'com.zhy:base-rvadapter:3.0.3'
testImplementation 'junit:junit:4.12'
}
AndroidStudio 3.0 版本以下需将 implementation 和 api 替换成 compile 。
https://blog.csdn.net/u012572323/article/details/78840845
https://blog.csdn.net/qq_32452623/article/details/82086470
需求场景描述
在Android组件化开发中,我们一般都是将应用按功能拆分成一个个单独的业务模块进行开发(app+module01+module02+…),然后上线时再将各个模块组装到主app模块一起打包发布,这样可以很方便的进行多人协同开发,同时各个模块也是可以单独运行的,在一定程度上也提高了开发效率。
假如现在有两个模块,城市定位模块和资讯模块,在城市定位模块中我们需要用到百度地图,在资讯模块中需要用到第三方分享,而这些第三方SDK的集成一般都需要我们在自定义的Application类的onCreate方法中进行初始化操作,那么问题就来了:这些模块独有的第三方库的初始化逻辑是统一写在壳app的application的oncreate方法中呢,还是写在各自模块下的自定义application中,如果是写在壳app的自定义application中,会有一个问题,就是当这些模块作为application单独运行时,会因初始化逻辑执行不到而报错,如果是写在在不同的模块下都去执行各自的初始化,就需要进行统一整合,整合的方法如下:
1,在基础组件baselibrary(不能单独运行)中创建接口 ApplicationImpl,如:
public interface ApplicationImpl {
/**
* Module`s Application onCreate
* @param application
*/
void onCreate(BaseApplication application);
}
2,在各个业务组件module中创建实现该接口的类,如在modulea中:
public class ModuleA implements ApplicationImpl {
@Override
public void onCreate(BaseApplication application) {
// 各个模块特有的第三方库等的初始化逻辑
}
}
3,在基础组件baselib的application中实现各组件Application初始化操作(反射方法实现)
3.1,首先记录各模块module的ApplicationImpl实现类路径,在baselib下创建类ModuleConfig,如下:
public class ModuleConfig {
public static final String[] MODULESLIST =
// 配置各个module中ApplicationImpl接口的实现类的全路径(包名.类名)
{"com.gpf.modulea.ModuleA",
"com.gpf.moduleb.ModuleB"};
}
3.2,在基础组件baselib库下创建BaseApplication类,通过反射方法找到各模块初始化逻辑所在的类(对应modulesApplicationInit方法)调用其onCreate方法,最后在BaseApplication的onCreate方法中调用modulesApplicationInit()方法,将各个模块的初始化逻辑进行统一整合,完整代码如下:
public class BaseApplication extends Application {
private static BaseApplication instance;
@Override
public void onCreate() {
super.onCreate();
instance = this;
if (BuildConfig.DEBUG) {
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(this); // 尽可能早,推荐在Application中初始化
modulesApplicationInit();
}
public static BaseApplication getInstance(){
return instance;
}
public static Context getContext() {
return getInstance().getApplicationContext();
}
private void modulesApplicationInit(){
for (String moduleImpl : ModuleConfig.MODULESLIST){
try {
Class<?> clazz = Class.forName(moduleImpl);
Object obj = clazz.newInstance();
if (obj instanceof ApplicationImpl){
((ApplicationImpl) obj).onCreate(this);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
}
3.3,然后在壳模块app的清单文件中application节点下配置name为baselib中的baseapplication的路径,如下:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gpf.zujian">
<application
android:name="com.gpf.baselib.base.BaseApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme" />
</manifest>
同时各module作为application单独运行时的清单文件中name也指向BaseApplication。
通常情况下我们的组件化架构的依赖关系是这样的,壳组件app中同时依赖了模块组件modulea和moduleb,而在modulea和moduleb中都依赖了基础组件baselib,就相当于在壳app组件中重复依赖了基础组件baselib,这样会不会出什么问题呢?
组件只是我们在代码开发阶段中为了方便叫的一个术语,在组件被打包进APP的时候是没有这个概念的,这些组件最后都会被打包成arr包,然后被app壳工程所依赖,在构建APP的过程中Gradle会自动将重复的arr包排除,APP中也就不会存在相同的代码了;
但是虽然组件是不会重复了,但是我们还是要考虑另一个情况,我们在build.gradle中compile的第三方库,例如AndroidSupport库经常会被一些开源的控件所依赖,而我们自己一定也会compile AndroidSupport库 ,这就会造成第三方包和我们自己的包存在重复加载,解决办法就是找出那个多出来的库,并将多出来的库给排除掉,而且Gradle也是支持这样做的,分别有两种方式:根据组件名排除或者根据包名排除,下面以排除support-v4库为例:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") {
exclude module: 'support-v4'//根据组件名排除
exclude group: 'android.support.v4'//根据包名排除
}
}
因为我们拆分出了很多业务组件和功能组件,在把这些组件合并到“app壳工程”时候就有可能会出现资源名冲突问题,例如A组件和B组件都有一张叫做“ic_back”的图标,这时候在集成模式下打包APP就会编译出错,解决这个问题最简单的办法就是在项目中约定资源文件命名规约,比如强制使每个资源文件的名称以组件名开始,这个可以根据实际情况和开发人员制定规则。当然了万能的Gradle构建工具也提供了解决方法,通过在在组件的build.gradle中添加如下的代码:
//设置了resourcePrefix值后,所有的资源名必须以指定的字符串做前缀,否则会报错。
//但是resourcePrefix这个值只能限定xml里面的资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名。
resourcePrefix "module1_"
但是设置了这个属性后有个问题,所有的资源名必须以指定的字符串做前缀,否则会报错,而且resourcePrefix这个值只能限定xml里面的资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名;
组件化项目的Java代码混淆方案采用在集成模式下集中在app壳工程中混淆,各个业务组件不配置混淆文件。集成开发模式下在app壳工程中build.gradle文件的release构建类型中开启混淆属性,其他buildTypes配置方案跟普通项目保持一致,Java混淆配置文件也放置在app壳工程中,各个业务组件的混淆配置规则都应该在app壳工程中的混淆配置文件中添加和修改。
之所以不采用在每个业务组件中开启混淆的方案,是因为 组件在集成模式下都被 Gradle 构建成了 release 类型的arr包,一旦业务组件的代码被混淆,而这时候代码中又出现了bug,将很难根据日志找出导致bug的原因;另外每个业务组件中都保留一份混淆配置文件非常不便于修改和管理,这也是不推荐在业务组件的 build.gradle 文件中配置 buildTypes (构建类型)的原因。