Android组件化手写实践

组件化就是将一个app分成多个Module,每个Module都是一个组件,开发的过程中可以单独调试部分组件,也可以集成到一个app中,组件间不需要互相依赖。组件化中会涉及到几个关键点:路由、组件间通信、组件初始化、组件/集成模式切换。

一、组件间通信

组件间通信主要利用SPI技术,SPI全称Service Provider Interface。实质就是为某个接口寻找服务的机制,将装配的控制权移交给ServiceLoader。Module间需要通信的部分基于接口编程,并开放接口给其他Module进行通信调用。

每个模块有不同的实现service provider,然后通过SPI机制自动注册到一个配置文件中,就可以实现在程序运行时扫描加载同一接口的不同service provider。这样模块之间不会基于实现类硬编码,可插拔。

接口module.png

如上图所示,首先定义一个接口Module。里面存放的是各个Module对外暴露的接口。而接口的实现类在各个Module中。

再看下订单Order Module,其Module对外暴露接口IOrderEntry的实现类如下:


import com.example.aninterface.IOrderEntry;
import com.example.order.OrderActivity;

public class OrderEntry implements IOrderEntry {
    @Override
    public String sayHello() {
        String helloWorld = "hello world from Module Order";
        return helloWorld;
    }

    @Override
    public void gotoOrderEntry(Context context){
        Intent intent = new Intent();
        intent.setClassName(context,OrderActivity.class.getName());
        context.startActivity(intent);
    }
}

OrderEntry实现了IOrderEntry对外暴露的接口方法,返回一个字符串以及执行一个界面跳转。

然后是要将接口实现类注册到配置文件中,spi的机制就是注册到META-INF/services中。需要新建一个以接口全路径名作为文件名的文件,文件保存在与res同级的resources/META-INF/services目录下。


spi配置.png

com.example.aninterface.IOrderEntry文件内容如下:

com.example.order.entry.OrderEntry

文件内容就是接口实现类的全限定名。

通过ServiceLoader加载实现类,然后得到迭代器,通过迭代器来调用实现类中的方法,从而达到调用Module暴露的接口的目的。

                ServiceLoader<IOrderEntry> orderEntry = ServiceLoader.load(IOrderEntry.class);
                Iterator<IOrderEntry> iterator = orderEntry.iterator();
                if (iterator.hasNext()){
                    IOrderEntry orderEntryImp = iterator.next();
                    String result = orderEntryImp.sayHello();
                    orderEntryImp.gotoOrderEntry(MainActivity.this);
                    Toast.makeText(getApplicationContext(),result,Toast.LENGTH_LONG).show();

                }else{
                    Log.e(TAG, "onClick: did not find service iml");
                }

路由

通过注解Annotation(编译时注解)以及APT(Annotation Processing Tool)来实现路由的收集。APT是一种处理注解的工具,可以对源代码文件进行检测并找出其中的Annotation,获取注解中的信息。
首先定义一个注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    String value();
}

在需要进行路由跳转的Activity中增加注解标注。

@Route("order/OrderActivity")
public class OrderActivity extends AppCompatActivity {

路由路径中order代表组件名称。

在Module router_complier主要进行注解的处理。build.gradle配置如下:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation project(':router_annotation')
    implementation 'com.squareup:javapoet:1.11.1'//方便编写代码的
}
sourceCompatibility = "7"
targetCompatibility = "7"

AutoService 主要的作用是注解 processor 类,自动生成。JavaPoet 这个库的主要作用就是帮助我们通过类调用的形式来生成代码(用代码写代码,生成类)。router_annotation中是定义的注解。

router_complier中增加一个注解处理器RouteProcess。

@AutoService(Processor.class)
public class RouteProcess extends AbstractProcessor {
    private Messager mMessager;
    private Elements mElementUtils;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv){
        super.init(processingEnv);
        mMessager = processingEnv.getMessager();
        mElementUtils = processingEnv.getElementUtils();
        mMessager.printMessage(Diagnostic.Kind.WARNING, "initlizing...");
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        mMessager.printMessage(Diagnostic.Kind.WARNING, "get supported types");
        HashSet<String> supportedTypes = new LinkedHashSet<>();
        supportedTypes.add(Route.class.getCanonicalName());
        return supportedTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if(set.size() <=0){
            return false;
        }
        Messager messager = processingEnv.getMessager();
        mMessager.printMessage(Diagnostic.Kind.WARNING,"=======Route annotation processor");
        HashMap<String,String> mapUrls = new HashMap<>();
        //获取注解
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Route.class);
        if (elements.size() <=0){
            return false;
        }
        for (Element element:elements){
            TypeElement variableElement = (TypeElement) element;
            String fullClassName = variableElement.getQualifiedName().toString();//value
            mMessager.printMessage(Diagnostic.Kind.WARNING, "className ="+fullClassName+" elemntSize="+elements.size());
            mapUrls.put(element.getAnnotation(Route.class).value(),fullClassName);
        }


        //成员变量path
        FieldSpec fieldPath = FieldSpec.builder(String.class,"path",Modifier.PUBLIC).build();

        //成员变量pathMap
        ClassName setName = ClassName.get("java.util","HashMap");
        ClassName stringName = ClassName.get("java.lang","String");
        TypeName setType = ParameterizedTypeName.get(setName,stringName,stringName);
        FieldSpec fieldPathMap = FieldSpec.builder(setType,"pathMap").build();
        //成员方法
        MethodSpec methodAdd = MethodSpec.methodBuilder("addPath")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(String.class,"path")
                .addParameter(String.class,"value")
                .beginControlFlow("if(pathMap == null)")
                .addStatement("$N = new HashMap()",fieldPathMap)
                .endControlFlow()
                .addStatement("$N.put("+"path,value"+")",fieldPathMap)
                .returns(void.class).build();
        MethodSpec.Builder methodAddBuidler = MethodSpec.methodBuilder("getAllPath")
                .addModifiers(Modifier.PUBLIC)
                .beginControlFlow("if(pathMap == null)")
                .addStatement("$N = new HashMap()",fieldPathMap)
                .endControlFlow();
        String groupName = "";
        for (Map.Entry<String,String> entry:mapUrls.entrySet()){
            groupName = entry.getKey();
            String key = entry.getKey();
            String value = entry.getValue();
            methodAddBuidler.addStatement("pathMap.put($S,$S)",key,value);
        }
        //确定分组名称,如果以“/”能得到分组名称,则以此作为生成的路由名称,若为空,则以跟时间相关的来取名称
        if (groupName != null && !groupName.isEmpty() && groupName.contains("/")){
            groupName = groupName.split("/")[0];
        }else{
            groupName = System.currentTimeMillis()%100+"";
        }
        methodAddBuidler.addStatement("return $N",fieldPathMap);
        MethodSpec methodAddAll = methodAddBuidler.returns(ParameterizedTypeName.get(ClassName.get(Map.class)
                ,ClassName.get(String.class),ClassName.get(String.class))).build();
        int randNum = (int) (System.currentTimeMillis()%10);
        messager.printMessage(Diagnostic.Kind.WARNING,"randomInt"+randNum);
        //构建类  以下划线作为分隔,以$作为分隔,后面根据包名查找类时匹配有问题
        TypeSpec routerClass = TypeSpec.classBuilder("MyRouterClass_"+groupName)
                .addModifiers(Modifier.PUBLIC)
                .addField(fieldPath)
                .addField(fieldPathMap)
                .addMethod(methodAdd)
                .addMethod(methodAddAll)
                .build();

//        TypeSpec autoClass = TypeSpec.classBuilder("MyRouterClass").addModifiers(Modifier.PUBLIC).addField(String.class,"path",Modifier.PUBLIC).build();


        JavaFile javaFile = JavaFile.builder("com.apt.demo.routes", routerClass).build();
        try{
            Filer filer = processingEnv.getFiler();
            javaFile.writeTo(filer);
        }catch (IOException e){
            e.printStackTrace();
            messager.printMessage(Diagnostic.Kind.WARNING,"generate apt class file fail");
        }
        return true;
    }

}

生成的类中使用HashMap保存了相应Module中路由路径和activity实际类名的对应关系。需要注意的是每个Module中都需要生成一个独立的类来保持路由映射关系,因为每个Module都会执行RouteProcess注解处理器,如果都用同一个类来保存映射关系,每个Module构建时会导致重复生成同一个类而报错。

调用DRouter的init2方法,将所有Module的路径保存到一个map中。

    public boolean init2(Context context){
        try {
            //将此包名下所有的类实例化,得到路由路径
            List<Class> routeClassList = ClassUtils.getClassesList(context,"com.apt.demo.routes");
            for(Class clz : routeClassList){
                Object routeInstance = clz.newInstance();
                Method method = clz.getDeclaredMethod("getAllPath");
                Map<String,String> groupRoutes = (Map<String, String>) method.invoke(routeInstance);
                routes.putAll(groupRoutes);
            }
            return true;
        }  catch (IllegalAccessException e) {
            e.printStackTrace();
            return false;
        } catch (InstantiationException e) {
            e.printStackTrace();
            return false;
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
            return false;
        } catch (InvocationTargetException e) {
            e.printStackTrace();
            return false;
        }
    }

ClassUtils.getClassesList方法查找指定包名下的所有类。处理路由注解时,apt生成的java文件(类)都在同一个包名下面。
路由跳转:

                DRouter.getInstance().build("order/OrderActivity").navigation(new DRouter.NavCallback() {
                    @Override
                    public void onLost(String routePath) {
                        Toast.makeText(MainActivity.this,"路由未找到",Toast.LENGTH_SHORT).show();
                    }
                });

跳转过程就是根据路由路径查找到对应的activity,然后执行跳转。在build方法中找到对应activity。在navigation中执行activity跳转。

    public DRouter build(String routePath){
        if (routePath != null && !routes.isEmpty()){
            selRoutePath = routes.get(routePath);
        }
        return this;
    }

    public void navigation(NavCallback callback){
        this.callback = callback;
        try{
            if(TextUtils.isEmpty(selRoutePath)){
                throw new ActivityNotFoundException();
            }
            Intent intent = new Intent();
            intent.setClassName(context,selRoutePath);
            ((Activity)context).startActivity(intent);
        }catch(ActivityNotFoundException exp){
            if (callback != null){
                callback.onLost(selRoutePath);
            }
        }
    }

组件初始化

不同Module在执行前可能会需要一些初始化工作,如一些第三方库的初始化等。没组件化之前可以统一放在主工程中的Application中进行,但是组件化之后,由于各个Module之间都解耦了,只能通过接口或者路由进行通信,这种情况下再以强耦合方式将各Module的初始化放在Application中肯定不合适。

可以有2种方案可供参考:
1、定义一个接口,各个Module在同一个包名下增加一个该接口的实现类,实现类中增加Module的初始化逻辑。在app的Application初始化时,查找该包名下的实现类,调用其中的方法完成初始化。
2、跟方法1类似,查找每个Module初始化的接口实现类,只不过通过注解来进行标记和查找。通过编译时注解(apt)记录接口初始化的接口实现类,在app的Application初始化时,拿到这些实现类,然后调用其中方法完成初始化。

初始化方法1

模块初始化接口如下:

/**
 * 【说明】:app生命周期接口
 */
public interface IAppLifecycleService {
    void onCreate();
}

在每个Module的固定包名下增加初始化实现类,在com.example.applife包名下

package com.example.applife;

import android.util.Log;

import com.example.router.annotation.RouterAppService;
import com.example.router.applife.IAppLifecycleService;

@RouterAppService(value = "order")
public class OrderAppLifecycleService implements IAppLifecycleService {
    private static final String TAG = "OrderAppLifecycleServic";
    @Override
    public void onCreate() {
        Log.e(TAG, "onCreate: 订单模块初始化");
    }
}

如上面代码所示,在订单Order模块,在指定包名下增加Module的初始化实现类,以及初始化逻辑。RouterAppService是初始化方法2中所使用的注解。

如何查找指定包名下、指定接口的所有实现类,增加一个ClassUtils类。

public class ClassUtils {
    public static List<Class> getClassesList(Context mContext, String packageName,Class clzzInterface) {
        ArrayList<String> classes = new ArrayList<>();
        ArrayList<Class> classList = new ArrayList<>();
        try {
            String packageCodePath = mContext.getPackageCodePath();
            DexFile df = new DexFile(packageCodePath);
            String regExp = "^" + packageName + ".\\w+$";
            for (Enumeration iter = df.entries(); iter.hasMoreElements(); ) {
                String className = (String) iter.nextElement();
                if (className.matches(regExp)) {
                    Class<?> clazz = Class.forName(className);
                    Log.e(TAG, "getClasses: "+clazz.getName());
                    //父类.class.isAssignableFrom(子类.class) 调用者为父类,参数为本身或者其子类。
                    //用于判断继承关系
                    if (clzzInterface.isAssignableFrom(clazz) && !clazz.isInterface()){
                        classes.add(className);
                        classList.add(clazz);
//                        continue;
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return classList;
    }
}

在主工程app的Application中调用上述方法找到初始化实现类,完成初始化。

public class MainApplication extends Application {
    private static final String TAG = "MainApplication";
    @Override
    public void onCreate() {
        super.onCreate();
        // 法1 - 模块初始化,每个模块在固定包名下定义一个接口实现类
        List<Class> appServiceList = ClassUtils.getClassesList(this,"com.example.applife", IAppLifecycleService.class);
        for (int i = 0; i < appServiceList.size(); i++) {
            try {
                IAppLifecycleService service = (IAppLifecycleService) appServiceList.get(i).newInstance();
                service.onCreate();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }
}

初始化方法2

在Module router_annotation增加一个注解RouterAppService。里面的value值是对应的Module名称。

/**
 * 【说明】:app生命周期初始化注解
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface RouterAppService {
    String value();
}

在Module router_complier中增加一个注解处理器RouterAppServiceProcess。

@SupportedAnnotationTypes("com.example.router.annotation.RouterAppService")
@AutoService(Processor.class)
public class RouterAppServiceProcess extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if(set.size() <=0){
            return false;
        }
        Messager messager = processingEnv.getMessager();
        messager.printMessage(Diagnostic.Kind.WARNING,"========RouterAppService annotation process start");
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(RouterAppService.class);
        if (elements.size() <=0){
            return false;
        }
        List<String> fullClassNameList = new ArrayList<>();
        String groupName = "";
        for(Element element:elements){
            TypeElement variableElement = (TypeElement) element;
            String fullClassName = variableElement.getQualifiedName().toString();
            messager.printMessage(Diagnostic.Kind.WARNING,"className="+fullClassName+" elemntSize="+elements.size());
            fullClassNameList.add(fullClassName);
            groupName = variableElement.getAnnotation(RouterAppService.class).value();
        }
        //构建方法
        MethodSpec.Builder methodAppBuilder = MethodSpec.methodBuilder("getAppServices")
                .addModifiers(Modifier.PUBLIC)
                .addStatement("List<String> serviceList = new $T<>()",ArrayList.class);
        for (String className : fullClassNameList){
            methodAppBuilder.addStatement("serviceList.add($S)",className);
        }
        methodAppBuilder.addStatement("return serviceList");
        MethodSpec methodGetService = methodAppBuilder.returns(ParameterizedTypeName.get(ClassName.get(List.class),ClassName.get(String.class))).build();
        if (groupName == null || groupName.isEmpty()){
            groupName = System.currentTimeMillis()%100+"";
        }
        //构建类
        TypeSpec serviceClass = TypeSpec.classBuilder("DAppServiceClass_"+groupName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodGetService)
                .build();
        JavaFile javaFile = JavaFile.builder("com.dj.apt.service",serviceClass).build();
        try{
            Filer filer = processingEnv.getFiler();
            javaFile.writeTo(filer);
        }catch (IOException e){
            e.printStackTrace();
            messager.printMessage(Diagnostic.Kind.WARNING,"generate apt class file fail");
        }
        return true;
    }
}

将被注解的类的类名全限定名保存下来,保存到一个List中。每个Module生成一个DAppServiceClass_组件名.java文件,里面getAppServices方法可以获取到该Module里初始化实现类的全限定名。生成的文件如下所示:

package com.dj.apt.service;

import java.lang.String;
import java.util.ArrayList;
import java.util.List;

public class DAppServiceClass_order {
  public List<String> getAppServices() {
    List<String> serviceList = new ArrayList<>();
    serviceList.add("com.example.applife.OrderAppLifecycleService");
    return serviceList;
  }
}

在DRouter类中增加方法,获取所有组件初始化的实现类的实例,进行初始化。

    public List<IAppLifecycleService> getAppServices(Context context){
        List<IAppLifecycleService> serviceList = new ArrayList<>();
        List<String> serviceNameList = new ArrayList<>();
        try {
            List<Class> serClassList = ClassUtils.getClassesList(context,"com.dj.apt.service");
            for(Class clz : serClassList){
                Object myRouterInstance = clz.newInstance();
                Method methodGetService = clz.getDeclaredMethod("getAppServices");
                List<String> serNameList= (List<String>) methodGetService.invoke(myRouterInstance);
                serviceNameList.addAll(serNameList);
            }
            for(String serName:serviceNameList){
                Class clSer = Class.forName(serName);
                IAppLifecycleService instance = (IAppLifecycleService) clSer.newInstance();
                serviceList.add(instance);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return serviceList;

    }

ClassUtils.getClassesList方法获取指定包名下的所有类信息。getAppServices方法获取到所有Module中实现IAppLifecycleService接口的所有实现类的实例。

public class MainApplication extends Application {
    private static final String TAG = "MainApplication";
    @Override
    public void onCreate() {
        super.onCreate();
        //法 2 - 模块初始化,每个模块定义一个接口实现类,用注解获取实现类,然后调用进行初始化
        Log.e(TAG, "onCreate: 使用注解进行初始化");
        List<IAppLifecycleService> serviceList = DRouter.getInstance().getAppServices(getApplicationContext());
        for (IAppLifecycleService service : serviceList){
            service.onCreate();
        }
    }
}

在app的Application进行所有组件的初始化操作,还可以根据实际需求在application销毁时增加相关反初始化操作。

组件、集成模式切换

组件化开发中需要能进行组件单独开发调试以及集成到app中进行。
可以在gradle.properties中增加一个变量来控制是组件还是集成模式。

#true代表集成模式 false代表组件模式
isModule=false

moduleOrder的build.gradle配置

if (isModule.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"


    defaultConfig {
        minSdkVersion 16
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles 'consumer-rules.pro'

        if (isModule.toBoolean()) {
            multiDexEnabled true
            applicationId "com.example.dj.order"
        }
    }

    sourceSets {
        main {
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                //集成开发模式下排除debug文件夹中的所有Java文件
                java {
                    exclude 'debug*//**'
                }
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

}

如上所示,如果是组件模式,则gradle中应用application插件,并在sourceSets中配置不同的AndroidManifest文件,组件模式的AndroidManifest中设置了本地组件的application,可以在其中执行组件的初始化操作,指定LaunchActivity等。
moduleOrder的工程目录结构如下图所示:


moduleOrder目录结构.png

如上图所示组件模式下有组件自己的application以及与集成模式不同的清单文件。

总结

本文主要对组件化中可能涉及到的路由、组件间通信、组件初始化、组件/集成模式切换等要点进行了阐述,并对其中涉及到的apt、spi等技术做了说明和简单实践。

代码参见:https://github.com/godtrace12/DRouter

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345