简书链接:模块化项目架构笔记高级技巧拦截所有模块实现字符串加密编译
文章字数:3787,阅读全文大约需要15分钟

说明

非模块划分有一种方法,
模块划分则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.

第三种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  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 布局 文件删除的逻辑,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 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 需要强制刷新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

//忽略变体渠道
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 }
}
}
}
}

删除资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
}
···

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 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 类只是一个抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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;
}

1
public abstract boolean createModuleMenu(ArrayList<SubMenuItemI> data, int classid, boolean match, boolean addTitle) ;

这样我的图标在不同的模块下也不影响,因为基于某个channel的引用肯定是能找到对应图标引用的。

另外如果模块太多太细,第一种还是第二种都很累,需要反复定义过多的模块,图标,文字, 判断 引用

而且还要交叉的情况出现,如二维码模块和打印模块,某些模块不需要某些需要,交叉很难搞的,这时候用第三种方法或许不错,在编译过程中一刀切直接干掉某个目录,但是第三种方法

只适合代码全部在一个模块的,不然每一个模块的拦截似乎需要再每一个模块写代码实现

除了代码的坑还有资源Id的坑,假设 资源R.string.app_name要让3个都能访问到,在3个里面定义,如果安装官方的优化就会掉坑里,
因为每一个都是隔离不共享同一个名称。

另外对于交叉代码逻辑 channel

下面是使用多个类实现不同的appmodulelist完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

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 也可以

1
2
3
4
5
6

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()的套路代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27


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
指定目录的写法,把它复制到每一个模块即可

1
2
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), new File(getRootDir().absolutePath+"/app",'proguard-rules.pro')

下面是完整build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456

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的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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清单注册整理,以后再进行模块化

模块划分完毕了还需要处理一个问题叫字符串加密,和代码混淆我刚开始用的第三种方法的,现在行不通了,
经过研究发现混淆也可以写绝对路径
每一个模块复制这句话就行,不过需要忽略开发工具给的找不到某类的提示

1
2
3
4
5
6
7
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), new File(getRootDir().absolutePath+"/app",'proguard-rules.pro')
}
}

关于字符串加密的问题,如果每一个模块复制这句话是很麻烦的,最后研究实现了根项目使用全局控制!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77


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是加密工具类

1
2
3
4
5
6
7
8
9
10
11

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的也有如下方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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是不用括号的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  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实际上都已经实现了,暂时不发布教程