說明
非模塊劃分有一種方法,
模塊劃分則2種方法 ,非模塊劃分是指一個項目,編譯時條件刪除對應(yīng)的代碼初茶,資源扎瓶, 在前期需要快速交付時可以使用所踊,即不同模塊先劃分包名文件夾,然后攔截編譯過程刪除指定代碼從而實現(xiàn)不打包具體代碼 概荷,但是后期優(yōu)化則應(yīng)該轉(zhuǎn)換 抽取為具體模塊。
具體描述
a b c 公司都有 登錄 碌燕,首頁 误证,應(yīng)用模塊,
其區(qū)別在于首頁的tab不一樣修壕,功能展示不一樣愈捅,
但是a公司 首頁tab沒有消息,推送慈鸠, b公司則有推送蓝谨,但是沒有對應(yīng)的業(yè)務(wù)模塊如 分箱之類的
但是 c公司 要推送,要 分箱青团,都要
tab中的應(yīng)用tab,對應(yīng)了所有的模塊譬巫,這些模塊的讀取 圖標(biāo),文字都是本地的督笆,而不是通過接口獲取的芦昔,
假設(shè) tab中的應(yīng)用中包含了 a公司需要考勤模塊 b公司需要打卡模塊 ,d公司 分別都需要娃肿,
這tab中的ApplistFragment應(yīng)該放哪里呢咕缎? 是各自實現(xiàn)還是根據(jù)channel判斷?如果 是a ,b,c公司都使用application不用分渠道法料扰,則需要通過實現(xiàn)接口法來做凭豪。
第一種模塊劃分法
base 模塊 : util,http, 基礎(chǔ)ui 顏色,主題 (library)
ui模塊:登錄 閃屏 ui模塊引入base模塊 (library)
a公司 創(chuàng)建application類型晒杈,然后 引入 ui模塊 嫂伞, 打包成apk
b公司 創(chuàng)建application類型,然后 引入 ui模塊 桐智,打包成apk
攔截一些tab之類的 實現(xiàn)
但是假設(shè) a 公司需要 分箱 b公司也需要分箱末早,則需要把分箱再創(chuàng)建出一個模塊 ,然后其應(yīng)用類型分別需要引入這個分箱模塊(分箱包括了界面,act,顏色说庭,邏輯自然也引入了ui,base模塊)
對于a,b 公司都用到了考勤然磷, 但是a公司需要打卡 b公司不需要,而且有一個界面就是展示了所有入口刊驴,則需要再這個界面
寫不同的代碼實現(xiàn) 姿搜,
在mainactivity .new AppListtab()的時候 攔截new AppListtab 返回一個自己公司對應(yīng)的tab實現(xiàn)寡润,
這種方法感覺坑少一些,channel大法舅柜,其實并不香梭纹,對于交叉引用的邏輯難以處理。
需要每一個公司在每一個公司對應(yīng)的application的gradle中需要引入一個或者多個 模塊 致份,然后代碼寫這些展示代碼
第二種模塊劃分法
base 模塊 util,http, 基礎(chǔ)ui 顏色变抽,主題 (library)
app : 登錄 閃屏 ui模塊引入base模塊 (application)
根據(jù) channel判斷不同的類型引入不同的實現(xiàn)
攔截一些tab之類的根據(jù)channel判斷
假設(shè)使用的是channel 法類似于c語言的條件編譯,java沒有條件編譯氮块,某一些引入了 不需要的模塊绍载,只是通過判斷 不展示是不行的,而是徹底的不導(dǎo)入滔蝉,則需要用compileOnly法實現(xiàn)
這樣打包后 對應(yīng)的模塊并沒打包進(jìn)去击儡,但是這個邏輯代碼是一直存在的,通過判斷channel不執(zhí)行這句話也不會報錯蝠引。
但是對于不同模塊的drawable資源阳谍,分離不進(jìn)行打包就太難了,需要偽造一個R 類 然后 compileOnly實現(xiàn)螃概,
databind則坑更多矫夯,compileOnly 引入的databind模塊,也會添加進(jìn)去谅年,這導(dǎo)致了classdefined,所以compileOnly是不能直接引入databind模塊茧痒,
只能把a(bǔ)pplication中用到的,自己定義一個模塊然后創(chuàng)建一個假的 類融蹂,然后使用compileOnly實現(xiàn)旺订。
或者是用多channel ,指定同一個包名,切換任意buildVariant都能識別到
總結(jié):第二種方法
compileOnly法坑太多了超燃,假設(shè) 打卡和 考勤都引入了 base(util,http,theme)但是使用了databind,就會導(dǎo)致會直接合并区拳,這是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 {//下面這些是等待重構(gòu)的代碼
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'
}
}
}
如果是已經(jīng)寫好的很龐大的app,
不區(qū)分模塊,全部在application節(jié)點意乓,但是如果要重構(gòu)模塊樱调,為了降低成本和可立即交付建議先給不同模塊劃分到不同的包名,
使用gradle配置指定多個java 路徑 多個res路徑届良,
不同的模塊功能類邏輯分不同的包名路徑笆凌,比如 a模塊放到com.example.module.a下 b則放到com.example.module.b下。
基于channel 攔截編譯過程 根據(jù)channel刪掉文件夾編譯出來的class,有點類似第二種士葫,但是這只能干掉java代碼乞而,布局資源是很難區(qū)分干掉了,除非定義一種命名再攔截慢显,但是顏色爪模,文字呢欠啤?
這種方法需要每次強(qiáng)制關(guān)閉 UP-TO-DATE 也就不能用快速緩存編譯 ,坑也很多屋灌。
不過這種改造成本最低的洁段,對于前期并沒有劃分模塊的人來說偷懶是很實用的,雖然圖片共郭,資源打包進(jìn)去了祠丝,至少 代碼模塊沒有打包進(jìn)去,
這樣強(qiáng)制干掉class的效果也類似于compileOnly,結(jié)合了channel邏輯判斷落塑,實現(xiàn)不執(zhí)行那句被干掉的代碼運(yùn)行的時候就不會報錯纽疟。
另外我只研究出來了 攔截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")
}
}
}
}
由于刪 除了文件夾,導(dǎo)致再次編譯會出現(xiàn)錯誤為了忽略todo 需要強(qiáng)制刷新
//忽略變體渠道
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)
}
}
//強(qiáng)制更新的邏輯
//設(shè)置強(qiáng)制 更新
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中根據(jù)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 上注冊的包名對應(yīng)的 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{
}
}
}
總結(jié):
第二種 第一種重構(gòu)的成本太大散吵,不過第一種和第二種都是可以切換的龙考,前提都是需要把模塊功能再次細(xì)分抽離出模塊,到時候從第二種變更第一種問題應(yīng)該也不是很大矾睦。
但是第二種真的坑太多了晦款,主要的坑是主應(yīng)用 是ab公司一個代碼,大師a引用了 a模塊 b公司引用了b模塊的代碼 枚冗,假設(shè) a公司不需要b模塊缓溅,通過impl控制大法會導(dǎo)致識別不到類編譯過不了,
compileOnly大法則不能有databind存在的情況赁温,改正compileOnly則導(dǎo)致databind bug, 把代碼引入進(jìn)去了坛怪, 所以我另外創(chuàng)建一個模塊作為接口類,但是這個需要每一個方法都定義依然有很多bug,所以我現(xiàn)在是盡量避免使用這種方法股囊,而是使用每一個channel定義一個實現(xiàn)類方法
袜匿,直接定義一個base_interface接口模塊,專門解決找不到的類的情況稚疹,然后偽造 一些找不到的類居灯,但是 對于如R,BR類,但是如果方法太多的話很容易反復(fù)反復(fù)編譯出錯警告内狗,所以工作量大就難搞了怪嫌。
圖標(biāo),文字無法分離柳沙,如果要分離也需要自己定義一個岩灭, 所以這里的工作量是最大的,后面偷懶就只能讓圖標(biāo)文字也打包進(jìn)去偎行,否則需要偽造圖標(biāo)id類 到base_interface接口模塊來解決編譯找不到類的情況川背。
結(jié)果發(fā)現(xiàn)還是有很多問題贰拿,最后我不得已,給每一個channel創(chuàng)造一個模塊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) ;
這樣我的圖標(biāo)在不同的模塊下也不影響熄云,因為基于某個channel的引用肯定是能找到對應(yīng)圖標(biāo)引用的膨更。
另外如果模塊太多太細(xì),第一種還是第二種都很累缴允,需要反復(fù)定義過多的模塊荚守,圖標(biāo),文字练般, 判斷 引用
而且還要交叉的情況出現(xiàn)矗漾,如二維碼模塊和打印模塊,某些模塊不需要某些需要薄料,交叉很難搞的敞贡,這時候用第三種方法或許不錯,在編譯過程中一刀切直接干掉某個目錄摄职,但是第三種方法
只適合代碼全部在一個模塊的誊役,不然每一個模塊的攔截似乎需要再每一個模塊寫代碼實現(xiàn)
除了代碼的坑還有資源Id的坑,假設(shè) 資源R.string.app_name要讓3個都能訪問到谷市,在3個里面定義蛔垢,如果安裝官方的優(yōu)化就會掉坑里,
因為每一個都是隔離不共享同一個名稱迫悠。
另外對于交叉代碼邏輯 channel
下面是使用多個類實現(xiàn)不同的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沒有消息列表鹏漆,沒有首頁,根據(jù)id查找返回fragment
* @param navigation_app
* @return
*/
public abstract FragmentUtil.PairX<String, SoftReference<Fragment>> createTabById(int navigation_app);
}
而ModuleManagerImpl 在每一個渠道是每一個Application模塊定義 不同的java/channel的實現(xiàn)即可创泄。
依賴判斷的2種方法
根據(jù)if 或者 直接渠道名Api 也可以
testApi project(path: ':print')
test2Api(project(path: ':upmaterial'))
if (DEPEND_CHANNEL == CHANNEL.TEST1) {
compileOnly project(path: ':third_xc_chartlib')
}
但是 是有區(qū)別的艺玲,使用if 條件 implement和 渠道名implement的區(qū)別是 前者的情況假設(shè)A.class被另外一個if條件引用了,但是沒走那個邏輯验烧,調(diào)用那個類卻沒提示紅色錯誤板驳,只有編譯運(yùn)行才報錯,這不符合美觀碍拆,而使用后者則很明顯的看出來它找不到改類若治。
布局不生效,有可能是多channel,的問題感混,想要生效就把這個channel順序調(diào)整為第一個端幼,
重構(gòu)分模塊呢,第三步必須有弧满,
也就是子同一個項目 先把模塊放到不同的包婆跑,利用開發(fā)工具的重構(gòu) 方法重構(gòu),把交叉的公共代碼分離出來庭呜,
這樣建立模塊的時候保持路徑不變滑进,這樣合并起來也不會亂犀忱。,比如 消息列表的模塊我弄到一個文件夾了扶关,然后我新建一個模塊阴汇,按文件夾路徑 直接移動過去,直接運(yùn)行會報錯的节槐,利用報錯提示一步一步處理就ok了搀庶。
關(guān)于主題部分,抽取到base,關(guān)于appcontext可以 在base里面 定義一個铜异,然后 應(yīng)用 里面繼承它就行了哥倔。
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
指定目錄的寫法,把它復(fù)制到每一個模塊即可
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", "標(biāo)準(zhǔn)版"
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 上注冊的包名對應(yīng)的 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生成的文件路徑 修復(fù)警告: 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
}
//設(shè)置強(qiáng)制 更新
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")//這里傳錯會導(dǎo)致解密出現(xiàn)問題,
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本來是在主應(yīng)用中的,我是把a(bǔ)pplication文件夾直接移動過來了田巴,這樣重構(gòu)省事钠糊,不亂,隨時可以撤回去壹哺。
BuildConfig中定義字段法是比較坑爹的抄伍,比如里面存放url,但是如果分了那么多模塊和databind,那么主buildconfig.class經(jīng)常不生成,完全可以使用 不同channel不同的相同類 但是不同返回定義值定義法實現(xiàn)管宵。
另外需要注意的是同一個application 下可以指定多個res, java,卻不能manifest.xml截珍,不然可以簡單的進(jìn)行分離activity清單注冊整理,以后再進(jìn)行模塊化
模塊劃分完畢了還需要處理一個問題叫字符串加密箩朴,和代碼混淆我剛開始用的第三種方法的岗喉,現(xiàn)在行不通了,
經(jīng)過研究發(fā)現(xiàn)混淆也可以寫絕對路徑
每一個模塊復(fù)制這句話就行,不過需要忽略開發(fā)工具給的找不到某類的提示
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), new File(getRootDir().absolutePath+"/app",'proguard-rules.pro')
}
}
關(guān)于字符串加密的問題炸庞,如果每一個模塊復(fù)制這句話是很麻煩的,最后研究實現(xiàn)了根項目使用全局控制钱床!
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"http://
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")//這里傳錯會導(dǎo)致解密出現(xiàn)問題,
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{
}
}
}
/**
* 配置階段完成以后的監(jiān)聽回調(diào)
*/
this.afterEvaluate {
//和 variant.javaCompileProvider.configure 一起執(zhí)行
System.err.println '配置階段執(zhí)行完畢'
}
/**
* gradle 執(zhí)行完畢的回調(diào)監(jiān)聽
*/
this.gradle.buildFinished {
System.err.println '----------執(zhí)行階段執(zhí)行完畢'
}
applicationVariants.all { variant ->
variant.getMergeAssetsProvider().configur
variant.javaCompileProvider.configure
variant.outputs.each
}
上面的if else impl 對應(yīng)的寫法實際上還是有一些問題的埠居,也是網(wǎng)上我找到的方法查牌,但是這種方法會導(dǎo)致編譯器在識別的時候不會報紅色事期,什么意思呢,就是沒有走這個邏輯纸颜,理論上是找不到這個類的兽泣,但是看不到詳細(xì)信息, 只有在你實際編譯的過程中才知道錯誤
那么正確的寫法是什么呢懂衩?
如果是vipChannel 則應(yīng)該這么寫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公司只需要推送相關(guān)
bbApi(project(":push_table"))
bbApi(project(":uisc"))
上面 vip是完整版撞叨,包含了product ,也包含了 push_table,
2022-6-15 13:57:03
上面的寫法是基于gradle,寫法,而kts實際上都已經(jīng)實現(xiàn)了浊洞,暫時不發(fā)布教程