翻译机组件化改造方案

一、为什么要做组件化

随着程序代码随着时间积累,逐渐发展状态,功能越来越多,代码之间的耦合也越来越严重。程序的维护和扩展也越来越困难。

image.png

把软件整体划分为几个模块,每个模块负责一款独立的业务。模块与模块之间互相隔离,通过接口来通信。多个模块组成一个完成的app。

image.png

二、 组件化的目标

目标: 分层、解耦、复用、扩展

1、 将程序分为 从上至下分为: app壳工程,业务层,功能组件层,base层。上层依赖下层。

image.png
1.1 base层为基础层,保存各个module公用的东西
  • build.gradle 需要统一依赖业务组件用到的第三方依赖库和jar包
  • Manifest.xml 声明所有的使用权限 uses-permission 和 uses-feature。
  • Base组件的资源文件中需要放置项目公用的 资源文件:res/value*文件夹(多语言翻译)
  • 各个基类
  • 公用的自定义View类
  • 公用的常量类
  • 公用的bean

命名规则:arch
运行方式:只以library方式运行。

1.2 模块组件层为独立的与业务无关的功能模块

如,日志模块,http模块,下载模块,数据库模块 等。
这些模块完成独立的功能,供上层使用。
命名规则:mod_功能名称;
运行方式: 只以library方式运行;

1.3 业务层

依托于base层和模块组件层,负责独立的一个业务模块。
业务模块内部高内聚,模块与模块之间物理隔绝,通过接口来进行通信。
如:拍照翻译、语音翻译、汇率、SOS、词典、目的地、备忘、设置模块

命名规则:busi_业务名称。

运行方式:可以app形式单独调试运行,也可以以libary形式与其他业务模块组成一个完成app 对外发布。

1.4 app壳

仅已提供一个容器,来容纳各个业务模块。

理想的组织模型:

image.png

三、组件化实际实施方案

1、实际业务中会遇到问题:

由A业务 进入B业务,需提前调用一下B业务的Http接口,数据准备好之后,再跳转到B业务

image.png

2、理想解决模型:


image.png

2、实际的组织模型:

2.1、多个业务模块可能会交叉调用 相同的网络接口,也肯定交叉调用一个数据库接口。

2.2、各个业务模块尚未完全拆分为独立的模块,无法严格的按照对外暴露Service的方式,提供模块间的调用。


image.png

四、组件化过程中的技术问题

1、由于每个业务是一个独立的模块,他们之间如何实现通信?

image.png
1.1 ==如何由ActivityA 跳转到ActivityB?==

(1)首先来看Android里面 Activity跳转的代码

 Intent intent = new Intent(this,ActivityB1.class);
        startActivity(intent);

但是在ModuleA中 我们不知道ActivityB1的存在,也就拿不到ActivityB1.cass,无法跳转。

(2)解决方案:利用单例类 在模块间传递信息.

public class WareHouse {
   static WareHouse mInstance;
    public static WareHouse getInstance(){
        if(mInstance == null){
            synchronized (WareHouse.class){
                if(mInstance == null){
                    mInstance = new WareHouse();
                }
            }
        }
        return mInstance;
    }
    
    public static Map<String, Class<?>> routes = new HashMap<>();
}

image.png

在ModuleA 中,创建一个类,RouterHelperA 负责收集moduleA 中的路由信息。

public class RouterHelperA {

    public void loadRouteInfo(Map<String, Class<?>> routeMap){
        routeMap.put("/moduleA/A1",AcitivityA1.class);
        routeMap.put("/moduleA/A2",ActivityA2.class);

    }
}

==在适当的时机,实例化RouterHelperA对象,调用loadRouteInfo()方法==,将moduleA中的路由信息 收集到了WareHouse当中。

  new RouterHelperA().loadRouteInfo(WareHouse.getInstance().routes);

同理在moduleB中,也创建一个辅助类

public class RouteHelperB {
    public void loadRouteInfo(Map<String, Class<?>> routeMap){
        routeMap.put("/moduleB/B1",ActivityB1.class);
        routeMap.put("/moduleB/B2",ActivityB2.class);

    }
}

==并在适当的时机,调用==

  new RouterHelperB().loadRouteInfo(WareHouse.getInstance().routes);

这样moduleB中的路由信息也被收集到了WareHouse当中。

此时再看moduleA中跳转到moduleB中ActivityB1,按照如下代码是不是就可以了吗?

 Class activityB = WareHouse.getInstance().routes.get("/moduleB/B1");
        Intent intent = new Intent(this,activityB);
        startActivity(intent);
1.2、==A模块如何调用B模块的服务operationB==?

我们将WareHouse进行扩展,补充一个seviceRoutes,为每一个Service路由,保留一个Service服务单例实例。

   public static Map<String,IService> seviceRoutes = new HashMap<>();
public class WareHouse {
   static WareHouse mInstance;
    public static WareHouse getInstance(){
        if(mInstance == null){
            synchronized (WareHouse.class){
                if(mInstance == null){
                    mInstance = new WareHouse();
                }
            }
        }
        return mInstance;
    }

    public static Map<String, Class> routes = new HashMap<>();

    public static Map<String,IService> seviceRoutes = new HashMap<>();

    public IService getServiceByRoute(String path){
        IService service = seviceRoutes.get(path);

        if(service == null){
            try {
                Class clazz = routes.get(path);
                Object tmp = clazz.newInstance()
                if(tmp  instanceof IService){
                    service = (IService)tmp;
                }
            seviceRoutes.put(path,service);

            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
        return service;
    }
}

我们为moduleB 创建一个ServiceB,用于对外暴露一些服务和操作

moduleB

public class ServiceB implements IService {
    @Override
    public void call(String comonad) {
        if(comonad.equals("queryDB")){
            //do something
        }

    }
}

public class RouteHelperB {
    public void loadRouteInfo(Map<String, Class<?>> routeMap){
        routeMap.put("/moduleB/B1",ActivityB1.class);
        routeMap.put("/moduleB/B2",ActivityB2.class);
        routeMap.put("/moduleB/ServiceB",ServiceB.class);
    }
}

在moduleA 中要访问moduleB的公用服务,可以通过

 WareHouse.getInstance().getServiceByRoute("/moduleB/ServiceB").call("queryDB");

2、组件化是如何实现 单个业务模块独立运行的?

moudle 从libary 切换到applciation 涉及到一下几点:

(1)引入插件变更

引入
apply plugin: 'com.android.library'

改为引入
apply plugin: 'com.android.application'

(2) 作为application运行AndroidMenifest.xml需要声明一个启动Activity,通常还需要一个BaseApplicatio类,来做一些初始化操作。所以需要切换sourcesets的操作。

在src/main目录下新建/debug文件夹。debug目录新建java、res和AndroidMenifest.xml文件。


image.png

debug目录的AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.sogou.teemo.busi_exchangerate"
    android:sharedUserId="android.uid.system">

    <application
        android:name="debug.AppContext"
        android:allowBackup="true"
        android:icon="@mipmap/exchange_ic_launcher"
        android:label="@string/exchange_app_name"
        android:supportsRtl="false"
        android:theme="@style/AppTheme"
        tools:replace="android:allowBackup,android:supportsRtl,android:icon,android:label">
        <activity
            android:name="com.sogou.teemo.busi_exchangerate.ExchangeActivity"
            android:configChanges="orientation|keyboardHidden"
            android:launchMode="singleTask"
            android:screenOrientation="portrait"
            android:windowSoftInputMode="adjustPan|stateHidden">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        <activity
            android:name="com.sogou.teemo.busi_exchangerate.RateSearchActivity"
            android:configChanges="orientation|keyboardHidden"
            android:screenOrientation="portrait"
            android:windowSoftInputMode="adjustPan|stateHidden" />
    </application>

</manifest>

(3)为了防止不同module中 资源文件名冲突,需要指定每个module的资源名前缀

build.gradle

 resourcePrefix "exchange_"

(4) 业务module 作为应用独立运行时,原来的宿主app不能再依赖该业务module。

综上,以翻译机汇率计算模块举例,需要做以下修改。

  • 根目录 build.gradle 扩展一个标志位
//true表示 业务模块作为applciatio独立运行。
//false表示,业务模块作为library集成到宿主app中集成打包
ext.isComponent = false
  • busi_exchangerate 模块的build.gradle修改如下。
if(isComponent.toBoolean()){ //组件独立运行模式
    apply plugin: 'com.android.application'
}else {
    apply plugin: 'com.android.library'
}
apply from: rootProject.file('plugin.gradle')


android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion


    defaultConfig {
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.minSdkVersion
        versionCode rootProject.ext.versionCode
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        
    }



    sourceSets {
        main {
            if (isComponent.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
                java.srcDirs = ['src/main/java', "src/main/debug/java"]
                res.srcDirs = ['src/main/res', "src/main/debug/res"]
                assets.srcDirs = ['src/main/assets', "src/main/debug/assets"]
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
    resourcePrefix "exchange_"

}
  • 宿主app build.gradle 修改如下:
apply plugin: 'com.android.application'


android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion
    defaultConfig {
        applicationId "com.sogou.teemo.translate.launcher"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.minSdkVersion
        versionCode rootProject.ext.versionCode
        versionName "${rootProject.ext.versionCode}.${releaseTime()}"
        multiDexEnabled true //使能multDex

        ndk {
            abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a'
        }

    }

}

//添加kapt 依赖
addKApt(dependencies)

dependencies {
    //内部模块依赖
    implementation project(':arch')
    if(!isComponent.toBoolean()){
        implementation project(path: ':busi_exchangerate')
    }

}

四、组件化改造的第三方库

image.png

路由驱动,代是ARouter。

总线驱动,代表CC。

ARouter 有以下特点:

1、核心技术点:Annotation注解。利用注解技术,自动生成辅助类,收集各个模块的路由信息。

2、页面跳转操作方便,侵入性低。

3、后续也实现了通过ASM 修改字节码,来收集路由信息作为补充。

CC 的特点:

1、核心技术点:编写gradle插件,在Transfrom 阶段,利用ASM字节码修改,来完成路由信息(总线信息)的收集。ContentProvider 跨进程通信。

2、支持跨进程调用。

3、可以渐进式的组件化。

4、在gradle插件中 实现业务组件 library和application的切换。

5、使用时 代码侵入性较强,需要保证setResult被调用。

翻译机应用页面跳转较多,因此选择了ARouter作为路由方案,同时结合gradle插件实现业务组件library和application的自动切换。

image.png

ProjectModuleManager.groovy 参考自CC库方案,直接拿来用。

package com.sogou.teemo.register.plugin

import org.gradle.api.Project
import org.gradle.util.GradleVersion

import java.util.regex.Pattern

/**
 * 工程中的组件module管理工具
 * 1. 用于管理组件module以application或library方式进行编译
 * 2. 用于管理组件依赖(只在给当前module进行集成打包时才添加对组件的依赖,以便于进行代码隔离)
 */
class ProjectModuleManager {
    static final String PLUGIN_NAME = RegisterPlugin.PLUGIN_NAME
    
    //为区别于组件单独以app方式运行的task,将组件module打包成aar时,在local.properties文件中添加 assemble_aar_for_cc_component=true
    static final String ASSEMBLE_AAR_FOR_CC_COMPONENT = "assemble_aar_for_cc_component"
    /**
     * 手动在gradle命令中指定当前是为哪一个module打apk
     * 主要用途:
     *      1. 插件化打包 (由于gradle命令不在正则表达式 {@link #TASK_TYPES}范围内,但需要集成打包)
     *          ./gradlew :demo:xxxxx -PccMain=demo
     *      2. 打aar包,相反的用途,指定ccMain为一个不存在的module名称即可,可替代assemble_aar_for_cc_component的作用
     *          ./gradlew :demo_component_b:assembleRelease -PccMain=nobody
     *          注意:此用法对于ext.mainApp=true的module无效,对于ext.alwaysLib=true的module来说无意义
     */
    static final String ASSEMBLE_APK_FOR_CC_COMPONENT = "ccMain"



    // --------- 分割线 ----------

    //组件单独以app方式运行时使用的测试代码所在目录(manifest/java/assets/res等),这个目录下的文件不会打包进主app
    static final String DEBUG_DIR = "src/main/debug/"

    //主app,一直以application方式编译
    static final String MODULE_MAIN_APP = "mainApp" 
    //一直作为library被其它组件依赖
    static final String MODULE_ALWAYS_LIBRARY = "alwaysLib" 

    static String mainModuleName //正在编译的moudle
    static boolean taskIsAssemble //任务是否正在被编译组装


    static boolean manageModule(Project project) {
        taskIsAssemble = false
        mainModuleName = null

        initByTask(project)

        //读取local.properties,判断该module 是否要编译成aar
        Properties localProperties = new Properties()
        try {
            def localFile = project.rootProject.file('local.properties')
            if (localFile != null && localFile.exists()) {
                localProperties.load(localFile.newDataInputStream())
            }
        } catch (Exception ignored) {
            println("${PLUGIN_NAME}: local.properties not found")
        }
        def buildingAar = isBuildingAar(localProperties)//该moudle要编译成aar

        def mainApp = isMainApp(project) //是不是主app
        def assembleFor = isAssembleFor(project)//该moudle 是不是人为指定的主模块.

        def alwaysLib = isAlwaysLib(project)//该moudle是不是一直是library

        boolean runAsApp = false
        if (mainApp) { //对于主app,runAsApp
            runAsApp = true
        } else if (alwaysLib || buildingAar) {
            runAsApp = false
        } else if (assembleFor || !taskIsAssemble) {//正在编译的moudle,runAsApp
            runAsApp = true
        }
        project.ext.runAsApp = runAsApp //project中设置了一个标志,记录runAsApp

        println "${PLUGIN_NAME}: mainModuleName=${mainModuleName}, project=${project.name}, runAsApp=${runAsApp} . taskIsAssemble:${taskIsAssemble}. " +
                "settings(mainApp:${mainApp}, alwaysLib:${alwaysLib}, assembleThisModule:${assembleFor}, buildingAar:${buildingAar})"


        if (runAsApp) {  //做app编译时, 切换 PlugIn 和 msourceSets资源路径

            //(1)指定com.android.application PlugIntaskIsAssemble
            project.apply plugin: 'com.android.application'

            //(2)更改 sourceSets 资源路径
            project.android.sourceSets.main {
                //debug模式下,如果存在src/main/debug/AndroidManifest.xml,则自动使用其作为manifest文件
                def debugManifest = "${DEBUG_DIR}AndroidManifest.xml"
                if (project.file(debugManifest).exists()) {
                    manifest.srcFile debugManifest
                }
                //debug模式下,如果存在src/main/debug/assets,则自动将其添加到assets源码目录
                if (project.file("${DEBUG_DIR}assets").exists()) {
                    assets.srcDirs = ['src/main/assets', "${DEBUG_DIR}assets"]
                }
                //debug模式下,如果存在src/main/debug/java,则自动将其添加到java源码目录
                if (project.file("${DEBUG_DIR}java").exists()) {
                    java.srcDirs = ['src/main/java', "${DEBUG_DIR}java"]
                }
                //debug模式下,如果存在src/main/debug/res,则自动将其添加到资源目录
                if (project.file("${DEBUG_DIR}res").exists()) {
                    res.srcDirs = ['src/main/res', "${DEBUG_DIR}res"]
                }
            }
        } else { //作为library 则直接指定com.android.library 即可.
            project.apply plugin: 'com.android.library'
        }
        //(3)当前的project添加依赖(addComponent)
        addComponentDependencyMethod(project, localProperties)
        return runAsApp
    }

    //需要集成打包相关的task
    static final String TASK_TYPES = ".*((((ASSEMBLE)|(BUILD)|(INSTALL)|((BUILD)?TINKER)|(RESGUARD)).*)|(ASR)|(ASD))"

    //此方法仅是针对这种gradlew命令特例,判定当前模块是否为主模块。--- 可以忽略
    static void initByTask(Project project) {
        //先检查是否手动在当前gradle命令的参数中设置了mainModule的名称
        //设置方式如:
        //  ./gradlew :demo:xxxBuildPatch -PccMain=demo //用某插件化框架脚本为demo打补丁包
        //  ./gradlew :demo_component_b:assembleRelease -PccMain=anyothermodules //为demo_b打aar包
        def projectProps = project.gradle.startParameter.projectProperties
        if (projectProps && projectProps.containsKey(ASSEMBLE_APK_FOR_CC_COMPONENT)) {
            mainModuleName = projectProps.get(ASSEMBLE_APK_FOR_CC_COMPONENT)//包含了ccMain,则ccMain指定的模块就是主Module
            taskIsAssemble = true
            return
        }

        def taskNames = project.gradle.startParameter.taskNames
        println("initByTask modle:${project.name}")
        println("initByTask projectProps :${projectProps}")
        println("initByTask taskNames :${taskNames.toString().toUpperCase()}")
        def allModuleBuildApkPattern = Pattern.compile(TASK_TYPES)
        for (String task : taskNames) {
            if (allModuleBuildApkPattern.matcher(task.toUpperCase()).matches()) {
                taskIsAssemble = true
                if (task.contains(":")) {
                    def arr = task.split(":")
                    mainModuleName = arr[arr.length - 2].trim()
                }
                break
            }
        }
    }

    /**
     * 当前是否正在给指定的module集成打包
     */
    static boolean isAssembleFor(Project project) {
        return project.name == mainModuleName
    }
    static boolean isMainApp(Project project) {
        return project.ext.has(MODULE_MAIN_APP) && project.ext.mainApp
    }
    static boolean isAlwaysLib(Project project) {
        return project.ext.has(MODULE_ALWAYS_LIBRARY) && project.ext.alwaysLib
    }
    //判断当前设置的环境是否为组件打aar包(比如将组件打包上传maven库)
    static boolean isBuildingAar(Properties localProperties) { //包含assemble_aar_for_cc_component 则说明要打包成aar
        return 'true' == localProperties.getProperty(ASSEMBLE_AAR_FOR_CC_COMPONENT)
    }

    //组件依赖的方法,用于进行代码隔离
    //对组件库的依赖格式: addComponent dependencyName [, realDependency]
    // 使用示例见demo/build.gradle
    //  dependencyName: 组件库的名称,推荐直接使用使用module的名称
    //  realDependency(可选): 组件库对应的实际依赖,可以是module依赖,也可以是maven依赖
    //    如果未配置realDependency,将自动依赖 project(":$dependencyName")
    //    realDependency可以为如下2种中的一种:
    //      module依赖 : project(':demo_component_b') //如果module名称跟dependencyName相同,可省略(推荐)
    //      maven依赖  : 'com.billy.demo:demoB:1.1.0' //如果使用了maven私服,请使用此方式
    static void addComponentDependencyMethod(Project project, Properties localProperties) {
        //当前task是否为给本module打apk包
        def curModuleIsBuildingApk = taskIsAssemble && (mainModuleName == null && isMainApp(project) || mainModuleName == project.name)//主app或者正在编译运行的moudle
        //为project 扩展 addComponent方法
        project.ext.addComponent = { dependencyName, realDependency = null ->
            //(1)不是在为本app module打apk包,不添加对组件的依赖
            if (!curModuleIsBuildingApk)
                return
            //(2)local.Properties 单独配置的模块,不会被添加到依赖当中.
            def excludeModule = 'true' == localProperties.getProperty(dependencyName)
            if (!excludeModule) {
                //找到被依赖的模块
                def componentProject = project.rootProject.subprojects.find { it.name == dependencyName }
//                def dependencyMode = GradleVersion.version(project.gradle.gradleVersion) >= GradleVersion.version('4.1') ? 'api' : 'compile'

                def dependencyMode ='api'

                if (realDependency) {
                    //通过参数传递的依赖方式,如:
                    // project(':moduleName')
                    // 或
                    // 'com.billy.demo:demoA:1.1.0'
                    project.dependencies.add(dependencyMode, realDependency)//定义了realDependency,则将realDependency 添加到dependencies当中
                    println "CC >>>> add $realDependency to ${project.name}'s dependencies"
                } else if (componentProject) {
                    //第二个参数未传,默认为按照module来进行依赖
                    project.dependencies.add(dependencyMode, project.project(":$dependencyName"))
                    println "CC >>>> add project(\":$dependencyName\") to ${project.name}'s dependencies"
                } else {
                    throw new RuntimeException(
                            "CC >>>> add dependency by [ addComponent '$dependencyName' ] occurred an error:" +
                                    "\n'$dependencyName' is not a module in current project" +
                                    " and the 2nd param is not specified for realDependency" +
                                    "\nPlease make sure the module name is '$dependencyName'" +
                                    "\nelse" +
                                    "\nyou can specify the real dependency via add the 2nd param, for example: " +
                                    "addComponent '$dependencyName', 'com.billy.demo:demoB:1.1.0'")
                }
            }
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容