模塊化項目架構(gòu)筆記高級技巧 攔截所有模塊實現(xiàn)字符串加密編譯

說明

非模塊劃分有一種方法,
模塊劃分則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ā)布教程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末牵敷,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子法希,更是在濱河造成了極大的恐慌枷餐,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件苫亦,死亡現(xiàn)場離奇詭異毛肋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)屋剑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門润匙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人唉匾,你說我怎么就攤上這事孕讳。” “怎么了巍膘?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵厂财,是天一觀的道長。 經(jīng)常有香客問我峡懈,道長璃饱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任肪康,我火速辦了婚禮荚恶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘梅鹦。我一直安慰自己裆甩,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布齐唆。 她就那樣靜靜地躺著嗤栓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上茉帅,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天叨叙,我揣著相機(jī)與錄音,去河邊找鬼堪澎。 笑死擂错,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的樱蛤。 我是一名探鬼主播钮呀,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼昨凡!你這毒婦竟也來了爽醋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤便脊,失蹤者是張志新(化名)和其女友劉穎蚂四,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哪痰,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡遂赠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了晌杰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跷睦。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖肋演,靈堂內(nèi)的尸體忽然破棺而出送讲,到底是詐尸還是另有隱情,我是刑警寧澤惋啃,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站监右,受9級特大地震影響边灭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜健盒,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一绒瘦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧扣癣,春花似錦惰帽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春呜魄,著一層夾襖步出監(jiān)牢的瞬間悔叽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工爵嗅, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留娇澎,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓睹晒,卻偏偏與公主長得像趟庄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子伪很,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容