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

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

作者: 吉凶以情迁 | 来源:发表于2022-06-15 13:58 被阅读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实际上都已经实现了,暂时不发布教程

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/pedxvrtx.html