手把手讲解 Android插件化启动Activity

前言

手把手讲解系列文章,是我写给各位看官,也是写给我自己的。
文章可能过分详细,但是这是为了帮助到尽量多的人,毕竟工作5,6年,不能老吸血,也到了回馈开源的时候.
这个系列的文章:
1、用通俗易懂的讲解方式,讲解一门技术的实用价值
2、详细书写源码的追踪,源码截图,绘制类的结构图,尽量详细地解释原理的探索过程
3、提供Github 的 可运行的Demo工程,但是我所提供代码,更多是提供思路,抛砖引玉,请酌情cv
4、集合整理原理探索过程中的一些坑,或者demo的运行过程中的注意事项
5、用gif图,最直观地展示demo运行效果

如果觉得细节太细,直接跳过看结论即可。
本人能力有限,如若发现描述不当之处,欢迎留言批评指正。

学到老活到老,路漫漫其修远兮。与众君共勉 !


引子

用过微信支付宝的人应该都知道,其中有很多功能,都是可以灵活配置的,比如,支付宝里面,可以用“淘票票”来买电影票,还可以购买火车票飞机票,,甚至还有“饿了么”外卖,如果细细去数的话,这些杂七杂八的功能,加起来可能有几十上百个了。
试想,这么多小功能,代码都写在支付宝里面,支付宝会不会爆炸 o(╯□╰)o !那支付宝的开发者如何做到这些功能的集成呢?

插件化开发Demo地址

支付宝 app本身更像是一个 “ 空壳 ”,里面可以搭载很多小功能,这些小功能都是以"插件"的形式存在,支持小功能的灵活配置,用户不想要某个功能,可以不显示出来。
插件化开发是当下大型app必备的一项技术,不可不学。


鸣谢

感谢 大佬 "潇湘夜雨" 提供的Demo
感谢 享学课堂alvin老师 的的热心帮助


正文大纲

1.插件化开发的核心难点

2.插件化所需的技术理论基础

3.核心难点的解决方案

4.核心代码结构

5.如何使用Demo

6.最终效果展示


正文

1.插件化开发的核心难点

根据引子中所说,支付宝中各种各样的功能,都是插件形式存在的,那么具体是如何存在?
我们所说的插件,其实是apk文件,即 xxx.apk
插件化开发的套路: 外壳app module + 多个插件Module + 插件框架层library module

  • 外壳app 负责整个app的外部架构,并且给插件提供入口组件(比如,用一个button作为“余额宝”的入口,点击button,进入“余额宝” );
  • 多个插件Module,负责分开开发各个功能。严格来说,每个功能必须可以单独运行,也必须支持集成到外壳app时运行。
  • 插件框架层library module, 所有插件化的核心代码,都集中到这里。并且这个library要同时被外壳app和插件module引用.

文字描述不够直观?
看下图:

插件化开发的代码结构.png

那么现在很清晰了,插件化开发的难点,就是如何让外壳app,启动插件apk中的Activity.
既然给出了demo的代码架构,那就顺便给出github地址了:Demo


2.插件化所需的技术理论基础

可能从上面的代码架构上看,这项技术并不是很复杂,但是也是需要一定的技术基础的,不然出一点小问题,一脸懵逼,无从查起就很尴尬了.

学习插件化开发,首先要了解

1.Activity是如何启动的.
在我们自己的Activity里,开启另一个Activity,使用startActivity即可,但是startActivity之后,系统做了什么?

开始追踪源码(源码追踪基于SDK 28 - 9.0):
我们通常通常启动Activity,一般都是在Activity中 使用startActivity(intent),像下面这样

public class MainActivity extends AppCompatActivity(){
    private void xxxx(){
        Intent i = new Intent(this,XXXActivity.class);
        startActivity(i);
    }
}

那么,startActivity到底做了什么,点进去看找到下面的代码:

@Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }

继续,追踪这两个startActivityForResult,直接到下面的代码:

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
        if (mParent == null) {
            options = transferSpringboardActivityOptions(options);
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);//注意看这里,mInstrumentation.execStartActivity
            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }
            if (requestCode >= 0) {
               ···
                mStartedActivity = true;
            }

            cancelInputsAndStartExitTransition(options);
            // TODO Consider clearing/flushing other event sources and events for child windows.
        } else {
            if (options != null) {
                mParent.startActivityFromChild(this, intent, requestCode, options);
            } else {
                // Note we want to go through this method for compatibility with
                // existing applications that may have overridden it.
                mParent.startActivityFromChild(this, intent, requestCode);
            }
        }
    }

mInstrumentation.execStartActivity在这里被执行,
然而另一个分支mParent 不为空时,会执行mParent.startActivityFromChild
那么追踪它startActivityFromChild

public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,
            int requestCode, @Nullable Bundle options) {
        options = transferSpringboardActivityOptions(options);
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, child,
                intent, requestCode, options);//然而,这里还是执行了mInstrumentation.execStartActivity
        if (ar != null) {
            mMainThread.sendActivityResult(
                mToken, child.mEmbeddedID, requestCode,
                ar.getResultCode(), ar.getResultData());
        }
        cancelInputsAndStartExitTransition(options);
    }

然而,这里还是执行了mInstrumentation.execStartActivity,
综上所述,startActivity,最终都会执行到mInstrumentation.execStartActivity
那么继续跟踪这个execStartActivity:

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
       ...省略一大段...
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(who);
            int result = ActivityManager.getService()//这个不就是大名鼎鼎的AMS么
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }
        return null;
    }

注意这里有一个ActivityManager.getService(),其实他就是安卓里大名鼎鼎的AMS(ActivityManagerService),负责对 Android四大组件(activityservicebroadcastcontentProvider)的管理,包括启动,生命周期管理等.

Activity里面startActivity的追踪就到这里。

PS:其实,Activity不只是可以在Activity里启动,还可以使用 getApplicationContext().startActivity(),有兴趣的可以去追踪一下,最终结论还是一样,都会执行AMS的startActivity.

结论:

我们通常在自己的XXXActivity里调用startActivity之后,最终会执行AMS的startActivity,从而让启动的那个Activity具有生命周期.
那么,如果只是new 一个 Activity实例,它会不会具有生命周期呢?显而易见了.


2. apk包(其实是压缩包)里的各个文件各自的作用
androidStudio里,运行app,或者gradle 执行assemble命令可以生成apk文件,那么,我们解压apk文件之后,它里面的各种内部文件,各自都起到了什么作用呢?

请看下图

一个apk解压之后的内容

这里我们发现了这么几个东西:

  • classes.dex 工程里面的java源码编译打包而成.
    classes.dex文件,包含了这个apk的所有java类,那么我们拿到了这个dex文件,就有能力反射创建其中的类对象.
    用AndroidStudio可以看到其内容:

    image.png

  • res目录 所有的资源文件

    image.png

    外壳app 通过资源包,可以拿到包里面的任意资源,当然,前提是,宿主要创建对应资源包的Resources对象.

  • resources.arsc res下所有资源的映射

    image.png

  • META-INF app签名的一些东西

  • AndroidManifest.xml 清单文件

3.核心难点的解决方案

了解了上面的技术基础,那么现在摆出解决方案:
外壳app,作为一个"宿主"。插件apk中的所有东西,无论是classes.dex里的类,还是res资源,都是"宿主"之外的东西,那么宿主要想使用自己身外的类和资源,需要解决3个问题:

1. 取得插件中的Activity的Class
解决方案 ==> 使用DexClassLoader.它是专门加载外部apk的类加载器.

2. 取得插件中的资源
解决方案 ==> 使用hook技术,创建只属于外部插件的Resouces资源管理器.

3. 反射创建了插件中的Activity对象,但是它是没有生命周期的,不能像使用宿主自身的Activity一样拥有完整的生命周期.如果不理解,请回去看 “2.插件化所需的技术理论基础”
解决方案 ==> 使用 代理Activity作为真正插件Activity的"傀儡".


4.核心代码结构 这是Demo地址

以demo为样板进行细节讲解,下图是demo的项目结构:

外壳app的结构.png

可以看到,外壳app很简单,唯一要说明的就是插件apk,我放置在src/main/assets目录,只是为了演示demo方便.

  • MyApp.java ,只做了一件事,PluginManager.getInstance().init(this); ,对PluginManager进行初始化并且赋予上下文.
  • MainActivity.java 只做了两件事,
  1. 将asssets里面的apk文件,通过工具类AssetUtil的copyAssetToCache方法,拷贝到了app的缓存目录下,然后使用PluginManager去加载这个apk.
String path = AssetUtil.copyAssetToCache(MainActivity.this, "plugin_module-debug.apk");
PluginManager.getInstance().loadPluginApk(path);
  1. 跳转到代理Activity,并且传入真正要跳的目标Activity的name.
// 先跳到代理Activity,由代理Activity展示真正的Activity内容
               Intent intent = new Intent(MainActivity.this, ProxyActivity.class);
               intent.putExtra(PluginApkConst.TAG_CLASS_NAME, 
                       PluginManager.getInstance().getPackageInfo().activities[0].name);
               startActivity(intent);

这里涉及到了PluginManager类,下一小节详述.

插件module.png

插件module十分简单,它的作用,就是生成插件apk,对它进行编译打包,取得apk文件即可。
两个重点:

  1. 插件中的所有Activity,必须都集成来自plugin_lib的PluginBaseActivity,只有继承了,才具有插件化特征,能够被外壳app执行startActivity成功跳转.
  2. 插件内部的Activity跳转,上下文,必须使用PluginBaseActivityproxy变量.
    image.png

前面两个module都很简单,那么核心技术在哪里?

插件化框架library.png

插件框架层代码,是插件化开发技术的核心。这个module要同时被外壳app和插件module引用.

其中,3个技术要点:

  1. PluginManager类
    它是一个单例,负责读取插件apk的内容,并且创建出专属于插件的类加载器DexClassLoader,资源管理器Resources,以及包信息 PackageInfo 并 用public get方法公开出去。
public class PluginManager {

    //应该是单例模式,因为一个宿主app只需要一个插件管理器对象即可
    private PluginManager() {
    }

    private volatile static PluginManager instance;//volatile 保证每一次取的instance对象都是最新的

    public static PluginManager getInstance() {
        if (instance == null) {
            synchronized (PluginManager.class) {
                if (instance == null) {
                    instance = new PluginManager();
                }
            }
        }
        return instance;
    }

    private Context mContext;//上下文

    private PackageInfo packageInfo;//包信息
    private DexClassLoader dexClassLoader;//类加载器
    private Resources resources;//资源包

    public void init(Context context) {
        mContext = context.getApplicationContext();//要用application 因为这是单例,直接用Activity对象作为上下文会导致内存泄漏
    }

    /**
     * 从插件apk中读出我们所需要的信息
     *
     * @param apkPath
     */
    public void loadPluginApk(String apkPath) {
        //先拿到包信息
        packageInfo = mContext.getPackageManager().getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);//只拿Activity
        if (packageInfo == null)
            throw new RuntimeException("插件加载失败");//如果apkPath是传的错的,那就拿不到包信息了,下面的代码也就不用执行

        //类加载器,DexClassLoader专门负责外部dex的类
        File outFile = mContext.getDir("odex", Context.MODE_PRIVATE);
        dexClassLoader = new DexClassLoader(apkPath, outFile.getAbsolutePath(), null, mContext.getClassLoader());

        //创建AssetManager,然后创建Resources
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            method.invoke(assetManager, apkPath);
            resources = new Resources(assetManager,
                    mContext.getResources().getDisplayMetrics(),
                    mContext.getResources().getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    //把这3个玩意公开出去
    public PackageInfo getPackageInfo() {
        return packageInfo;
    }

    public DexClassLoader getDexClassLoader() {
        return dexClassLoader;
    }

    public Resources getResources() {
        return resources;
    }
}
  1. ProxyActivity类
    它作为一个代理,一个傀儡,宿主能够通过它,来间接地管理真正插件Activity的生命周期.
    那它是如何间接管理真正Activity的生命周期?用类似下面的代码:
public void ProxyActivity extends Activity{
    @Override
    protected void onStart() {
        iPlugin.onStart();//iPlugin是插件Activity实现的接口,前面用IPlugin将插件Activity对象接收了
        super.onStart();
    }
}

然而,ProxyActivity的onCreate另有玄机

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        realActivityName = getIntent().getStringExtra(PluginApkConst.TAG_CLASS_NAME);//宿主,将真正的跳转意图,放在了这个参数className中,
        //拿到realActivityName,接下来的工作,自然就是展示出真正的Activity
        try {// 原则,反射创建RealActivity对象,但是,去拿这个它的class,只能用dexClassLoader
            Class<?> realActivityClz = PluginManager.getInstance().getDexClassLoader().loadClass(realActivityName);
            Object obj = realActivityClz.newInstance();
            if (obj instanceof IPlugin) {//所有的插件Activity,都必须是IPlugin的实现类
                iPlugin = (IPlugin) obj;
                Bundle bd = new Bundle();
                bd.putInt(PluginApkConst.TAG_FROM, IPlugin.FROM_EXTERNAL);
                iPlugin.attach(this);
                iPlugin.onCreate(bd);//反射创建的插件Activity的生命周期函数不会被执行,那么,就由ProxyActivity代为执行
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

前面PluginManager返回了专属于插件的类加载器DexClassLoader,资源管理器Resources,那么这个ProxyActivity真正展示的是插件的Activity内容,就要使用插件自己的类加载器和资源管理器了.

    @Override
    public ClassLoader getClassLoader() {
        ClassLoader classLoader = PluginManager.getInstance().getDexClassLoader();
        return classLoader != null ? classLoader : super.getClassLoader();
    }

    @Override
    public Resources getResources() {
        Resources resources = PluginManager.getInstance().getResources();
        return resources != null ? resources : super.getResources();
    }

注意,前方大坑,
public class ProxyActivity extends Activity{},我的ProxyActivity继承的是android.app.Activity,而不是 android.support.v7.app.AppCompatActivity,这是因为,AppCompatActivity会检测上下文context,从而导致空指针.
至于更深层的原因,有兴趣的大佬可以继续挖掘,没兴趣的话直接用android.app.Activity就完事了.


  1. IPlugin接口和PluginBaseActivity类
    插件module中也许不只一个Activity,我们启动插件Activity之后,插件内部如果需要跳转,仍然要遵守插件化的规则,那就给他们创建一个共同的父类PluginBaseActivity.

IPlugin 接口

/**
 * 插件Activity的接口规范
 */
public interface IPlugin {

    int FROM_INTERNAL = 0;//插件单独测试时的内部跳转
    int FROM_EXTERNAL = 1;//宿主执行的跳转逻辑

    /**
     * 给插件Activity指定上下文
     *
     * @param activity
     */
    void attach(Activity activity);

    // 以下全都是Activity生命周期函数,
    // 插件Activity本身 在被用作"插件"的时候不具备生命周期,由宿主里面的代理Activity类代为管理
    void onCreate(Bundle saveInstanceState);

    void onStart();

    void onResume();

    void onRestart();

    void onPause();

    void onStop();

    void onDestroy();

    void onActivityResult(int requestCode, int resultCode, Intent data);
}

PluginBaseActivity 抽象类

/**
 * 插件Activity的基类,插件中的所有Activity,都要继承它
 */
public abstract class PluginBaseActivity extends AppCompatActivity implements IPlugin {

    private final String TAG = "PluginBaseActivityTag";
    protected Activity proxy;//上下文

    //这里基本上都在重写原本Activity的函数,因为 要兼容“插件单独测试” 和 "集成到宿主整体测试",所以要进行情况区分
    private int from = IPlugin.FROM_INTERNAL;//默认是“插件单独测试”

    @Override
    public void attach(Activity proxyActivity) {
        proxy = proxyActivity;
    }

    @Override
    public void onCreate(Bundle saveInstanceState) {
        if (saveInstanceState != null)
            from = saveInstanceState.getInt(PluginApkConst.TAG_FROM);

        if (from == IPlugin.FROM_INTERNAL) {
            super.onCreate(saveInstanceState);
            proxy = this;//如果是从内部跳转,那就将上下文定为自己
        }
    }

    @Override
    public void onStart() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onStart();
        } else {
            Log.d(TAG, "宿主启动:onStart()");
        }
    }

    @Override
    public void onResume() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onResume();
        } else {
            Log.d(TAG, "宿主启动:onResume()");
        }
    }

    @Override
    public void onRestart() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onRestart();
        } else {
            Log.d(TAG, "宿主启动:onRestart()");
        }
    }

    @Override
    public void onPause() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onPause();
        } else {
            Log.d(TAG, "宿主启动:onPause()");
        }
    }

    @Override
    public void onStop() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onStop();
        } else {
            Log.d(TAG, "宿主启动:onStop()");
        }
    }

    @Override
    public void onDestroy() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onDestroy();
        } else {
            Log.d(TAG, "宿主启动:onDestroy()");
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onActivityResult(requestCode, resultCode, data);
        } else {
            Log.d(TAG, "宿主启动:onActivityResult()");
        }
    }

    //下面是几个生命周期之外的重写函数
    @Override
    public void setContentView(int layoutResID) {//设置contentView分情况
        if (from == IPlugin.FROM_INTERNAL) {
            super.setContentView(layoutResID);
        } else {
            proxy.setContentView(layoutResID);
        }
    }

    @Override
    public View findViewById(int id) {
        if (from == FROM_INTERNAL) {
            return super.findViewById(id);
        } else {
            return proxy.findViewById(id);
        }
    }

    @Override
    public void startActivity(Intent intent) {//同理
        if (from == IPlugin.FROM_INTERNAL) {
            super.startActivity(intent);//原intent只能用于插件单独运行时
        } else {
            // 如果是集成模式下,插件内的跳转,控制权 仍然是在宿主上下文里面,所以--!
            // 先跳到代理Activity,由代理Activity展示真正的Activity内容
            Intent temp = new Intent(proxy, ProxyActivity.class);
            temp.putExtra(PluginApkConst.TAG_CLASS_NAME, intent.getComponent().getClassName());
            proxy.startActivity(temp);//这里不能跳原来的intent,,必须重新创建
        }
    }

}

PluginBaseActivity 抽象类中,3个重点需要特别说明

1. 插件module需要单独测试,也需要 作为插件来集成测试,所以这里IPlugin接口中定义了 FROM_INTERNAL和FROM_EXTERNAL 进行情形区分.

2. 除了IPlugin必须实现的一些生命周期方法之外,最后我还新增了3个方法:
setContentViewfindViewByIdstartActivity
设置布局,寻找组件,跳转Activity,也是需要区分 单测还是集成测试的,所以,也要做if/else判定.
并且[看3.]

3. 上面说的startActivity,当从外部跳转,也就是宿主来启动插件Activity的时候,也只能跳到ProxyActivity,然后把真正的目标Activity放在参数中.


5.如何使用Demo

image.png

我的demo中已经有了一个插件apk,如上图.
如果你更改了plugin_module的内容,请重新生成一个apk,放到上图所示位置,文件名必须和外壳app内写的一样.放好之后,运行外壳app即可。


6.最终效果展示

集成测试,由外壳app启动插件Activity


插件化集成测试.gif

插件单独测试


插件单独测试.gif

结语

可能有人说,目前自己公司的app还用不着插件化这种重量级的技术,如果这样说的人,真的是这么想,那么可能永远只能做小app了。做技术如果不想做大做强,那和咸鱼有什么区别,o(╯□╰)o
另有问题需要探讨,或者发现错误,欢迎给我留言!
技术之路漫漫长,码字不易,希望看到的客官点个好评,谢谢支持.
Demo地址


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

推荐阅读更多精彩内容

  • 嗯哼嗯哼蹦擦擦~~~ 转载自:https://github.com/Tim9Liu9/TimLiu-iOS 目录 ...
    philiha阅读 4,835评论 0 6
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 秋风乍起夜未央,海河丽景伴在旁, 情怀满斟杯中酒,热语欢声意飞扬。
    幻儿11阅读 213评论 0 1
  • 江湖不仅神棍多,神一样的户型也不少。相信大家看了以后,可能都无法想象这些户型是怎么一层一层的通过设计公司、地产公司...
    盖帮贵州阅读 478评论 0 3
  • 霓虹听起来就色彩斑斓字眼 霓虹灯是温暖的,可爱的 让人想到家 想到节日 生日与家人相聚的快乐 那些属于小时候的记忆
    很深的绿阅读 379评论 0 0