说明
非模块划分有一种方法,
模块划分则2种方法 ,非模块划分是指一个项目,编译时条件删除对应的代码,资源, 在前期需要快速交付时可以使用,即不同模块先划分包名文件夹,然后拦截编译过程删除指定代码从而实现不打包具体代码 ,但是后期优化则应该转换 抽取为具体模块。
具体描述
a b c 公司都有 登录 ,首页 ,应用模块,
其区别在于首页的tab不一样,功能展示不一样,
但是a公司 首页tab没有消息,推送, b公司则有推送,但是没有对应的业务模块如 分箱之类的
但是 c公司 要推送,要 分箱,都要
tab中的应用tab,对应了所有的模块,这些模块的读取 图标,文字都是本地的,而不是通过接口获取的,
假设 tab中的应用中包含了 a公司需要考勤模块 b公司需要打卡模块 ,d公司 分别都需要,
这tab中的ApplistFragment应该放哪里呢? 是各自实现还是根据channel判断?如果 是a ,b,c公司都使用application不用分渠道法,则需要通过实现接口法来做。
第一种模块划分法
base 模块 : util,http, 基础ui 颜色,主题 (library)
ui模块:登录 闪屏 ui模块引入base模块 (library)
a公司 创建application类型,然后 引入 ui模块 , 打包成apk
b公司 创建application类型,然后 引入 ui模块 ,打包成apk
拦截一些tab之类的 实现
但是假设 a 公司需要 分箱 b公司也需要分箱,则需要把分箱再创建出一个模块 ,然后其应用类型分别需要引入这个分箱模块(分箱包括了界面,act,颜色,逻辑自然也引入了ui,base模块)
对于a,b 公司都用到了考勤, 但是a公司需要打卡 b公司不需要,而且有一个界面就是展示了所有入口,则需要再这个界面
写不同的代码实现 ,
在mainactivity .new AppListtab()的时候 拦截new AppListtab 返回一个自己公司对应的tab实现,
这种方法感觉坑少一些,channel大法,其实并不香,对于交叉引用的逻辑难以处理。
需要每一个公司在每一个公司对应的application的gradle中需要引入一个或者多个 模块 ,然后代码写这些展示代码
第二种模块划分法
base 模块 util,http, 基础ui 颜色,主题 (library)
app : 登录 闪屏 ui模块引入base模块 (application)
根据 channel判断不同的类型引入不同的实现
拦截一些tab之类的根据channel判断
假设使用的是channel 法类似于c语言的条件编译,java没有条件编译,某一些引入了 不需要的模块,只是通过判断 不展示是不行的,而是彻底的不导入,则需要用compileOnly法实现
这样打包后 对应的模块并没打包进去,但是这个逻辑代码是一直存在的,通过判断channel不执行这句话也不会报错。
但是对于不同模块的drawable资源,分离不进行打包就太难了,需要伪造一个R 类 然后 compileOnly实现,
databind则坑更多,compileOnly 引入的databind模块,也会添加进去,这导致了classdefined,所以compileOnly是不能直接引入databind模块,
只能把application中用到的,自己定义一个模块然后创建一个假的 类,然后使用compileOnly实现。
或者是用多channel ,指定同一个包名,切换任意buildVariant都能识别到
总结:第二种方法
compileOnly法坑太多了,假设 打卡和 考勤都引入了 base(util,http,theme)但是使用了databind,就会导致会直接合并,这是databind的bug.
第三种
def moduleSrcDirs = [
"accept", "demo", "manager", "webview", "misc", "product", 'quality'
]
sourceSets {
main {
jniLibs.srcDirs = ['libs']
// exclude 'schemaorg_apache_xmlbeans/**'
res.srcDirs = ['src/main/res', "src/vip/res"]
moduleSrcDirs.forEach {//下面这些是等待重构的代码
res.srcDirs += 'src/main/mymodule/' + it + '/res'
java.srcDirs += 'src/main/mymodule/' + it + '/java'
}
}
test2 {
manifest.srcFile 'src/main/java/AndroidManifest.xml'
jniLibs.srcDirs = ['libs']
res.srcDirs = ['src/main/res', "src/rlkm/res"]
moduleSrcDirs.forEach {
res.srcDirs += 'src/main/mymodule/' + it + '/res'
java.srcDirs += 'src/main/mymodule/' + it + '/java'
}
}
}
如果是已经写好的很庞大的app,
不区分模块,全部在application节点,但是如果要重构模块,为了降低成本和可立即交付建议先给不同模块划分到不同的包名,
使用gradle配置指定多个java 路径 多个res路径,
不同的模块功能类逻辑分不同的包名路径,比如 a模块放到com.example.module.a下 b则放到com.example.module.b下。
基于channel 拦截编译过程 根据channel删掉文件夹编译出来的class,有点类似第二种,但是这只能干掉java代码,布局资源是很难区分干掉了,除非定义一种命名再拦截,但是颜色,文字呢?
这种方法需要每次强制关闭 UP-TO-DATE 也就不能用快速缓存编译 ,坑也很多。
不过这种改造成本最低的,对于前期并没有划分模块的人来说偷懒是很实用的,虽然图片,资源打包进去了,至少 代码模块没有打包进去,
这样强制干掉class的效果也类似于compileOnly,结合了channel逻辑判断,实现不执行那句被干掉的代码运行的时候就不会报错。
另外我只研究出来了 拦截java代码删除的逻辑,但是并没有找到res 布局 文件删除的逻辑,
variant.javaCompileProvider.configure {
it.doLast {
var myprovider = variant.javaCompileProvider.get()
print("build dir ${myprovider.destinationDir}\n");
if (CHANNEL.XX == DEPEND_CHANNEL && isRelease) {
moduleSrcDirs.forEach {
if (it.toString() == "splitmerge"
|| it.toString() == "print"
|| it.toString() == "product"
|| it.toString() == "zxing"
) {
print("keep module " + it.toString() + "\n");
} else {
String currentDeleteDir = new File(myprovider.destinationDir, '/module/' + it + '')
// String currentDeleteDir = new File(variant.javaCompile.destinationDir, '/module/' + it + '')
File file1 = new File(currentDeleteDir)
if (file1.exists()) {
file1.deleteDir();
print("delete dir $currentDeleteDir succ!\n")
} else {
print("delete dir $currentDeleteDir fail no exist\n")
}
}
}
}
由于删 除了文件夹,导致再次编译会出现错误为了忽略todo 需要强制刷新
//忽略变体渠道
variantFilter { variant ->
def names = variant.flavors*.name
// To check for a certain build type, use variant.buildType.name == "<buildType>"
if (names.contains("XX") && names.contains("debug")) {
// Gradle ignores any variants that satisfy the conditions above.
setIgnore(true)
}
}
//强制更新的逻辑
//设置强制 更新
gradle.taskGraph.whenReady { taskGraph ->
def tasks = taskGraph.getAllTasks()
tasks.each {
def taskName = it.getName()
if (isRelease && DEPEND_CHANNEL != CHANNEL.DEFAULT) {
if (taskName == 'compileScReleaseJavaWithJavac' || taskName == 'processScReleaseMainManifest') {
print("found task $taskName\n")
System.err.println("${DEPEND_CHANNEL} Found release $taskName needReleaseBuld")
it.setOnlyIf { true }
it.outputs.upToDateWhen { false }
}
}
}
}
删除资源
applicationVariants.all { variant ->
variant.getMergeAssetsProvider().configure {
it.doLast {
var getMergeAssetsProvider = variant.getMergeAssetsProvider().get()
System.err.println("getMergeAssetsProvider xsb :${getMergeAssetsProvider.variantName}");
//incrementalFolder
System.err.println("delete dir xsb :${getMergeAssetsProvider.incrementalFolder}");
delete(fileTree(dir: getMergeAssetsProvider.incrementalFolder, includes: ['*.zip', "'*.xsb'"]))
System.err.println("getMergeAssetsProvider xsb :${getMergeAssetsProvider.incrementalFolder}");
}
}
}
build.gradle中根据channel生成变量
···
enum CHANNEL {
XX1, DEFAULT, XX2
}
···
def isRelease = false;
//android.buildTypes.release.ndk.debugSymbolLevel = {SYMBOL_TABLE |FULL }
def DEPEND_CHANNEL = CHANNEL.XX2// CHANNEL.DEFAULT
gradle.startParameter.getTaskNames().each { task ->
if (task.toLowerCase().contains("test1")) {
DEPEND_CHANNEL = CHANNEL.XX1
System.err.println(" current :XX1 kemi channel ${task}")
} else if (task.toLowerCase().contains("test2")) {
DEPEND_CHANNEL = CHANNEL.XX2
System.err.println(" current :XX2 channel ${task}")
} else {
DEPEND_CHANNEL = CHANNEL.DEFAULT
System.err.println(" current :default channel ${task}")
}
if (task.toLowerCase().contains("release")) {
isRelease = true;
}
}
定义channel
flavorDimensions 'myflavor'
productFlavors {
test1 {
manifestPlaceholders = [
JPUSH_PKGNAME: "com.example.xxx",
JPUSH_APPKEY : "xxx",
JPUSH_CHANNEL: "test",
//JPush 上注册的包名对应的 Appkey.
GETUI_APPID : xx.ext.GETUI_APPID,
test_CHANNEL : "test123"]
// manifestPlaceholders = [GETUI_APPID: rootProject.ext.GETUI_APPID, test_CHANNEL: "test"]
buildConfigField("String", "CHANNEL", "\"test\"")
buildConfigField("int[]", "MODULES", "new int[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13}")
buildConfigField("String[]", "MODULES_NAME", """new String[]{"all"}""")
resValue "string", "test_channel", "teststr"
buildConfigField("String", "REGCODE", "\"aaa\"")
dimension 'example'
applicationId "com.example.testmes"
// applicationIdSuffix '.ui'
versionNameSuffix "-iview"
}
}
test2{
}
}
}
总结:
第二种 第一种重构的成本太大,不过第一种和第二种都是可以切换的,前提都是需要把模块功能再次细分抽离出模块,到时候从第二种变更第一种问题应该也不是很大。
但是第二种真的坑太多了,主要的坑是主应用 是ab公司一个代码,大师a引用了 a模块 b公司引用了b模块的代码 ,假设 a公司不需要b模块,通过impl控制大法会导致识别不到类编译过不了,
compileOnly大法则不能有databind存在的情况,改正compileOnly则导致databind bug, 把代码引入进去了, 所以我另外创建一个模块作为接口类,但是这个需要每一个方法都定义依然有很多bug,所以我现在是尽量避免使用这种方法,而是使用每一个channel定义一个实现类方法
,直接定义一个base_interface接口模块,专门解决找不到的类的情况,然后伪造 一些找不到的类,但是 对于如R,BR类,但是如果方法太多的话很容易反复反复编译出错警告,所以工作量大就难搞了。
图标,文字无法分离,如果要分离也需要自己定义一个, 所以这里的工作量是最大的,后面偷懒就只能让图标文字也打包进去,否则需要伪造图标id类 到base_interface接口模块来解决编译找不到类的情况。
结果发现还是有很多问题,最后我不得已,给每一个channel创造一个模块impl, 而 ModuleManager 类只是一个抽象类
public static ModuleManager getInstance() {
if (moduleManager == null) {
synchronized (ModuleManager.class) {
if (moduleManager == null) {
try {
Class<?> aClass = Class.forName("channel.ModuleManagerImpl");
moduleManager= (ModuleManager) aClass.newInstance();
} catch (Throwable e) {
e.printStackTrace();
throw new RuntimeException("not found moduleManager impl!");
}
// moduleManager = new ModuleManagerImpl();
}
}
}
return moduleManager;
}
public abstract boolean createModuleMenu(ArrayList<SubMenuItemI> data, int classid, boolean match, boolean addTitle) ;
这样我的图标在不同的模块下也不影响,因为基于某个channel的引用肯定是能找到对应图标引用的。
另外如果模块太多太细,第一种还是第二种都很累,需要反复定义过多的模块,图标,文字, 判断 引用
而且还要交叉的情况出现,如二维码模块和打印模块,某些模块不需要某些需要,交叉很难搞的,这时候用第三种方法或许不错,在编译过程中一刀切直接干掉某个目录,但是第三种方法
只适合代码全部在一个模块的,不然每一个模块的拦截似乎需要再每一个模块写代码实现
除了代码的坑还有资源Id的坑,假设 资源R.string.app_name要让3个都能访问到,在3个里面定义,如果安装官方的优化就会掉坑里,
因为每一个都是隔离不共享同一个名称。
另外对于交叉代码逻辑 channel
下面是使用多个类实现不同的appmodulelist完整代码
public abstract class ModuleManager {
public static ModuleManager getInstance() {
if (moduleManager == null) {
synchronized (ModuleManager.class) {
if (moduleManager == null) {
try {
Class<?> aClass = Class.forName("channel.ModuleManagerImpl");
moduleManager= (ModuleManager) aClass.newInstance();
} catch (Throwable e) {
e.printStackTrace();
throw new RuntimeException("not found moduleManager impl!");
}
// moduleManager = new ModuleManagerImpl();
}
}
}
return moduleManager;
}
public static ModuleManager moduleManager;
@NotNull
public abstract String getClientID();
public abstract void onAgreeAgreement(Context context);
// JCollectionAuth.setAuth(context,true);
public abstract void initPush(Context context) ;
public abstract boolean createModuleMenu(ArrayList<SubMenuItemI> data, int classid, boolean match) ;
public void writeExcelFromModel(ArrayList<TableModel> tableModels, String absolutePath) {
WriteExcelUtils.writeExcelFromModel(tableModels,absolutePath);;//sc的报表也有写xls,
}
public abstract boolean isRuiliChannel();
public abstract boolean isVipChannel();
public abstract int[] getModules();
public abstract ArrayList<ModuleMenu> getModulesGroup();
public abstract void jumpModulePage(FragmentActivity activity, SubIconMenuBean menuBean);
public List<? extends SubMenuItemI> getCommonlyUseModuleFromDb() {
return SCUtil.getCommonlyUseModuleFromDb();
}
public abstract List getAllReportItem();
public abstract void onLogin(JSONObject jsonObject);
/**
* 首页tab 不同渠道不同的tab, 有的channel没有消息列表,没有首页,根据id查找返回fragment
* @param navigation_app
* @return
*/
public abstract FragmentUtil.PairX<String, SoftReference<Fragment>> createTabById(int navigation_app);
}
而ModuleManagerImpl 在每一个渠道是每一个Application模块定义 不同的java/channel的实现即可。
依赖判断的2种方法
根据if 或者 直接渠道名Api 也可以
testApi project(path: ':print')
test2Api(project(path: ':upmaterial'))
if (DEPEND_CHANNEL == CHANNEL.TEST1) {
compileOnly project(path: ':third_xc_chartlib')
}
但是 是有区别的,使用if 条件 implement和 渠道名implement的区别是 前者的情况假设A.class被另外一个if条件引用了,但是没走那个逻辑,调用那个类却没提示红色错误,只有编译运行才报错,这不符合美观,而使用后者则很明显的看出来它找不到改类。
布局不生效,有可能是多channel,的问题,想要生效就把这个channel顺序调整为第一个,
重构分模块呢,第三步必须有,
也就是子同一个项目 先把模块放到不同的包,利用开发工具的重构 方法重构,把交叉的公共代码分离出来,
这样建立模块的时候保持路径不变,这样合并起来也不会乱。,比如 消息列表的模块我弄到一个文件夹了,然后我新建一个模块,按文件夹路径 直接移动过去,直接运行会报错的,利用报错提示一步一步处理就ok了。
关于主题部分,抽取到base,关于appcontext可以 在base里面 定义一个,然后 应用 里面继承它就行了。
getInstance()的套路代码
public class SuperContext extends Application {
protected static SuperContext SuperContext;
protected Handler handler;
public android.os.Handler getHandler() {
return handler;
}
public static SuperContext getInstance() {
return SuperContext;
}
@Override
public void onCreate() {
super.onCreate();
SuperContext = this;
handler = new Handler();
}
}
可以慢慢的把东西弄到第二层,但是又可以随时交付不包含代码的app,
分模块有分模块的弊端,分模块后,混淆配置可能需要每一个模块都引用这个混淆配置了z
指定目录的写法,把它复制到每一个模块即可
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), new File(getRootDir().absolutePath+"/app",'proguard-rules.pro')
下面是完整build.gradle
import java.text.SimpleDateFormat
plugins {
//noinspection DuplicatePlatformClasses
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'com.huawei.agconnect'
// id 'kotlin-android-extensions'
// id 'kotlin-parcelize'
}
enum CHANNEL {
RUILI, DEFAULT, LZ
}
def isRelease = false;
//android.buildTypes.release.ndk.debugSymbolLevel = {SYMBOL_TABLE |FULL }
def DEPEND_CHANNEL = CHANNEL.LZ// CHANNEL.DEFAULT
gradle.startParameter.getTaskNames().each { task ->
if (task.toLowerCase().contains("test1")) {
DEPEND_CHANNEL = CHANNEL.RUILI
System.err.println(" current :xxx kemi channel ${task}")
} else if (task.toLowerCase().contains("test")) {
DEPEND_CHANNEL = CHANNEL.LZ
System.err.println(" current :test channel ${task}")
} else {
DEPEND_CHANNEL = CHANNEL.DEFAULT
System.err.println(" current :default channel ${task}")
}
if (task.toLowerCase().contains("release")) {
isRelease = true;
}
}
android {
flavorDimensions 'myflavor'
productFlavors {
vip {
manifestPlaceholders = defaultConfig.manifestPlaceholders + [
// GETUI_APPID : rootProject.ext.GETUI_APPID,
LZ_CHANNEL : "vip",
JPUSH_PKGNAME: "com.example.mytest",
JPUSH_APPKEY : "xxxxxxx",
JPUSH_CHANNEL: "test_vip",
]
buildConfigField("String", "CHANNEL", "\"vip\"")
buildConfigField("int[]", "MODULES", "new int[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13}")
buildConfigField("String[]", "MODULES_NAME", """new String[]{"all"}""")
resValue "string", "test_channel", "标准版"
buildConfigField("String", "REGCODE", "\"\"")
buildConfigField("String", "LOGINURL", "\"http://192.168.1.1\"")
buildConfigField("String", "LOGINURL2", "\"http://192.168.1.70\"")
applicationId "com.example.mytest"
dimension 'example'
}
test1 {
buildConfigField("String", "REGCODE", "\"mycode\"")
manifestPlaceholders = defaultConfig.manifestPlaceholders + [GETUI_APPID : rootProject.ext.GETUI_APPID, LZ_CHANNEL: "test1",
JPUSH_PKGNAME: "com.example.mytest",
JPUSH_APPKEY : "xxxxxxx",
JPUSH_CHANNEL: "test",
]
buildConfigField("String", "LOGINURL", "\"http://192.168.1.2:8086\"")
buildConfigField("String", "LOGINURL2", "\"http://192.168.1.1\"")
resValue "string", "test_channel", "tt定制版"
// manifestPlaceholders = [LZ_CHANNEL: "xxx"]
buildConfigField("String", "CHANNEL", "\"xxx\"")
buildConfigField("int[]", "MODULES", "new int[]{0,1,2,3}")
buildConfigField("String[]", "MODULES_NAME", """new String[]{"box","split","product"}""")
dimension 'example'
applicationIdSuffix '.xxx'
versionNameSuffix "-xxx"
}
test {
manifestPlaceholders = [
JPUSH_PKGNAME: "com.example.mytest",
JPUSH_APPKEY : "xxxxxxx",
JPUSH_CHANNEL: "test",
//JPush 上注册的包名对应的 Appkey.
GETUI_APPID : rootProject.ext.GETUI_APPID,
LZ_CHANNEL : "test123"]
// manifestPlaceholders = [GETUI_APPID: rootProject.ext.GETUI_APPID, LZ_CHANNEL: "test"]
buildConfigField("String", "CHANNEL", "\"test\"")
buildConfigField("int[]", "MODULES", "new int[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13}")
buildConfigField("String[]", "MODULES_NAME", """new String[]{"all"}""")
resValue "string", "test_channel", "testAPP"
buildConfigField("String", "REGCODE", "\"test123\"")
buildConfigField("String", "LOGINURL", "\"http://192.168.1.1\"")
buildConfigField("String", "LOGINURL2", "\"http://192.168.1.2\"")
dimension 'example'
applicationId "com.example.mytest"
// applicationIdSuffix '.ui'
versionNameSuffix "-iview"
}
}
/* packageOptions {
exclude ['testhemaorg_apache_xmlbeans']
}*/
signingConfigs {
release {
storeFile file("example_xxx.jks")
storePassword "xxxx"
keyAlias "xxxx"
keyPassword "xxx"
}
}
namespace 'com.example.app'
configurations { all { exclude module: 'httpclient' exclude module: 'commons-logging' } }
compileSdk 31
defaultConfig {
applicationId "com.example.app"
minSdk 21
targetSdk 28
versionCode 3
versionName rootProject.ext.versionName
manifestPlaceholders = [
GETUI_APPID : rootProject.ext.GETUI_APPID,
APP_LZHEME : "example",
MEIZU_APPKEY : "MZ-xx",
MEIZU_APPID : "MZ-xx",
XIAOMI_APPID : "MI-xx",
XIAOMI_APPKEY : "MI-xx",
OPPO_APPKEY : "OP-xx",
OPPO_APPID : "OP-xx",
OPPO_APPSECRET: "OP-xx",
VIVO_APPKEY : "xx",
VIVO_APPID : "xx"
]
resValue "string", "channel_hard", "keep"
buildConfigField "String", "BUILD_TIME_STR", "\"" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis()) + "\""
buildConfigField("String", "VERSION_NAME", "\"${versionName}\"")
buildConfigField("boolean", "DEBUG_", "false")
buildConfigField("String", "SERVER_URL", "\"${rootProject.ext.SERVER_URL}\"")
//请勿在线上版本配置MASTERSECRET的值,避免泄漏
buildConfigField("String", "MASTERSECRET", "\"\"")
buildConfigField("String", "APPKEY", "\"${rootProject.ext.GETUI_APP_KEY}\"")
ndk {
abiFilters "arm64-v8a", "armeabi-v7a", "x86"
// abiFilters "armeabi", /**/"armeabi-v7a", "x86_64", "x86"
}
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
sourceSets { main { assets.srcDirs = ['src/main/assets'] } }
//指定room.testhemaLocation生成的文件路径 修复警告: Schema export directory is not provided to the annotation processor so we cannot export the testhe 错误,
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.testhemaLocation": "$projectDir/testhemas".toString()]
}
}
}
buildTypes {
debug {
aaptOptions {
ignoreAssetsPattern "!testhemaorg_apache_xmlbeans"
}
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
minifyEnabled false
signingConfig signingConfigs.release
}
release {
aaptOptions {
ignoreAssets '*.xsd'
ignoreAssetsPattern "!testhemaorg_apache_xmlbeans"
}
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
/* sourceCompatibility JavaVersion.VERSION_1_9
targetCompatibility JavaVersion.VERSION_1_9*/
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures {
viewBinding true
dataBinding true
}
def moduleSrcDirs = [
"accept", "application", "demo", "manager", "material", "webview", "mitest", "print", "product", 'quality', "splitmerge", "zxing"
]
sourceSets {
main {
jniLibs.srcDirs = ['libs']
// exclude 'testhemaorg_apache_xmlbeans/**'
res.srcDirs = ['src/main/res', "src/vip/res"]
moduleSrcDirs.forEach {
res.srcDirs += 'src/main/java/module/' + it + '/res'
}
}
test1 {
manifest.srcFile 'src/main/java/AndroidManifest.xml'
jniLibs.srcDirs = ['libs']
res.srcDirs = ['src/main/res', "src/test1/res"]
moduleSrcDirs.forEach {
res.srcDirs += 'src/main/java/module/' + it + '/res'
}
}
test {
manifest.srcFile 'src/main/java/AndroidManifest.xml'
jniLibs.srcDirs = ['libs']
res.srcDirs = ['src/main/res', "src/test/res"]
moduleSrcDirs.forEach {
res.srcDirs += 'src/main/java/module/' + it + '/res'
}
}
}
lintOptions {
abortOnError false
}
//设置强制 更新
gradle.taskGraph.whenReady { taskGraph ->
def tasks = taskGraph.getAllTasks()
tasks.each {
def taskName = it.getName()
if (isRelease && DEPEND_CHANNEL != CHANNEL.DEFAULT) {
if (taskName == 'compileScReleaseJavaWithJavac' || taskName == 'processScReleaseMainManifest') {
print("found task $taskName\n")
System.err.println("${DEPEND_CHANNEL} Found release $taskName needReleaseBuld")
it.setOnlyIf { true }
it.outputs.upToDateWhen { false }
}
}
}
}
//忽略变体渠道
variantFilter { variant ->
def names = variant.flavors*.name
// To check for a certain build type, use variant.buildType.name == "<buildType>"
if (names.contains("test1") && names.contains("debug")) {
// Gradle ignores any variants that satisfy the conditions above.
setIgnore(true)
}
}
applicationVariants.all { variant ->
variant.getMergeAssetsProvider().configure {
it.doLast {
var getMergeAssetsProvider = variant.getMergeAssetsProvider().get()
System.err.println("getMergeAssetsProvider xsb :${getMergeAssetsProvider.variantName}");
//test1Debug
//incrementalFolder
System.err.println("delete dir xsb :${getMergeAssetsProvider.incrementalFolder}");
delete(fileTree(dir: getMergeAssetsProvider.incrementalFolder, includes: ['*.zip', "'*.xsb'"]))
System.err.println("getMergeAssetsProvider xsb :${getMergeAssetsProvider.incrementalFolder}");
}
}
// variant.javaCompileProvider
variant.javaCompileProvider.configure {
it.doLast {
// JavaCompile javaCompile=null;
// if (variant.hasProperty('javaCompileProvider')) {
//android gradle 3.3.0 +
var myprovider = variant.javaCompileProvider.get()
/* } else {
javaCompile = variant.javaCompile
}*/
print("build dir ${myprovider.destinationDir}\n");
// String[] deleteDir = [];
if (CHANNEL.RUILI == DEPEND_CHANNEL && isRelease) {
moduleSrcDirs.forEach {
if (it.toString() == "splitmerge"
|| it.toString() == "print"
|| it.toString() == "product"
|| it.toString() == "zxing"
) {
print("keep module " + it.toString() + "\n");
} else {
String currentDeleteDir = new File(myprovider.destinationDir, '/module/' + it + '')
// String currentDeleteDir = new File(variant.javaCompile.destinationDir, '/module/' + it + '')
File file1 = new File(currentDeleteDir)
if (file1.exists()) {
file1.deleteDir();
print("delete dir $currentDeleteDir succ!\n")
} else {
print("delete dir $currentDeleteDir fail no exist\n")
}
}
}
} else if (CHANNEL.LZ == DEPEND_CHANNEL && isRelease) {
moduleSrcDirs.forEach {
if (it.toString() == "application"
|| it.toString() == "mitest"
|| it.toString() == "webview"
) {
print("keep module " + it.toString() + "\n");
} else {
String currentDeleteDir = new File(myprovider.destinationDir, '/module/' + it + '')
File file1 = new File(currentDeleteDir)
if (file1.exists()) {
file1.deleteDir();
print("delete dir $currentDeleteDir succ!\n")
} else {
print("delete dir $currentDeleteDir fail no exist\n")
}
}
}
} else {
print("KEEP CHANNEL MODULE DIR $DEPEND_CHANNEL")
}//字符串加密
if ("${myprovider.destinationDir}".toLowerCase().contains("release")) {
println("start classes obfutestation " + "${myprovider.destinationDir}")
try {
javaexec {
setDefaultCharacterEncoding("utf-8")//这里传错会导致解密出现问题,
main("-jar")
args(
"../obfuseStringGradle.jar",
project.name,
myprovider.destinationDir,
"../ignore_class.txt",
ENCRYPT_CONFIG_JSON
)
}
} catch (e) {
e.printStackTrace()
println("exec encrypt fail.. " + "${e.getMessage()}")
}
} else {
println("ignore class obfutestation " + "${myprovider.destinationDir}")
}
}
}
variant.outputs.each { output -> //every thing example\\app\\build\\.*?AndroidManifest.xml
output.processManifestProvider.configure {
it.doLast {
/* print("getxx"+output.processManifestProvider.get().singleVariantOutput);
print("getxx"+output.processManifestProvider.get().mainMergedManifest.name);
print("getxx"+output.processManifestProvider.get().multiApkManifestOutputDirectory.name);*/
// def cxx=output.processManifestProvider.get().mainManifest;
// print(cxx);
//C:\project\example\app\build\intermediates\merged_manifest\testDebug
def manifestPath = new File("${buildDir}/intermediates/bundle_manifest/${variant.dirName}/process${variant.dirName}Manifest/bundle-manifest/AndroidManifest.xml")
if (!manifestPath.exists()) {
manifestPath = new File("${buildDir}/intermediates/merged_manifest/${output.processManifestProvider.get().variantName}/AndroidManifest.xml")
}
print("do clean manifest axml ${manifestPath}");
// def manifestPath = "";//"""${manifestOutputDirectory.get()}/AndroidManifest.xml"
// def manifestPath = "${manifestOutputDirectory}/AndroidManifest.xml"
// String manifestPath = "$it.manifestOutputDirectory/AndroidManifest.xml"
// Stores the contents of the manifest.
def manifestContent = file(manifestPath).getText()
print("manifestPath:" + manifestPath)
// Changes the version code in the stored text.
manifestContent = manifestContent.replace('testtest', "fffshit")
// Overwrites the manifest with the new text.
file(manifestPath).write(manifestContent)
}
}
}
}
}
dependencies {
compileOnly 'com.android.tools.build:gradle:7.1.2'
implementation project(path: ':third_picture_library')//测试查看源码
implementation 'pub.devrel:easypermissions:3.0.0' //@depanre
vipApi project(path: ':print')
// testRuntimeOnly(project(path: ':print'))
// testCompileOnly(project(path: ':print'))//bug : Didn't find class "com.example.print.DataBinderMapperIm
test1Api project(path: ':print')
vipApi(project(path: ':upmaterial'))
// testCompileOnly(project(path: ':upmaterial'))
// test1CompileOnly project(path: ':upmaterial')
if (DEPEND_CHANNEL == CHANNEL.RUILI) {
/* compileOnly 'cn.jiguang.sdk:jpush:4.6.3' // 此处以JPush 4.5.0 版本为例。
compileOnly 'cn.jiguang.sdk:jcore:3.1.2' // 此处以JCore 3.1.2 版本为例。
compileOnly 'com.getui:gtsdk:3.2.2.0' //个推SDK
compileOnly 'com.getui:gtc:3.1.4.0' //个推核心组件
*/
compileOnly project(path: ':third_xc_chartlib')
} else if (DEPEND_CHANNEL == CHANNEL.LZ) {
print("test channel--------")
compileOnly project(path: ':base_interface')
implementation project(path: ':push_table')
} else {
compileOnly project(path: ':base_interface')
implementation project(path: ':push_table')
}
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
implementation('com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3') {
transitive false //阻断里面的okhttp3依赖
}
implementation files('libs/poishadow-all.jar') {
// exclude module:'*.xsb'
}
//room
implementation deps.room.runtime
implementation deps.room.rxjava3
kapt deps.room.compiler
api project(path: ':base')
api project(path: ':base_mes')
implementation "androidx.startup:startup-runtime:1.1.0"
configurations.all {
resolutionStrategy {
}
}
}
子module的写法
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-kapt'
}
android {
//包含通知 推送 ,审批?
namespace 'com.xx.notifiction'
compileSdk 32
defaultConfig {
minSdk 21
targetSdk 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
res.srcDirs = ["src/main/java/module/application/res", "src/main/res"]
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
dataBinding true
}
}
dependencies {
api(project(path: ':base')) {
exclude module: ':base_interface'
}
api(project(path: ':push_table'));
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
子模块的src/main/java/module/application/res本来是在主应用中的,我是把application文件夹直接移动过来了,这样重构省事,不乱,随时可以撤回去。
BuildConfig中定义字段法是比较坑爹的,比如里面存放url,但是如果分了那么多模块和databind,那么主buildconfig.class经常不生成,完全可以使用 不同channel不同的相同类 但是不同返回定义值定义法实现。
另外需要注意的是同一个application 下可以指定多个res, java,却不能manifest.xml,不然可以简单的进行分离activity清单注册整理,以后再进行模块化
模块划分完毕了还需要处理一个问题叫字符串加密,和代码混淆我刚开始用的第三种方法的,现在行不通了,
经过研究发现混淆也可以写绝对路径
每一个模块复制这句话就行,不过需要忽略开发工具给的找不到某类的提示
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), new File(getRootDir().absolutePath+"/app",'proguard-rules.pro')
}
}
关于字符串加密的问题,如果每一个模块复制这句话是很麻烦的,最后研究实现了根项目使用全局控制!
subprojects { project ->
afterEvaluate {
final boolean isAndroidLibrary =
(project.pluginManager.hasPlugin('com.android.library'))
if (isAndroidLibrary) {
boolean ignoreEncrypt;
var moduleName=project.name;
switch (moduleName){
case "third_xc_chartlib":
case "third_print_sdk":
case "third_picture_library":
case "third_magicindicator":
case "third_form":
case "third_consecutivescroller":
ignoreEncrypt=true;
break;
default:
ignoreEncrypt=false;
break;
}
if(ignoreEncrypt){
System.err.println("library-ignore-encrypt------"+project.name);
return;
}else{
System.err.println("library-need-encrypt-str"+project.name);
}
android {
libraryVariants.all { variant ->
variant.javaCompileProvider.configure {
it.doLast {
System.err.println "module[${project.name}]Encrypt"//
var myprovider = variant.javaCompileProvider.get()
print("build dir ${myprovider.destinationDir}\n");
if ("${myprovider.destinationDir}".toLowerCase().contains("release")) {
println("start module classes obfuscation " + "${myprovider.destinationDir}")
try {
javaexec {
setDefaultCharacterEncoding("utf-8")//这里传错会导致解密出现问题,
main("-jar")
args(
"${getRootDir().absolutePath}\\obnew.jar",
project.name,
myprovider.destinationDir,
"${getRootDir().absolutePath}/ignore_class.txt",
ENCRYPT_MODULE_JSON
)
}
} catch (e) {
e.printStackTrace()
System.err.println("exec encrypt fail.. " + "${e.getMessage()}")
}
} else {
println("notReleaseApk,ignore class obfuscation " + "${myprovider.destinationDir}")
}
}
}
}
}
}else{
System.err.println("other library-------");
}
dependencies {
if (isAndroidLibrary) {
// android dependencies here
}
// all subprojects dependencies here
}
}
}
obnew.jar是加密工具类
ENCRYPT_MODULE_JSON = "{\\\"ignoreUseSimpleEncrypt\\\":true," +
"\\\"onlydebug\\\":false," +
"\\\"EncryptClassSign\\\":\\\"com/xxx/app/encrypt/no_no\\\"," +
"\\\"SimpleEncryptClassSign\\\":\\\"com/xxx/app/encrypt/no_no\\\"," +
"\\\"simpleEncryptMethod\\\":\\\"call\\\"," +
"\\\"mode\\\":\\\"str_wrap\\\"," +
"\\\"module\\\":true," +
"\\\"loglevel\\\":2," +
"\\\"dexProguard\\\":0," +
"\\\"soEncryptMethod\\\":\\\"call1\\\"}"
如果非是 aa bb的包名就跳过加密字符串
^((?!aa|bb).)*$
另外全局控制gradle task的也有如下方法
getRootProject().getAllprojects().forEach{
project->
project.getTasksByName("javaPreCompileRelease",true).forEach{
task->task.doLast{
}
}
}
/**
* 配置阶段完成以后的监听回调
*/
this.afterEvaluate {
//和 variant.javaCompileProvider.configure 一起执行
System.err.println '配置阶段执行完毕'
}
/**
* gradle 执行完毕的回调监听
*/
this.gradle.buildFinished {
System.err.println '----------执行阶段执行完毕'
}
applicationVariants.all { variant ->
variant.getMergeAssetsProvider().configur
variant.javaCompileProvider.configure
variant.outputs.each
}
上面的if else impl 对应的写法实际上还是有一些问题的,也是网上我找到的方法,但是这种方法会导致编译器在识别的时候不会报红色,什么意思呢,就是没有走这个逻辑,理论上是找不到这个类的,但是看不到详细信息, 只有在你实际编译的过程中才知道错误
那么正确的写法是什么呢?
如果是vipChannel 则应该这么写vipImplements
下面是kts的写法 而gradle是不用括号的
vipApi(project(":accept"))
vipApi(project(":quality"))
vipApi(project(":product"))
vipApi(project(":webapi"))
vipApi(project(":print"))
vipApi(project(":solider")
vipApi(project(":upmaterial"))
vipApi(project(":uisc"))
vipApi(project(":push_table"))
vipApi(project(":base_mes"))
//A公司
aaaApi(project(":product"))
aaaApi(project(":webapi"))
aaaApi(project(":base_mes"))
aaaApi(project(":print"))
//B公司只需要推送相关
bbApi(project(":push_table"))
bbApi(project(":uisc"))
上面 vip是完整版,包含了product ,也包含了 push_table,
2022-6-15 13:57:03
上面的写法是基于gradle,写法,而kts实际上都已经实现了,暂时不发布教程
网友评论