Android 架构

模块化项目架构笔记高级技巧 拦截所有模块实现字符串加密编译

2022-06-15  本文已影响0人  吉凶以情迁

说明

非模块划分有一种方法,
模块划分则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实际上都已经实现了,暂时不发布教程

上一篇下一篇

猜你喜欢

热点阅读