Handler 我必须知道的一切

Android提供的 Handler 消息收发处理机制,其根本目的就是解决多线程并发的问题,与之关联的 LooperMessageMessageQueue,无论是日常开发或者面试都是出场率极高,所以无论如何都必须搞清楚

Handler

导包要注意啦,package android.os

Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
    }
};

这是我们通常创建一个 Handler 的方法,Handler 内部创建调用实现:

public Handler() {
    this(null, false);
}

再跟进 Handler(Callback callback, boolean async) 方法实现:

public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " + klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
    }
  • FIND_POTENTIAL_LEAKS 是Handler中设置的一个标志,如果将这个值设置为true用于检测扩展继承Handler类是否存在潜在的内存泄漏
  • Looper.myLooper() 由于是默认创建的handler,所以会自动绑定主线程的Looper,并将主线程Looper维护的消息队列(mLooper.mQueue)作为自己(Handler)的消息走向(往消息队发送消息)
  • callback 用于是否拦截发送的消息(后面会解释)
  • mAsynchronous 的作用是设置消息是否异步,这意味着发送的消息将不受Looper同步设置的影响

至此,通过 new Handler() 方法创建handler就完成了(关于Looper,后面会详解)

创建 Handler 实例 → 绑定主线程的 Looper(只是默认绑定主线程,也可以调用其他构造方法指定 Looper) → 连接绑定 Looper 所维护的 MessageQueue → 设置消息拦截回调和消息是否异步

那Handler怎么发送消息?
通常情况,我们会使用 handler.sendMessage() 来发送消息,如下:

Message message = handler.obtainMessage();
message.agr1 = 1;
....  //message信息携带操作
handler.sendMessage(message);

这样消息就发送出去了,只要重写了handler的 handleMessage(Message message) 便可以接收到消息进行自己的逻辑处理。接下来看一下细节实现:

  • handler.obtainMessage() 会返回一个Message对象
public final Message obtainMessage()
{
    return Message.obtain(this);
}

继续跟进Message的 obtain(this) 方法

public static Message obtain(Handler h) {
    Message m = obtain();
    m.target = h;
    return m;
}

obtain() 方法,从全局池中返回一个新的 Message 对象实例,可以在很多情况下避免分配新的对象。并给这个消息对象设置了 target 属性,指定这个消息由谁发送并由谁处理。下面是 obtain() 方法:

/**
 * Return a new Message instance from the global pool. Allows us to
 * avoid allocating new objects in many cases.
 */
public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

这样,我们就拿了一个指定了消息发送和处理者的 Message,它的消息携带后面再说。当然,也可以不用 obtainMessage() 方法来获取消息对象,完全可以自己 new Message() 对象来携带信息并发送,但是 obtain() 方法的注释也说明了,有更好的方法为什么不用呐

  • sendMessage(Message msg) 发送消息
public final boolean sendMessage(Message msg)
{
    return sendMessageDelayed(msg, 0);
}

调用了 sendMessageDelayed(msg, 0) 方法

public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
    //处理了时间不正确的情况,自己写代码的时候也应该要考虑完善各种情况
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}


public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    //默认构造方法中设置了关联主线程Looper的消息队列,所以如果消息队列为空则抛出异常
    if (queue == null) {
        RuntimeException e = new RuntimeException(this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}


private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    //再次确认消息的发送和处理者,所以即便自己 new Message() 忘了设置target也不用担心
    msg.target = this;
    //这里用到了默认构造方法中设置的 async 属性,设置消息是否异步
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

queue.enqueueMessage(msg, uptimeMillis) 方法有点复杂,这里就不贴了,它做的事情就是根据 msguptimeMillis 与消息队列里现有的消息比较,决定消息插入队列的位置

当然,也可以直接调用 sendMessageDelayed(Message msg, long delayMillis) 来延时发送消息

OK,现在消息已经发送到消息队列里面了,先处理完一些遗漏的地方,并先来看看 Handler是怎么在 handleMessage(Message msg) 方法中处理的,后面再说它怎么拿到消息 (剧透:Looper的死循环)

Handler的其他构造方法
Handler 有很多构造方法,在创建 Handler 的时候也可以使用 new Handler(Callback callback) 来创建。由上面的分析可见,new Handler() 默认创建的时候,调用了this(null, false) 方法,默认创建一个空的 callback ,那 callback 到底有什么作用?

首先得知道,CallbackHandler 内部的一个接口,它包含一个返回 boolean 值的 handleMessage(Message msg) 方法,区别于 HandlerhandleMessage(Message msg)(这个是不带返回值的)

public interface Callback {
    public boolean handleMessage(Message msg);
}

来创建一个带 Callbackhandler

Handler handler = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        return false;
    }
}){
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
    }
};

调用的是 Handler(Callback callback) 方法来创建,传入了一个 callback 并重写 handlerMessage(Message msg)

public Handler(Callback callback) {
    this(callback, false);
}

还是调用的 Handler(Callback callback, boolean async) 方法来创建 Handler,不清楚看上面的分析。

好了,到了 callback 的作用了,这个方法其实就是在处理消息的时候用到的,先来看一下 HandlerhandleMessage(Message msg) 方法(),它负责处理接收到的消息,也是我们经常需要重写的方法,那 HandlerhandleMessage(Message msg) 是怎么被触发的,答案就在 Looper 的死循环中 --> msg.target.dispatchMessage(msg)(后面详解)
Messagetarget 上面已经说明了,它指定了消息发送和处理的 Handlermsg.target.dispatchMessage(msg) 这句话就是触发 Handler 进行消息处理,先上源码

/**
 * Handle system messages here.
 */
public void dispatchMessage(Message msg) {
    //首先检查msg是否设置了回调
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            //重点在这里,callback的handleMessage()方法的返回值决定了是否拦截消息
            //重点在这里,callback的handleMessage()方法的返回值决定了是否拦截消息
            //重点在这里,callback的handleMessage()方法的返回值决定了是否拦截消息
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

重要的事情说三遍,拦截以后的消息,不会调用 HandlerhandleMessage(Message msg)方法,所以如果你需要对某些特定消息进行拦截,可以给一个 callback,并根据消息携带的信息来决定是否拦截

Handler的其他用法

  • Handler 不仅能发送 Message 对象,还能 post(Runnable runnable)到消息队列中
/**
 * Causes the Runnable r to be added to the message queue
 * The runnable will be run on the thread to which this handler is 
 * attached. 
 *  
 * @param r The Runnable that will be executed.
 * 
 * @return Returns true if the Runnable was successfully placed in to the 
 *         message queue.  Returns false on failure, usually because the
 *         looper processing the message queue is exiting.
 */
public final boolean post(Runnable r)
{
   return  sendMessageDelayed(getPostMessage(r), 0);
}


private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

注释清晰明了
可能上面 dispatchMessage() 的时候还会有疑惑,为什么 Message 也有 Callback?为什么 Runnable 能直接扔到消息队列中? 。现在算是明白了。尽管 post() 出去的是一个 RunnableHandler 自己帮我们把它封装成了 Message

Handler post Runnable 后是直接把 run() 中的代码送回到了 Handler 附属的线程,通常就是创建 Handler 的主线程,所以我们经常使用 handler.post() 在子线程中更新UI

当然,也可以使用 postDelayed(Runnable r, long delayMillis) 来延时发送,其实它最终还是调用的 sendMessageDelayed(getPostMessage(r), delayMillis)

  • Handler 发送 空的 Message 对象
/**
 * Sends a Message containing only the what value.
 */
public final boolean sendEmptyMessage(int what)
{
    return sendEmptyMessageDelayed(what, 0);
}

public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
    Message msg = Message.obtain();
    msg.what = what;
    return sendMessageDelayed(msg, delayMillis);
}

最终也会调用我们熟悉的方法

空消息只会附带一个 int 类型的 what 值,可以用来进行一些特殊类型或者拦截消息的判断

(Message消息携带后面详解)

//////////////////////////////////////// 华丽的分割线 ////////////////////////////////////////
//////////////////////////////////////// 华丽的分割线 ////////////////////////////////////////
//////////////////////////////////////// 华丽的分割线 ////////////////////////////////////////

Looper

Looper 的理解现在还不是很到位,知道多少先记录多少
开始之前要扯一点其他的东西,做Android开发也这么久了,一直都知道Android是基于JAVA的 (当然现在Kotlin才是亲儿子了),也知道每个JAVA程序都是有一个主入口 main()的,之前却一直不知道Android程序的主入口在哪里,就只知道通过 AndroidMamifest.xml 配置好了 intent-filter 就可以启动 activity,很尴尬......

那Android的主入口到底在哪里?首先得知道 ActivityThread,好吧,其实我也不知道,附上传送门

Android ActivityThread(主线程或UI线程)简介

ActivityThread 它管理应用进程的主线程的执行(相当于普通Java程序的main入口函数),并根据AMS的要求(通过IApplicationThread接口,AMS为Client、ActivityThread.ApplicationThread和Server)负责调度和执行activities、broadcasts和其它操作。

主线程既要处理Activity组件的UI事件,又要处理Service后台服务工作,通常会忙不过来。为了解决此问题,主线程可以创建多个子线程来处理后台服务工作,而本身专心处理UI画面的事件。

【主线程】的主要责任:
• 快速处理UI事件。而且只有它才处理UI事件, 其它线程还不能存取UI画面上的对象(如TextView等),此时, 主线程就叫做UI线程。基本上,Android希望UI线程能根据用户的要求做出快速响应,如果UI线程花太多时间处理后台的工作,当UI事件发生时,让用户等待时间超过5秒而未处理,Android系统就会给用户显示ANR提示信息。只有UI线程才能执行View派生类的onDraw()函数。
• 快速处理Broadcast消息。【主线程】除了处理UI事件之外,还要处理Broadcast消息。所以在BroadcastReceiver的onReceive()函数中,不宜占用太长的时间,否则导致【主线程】无法处理其它的Broadcast消息或UI事件。如果占用时间超过10秒, Android系统就会给用户显示ANR提示信息。

注意事项:
• 尽量避免让【主线程】执行耗时的操作,让它能快速处理UI事件和Broadcast消息。
• BroadcastReceiver的子类都是无状态的,即每次启动时,才会创建其对象,然后调用它的onReceive()函数,当执行完onReceive()函数时,就立即删除此对象。由于每次调用其函数时,会重新创建一个新的对象,所以对象里的属性值,是无法让各函数所共享。

跑得有点远,但是这些都是我们应该知道或者熟悉的。之前又一次面试,被问到Framewark与应用层怎么通讯?当时一脸懵逼,看了传送门那篇文章,现在还能说个大概,C/S方式,AMS,Binder等等。下面进入正题,知道了Android管理应用进程的入口在 ActivityThread 中,那我们进去看一下(ActivityThreadmain() 方法)

public static void main(String[] args) {
        ......  //自动屏蔽其他不需要或者看不懂的代码
        Looper.prepareMainLooper();
        ......
        Looper.loop();
        ......    
}

好像发现了什么的感觉,Looper 出现了,初始化了一个主线程的 Looper,然后执行了它的 loop() 方法。我们可以这样认为,每个Android程序(进程)启动的时候,会自动创建一个 Looper 对象,并开启循环(调用 loop() 方法)拉取 MessageQueueLooper 对象中维护的消息队列)的消息。可能表述不是很准确,但是可以这样的理解。接着跟进里面的方法:

/**
 * Initialize the current thread as a looper, marking it as an
 * application's main looper. The main looper for your application
 * is created by the Android environment, so you should never need
 * to call this function yourself.  See also: {@link #prepare()}
 */
public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
}

注意最后一句注释:应用的主 Looper会由Android系统自动创建,因此我们不需要手动调用这个方法。现在好像也不明白这个方法干了什么,准备了什么,开启了一个同步锁保证线程安全,最后把 myLooper() 返回值赋给了 sMainLooper,继续跟进 prepare()

 /** Initialize the current thread as a looper.
  * This gives you a chance to create handlers that then reference
  * this looper, before actually starting the loop. Be sure to call
  * {@link #loop()} after calling this method, and end it by calling
  * {@link #quit()}.
  */
public static void prepare() {
    prepare(true);
}

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

这里看到了 new Looper(quitAllowed) 明白是创建了一个 Looper,再结合 myLooper() 一起解释:

/**
 * Return the Looper object associated with the current thread.  Returns
 * null if the calling thread is not associated with a Looper.
 */
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

到了这里,我们需要搞清楚 sThreadLocalsMainLooper 等属性到底是什么,先来看一下 Looper 类中最开始定义的几个变量:

// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static Looper sMainLooper;  // guarded by Looper.class

final MessageQueue mQueue;
final Thread mThread;
  • sThreadLocal 属性, ThreadLocal 这个东西应该翻译为:线程局部变量,不是线程!!!,它干了什么事?(变量是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本。)保证了 Looper 的线程安全,并保证每个线程使用的是同一个 Looper 的不同副本。(有点晦涩难懂,先大致理解概念就行)
  • sMainLooper 属性,应用程序的主 Looper
  • mQueue 属性,Looper 内部维护的消息队列
  • mThread 属性, Looper 所在的线程,即我们熟悉的UI线程(主线程)

好了,回到上面的方法流程:
prepareMainLooper() 调用了 prepare(false) 方法创建了一个不允许消息队列退出的 Looper (主线程 Looper 当然不允许退出,只要应用程序还在执行就会一直拉取消息或者阻塞,消息队列都没了去哪里拉取?),并将这个 Looper 复制给了本地线程局部变量 sThreadLocal 。随后设置主线程的 Looper 为刚才我们创建的 Looper

注意:throw new RuntimeException("Only one Looper may be created per thread") 这个异常,每个线程中只能存在一个 Looper每个线程中只能存在一个 Looper每个线程中只能存在一个 Looper (重要的事情说三遍)

Looper.prepareMainLooper() 方法执行完后,主线程的 Looper 就准备就绪了,接下来看看 Looper.loop() 在干什么:

 /**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
public static void loop() {
    //从该线程中取出对应的 Looper 对象
    final Looper me = myLooper();
    if (me == null) {
        //这里可以看到,必须先调用 Looper.prepare() 初始化
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    // 拿到Looper 对象维护的消息队列
    final MessageQueue queue = me.mQueue;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    //重点 重点 重点
    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        final long traceTag = me.mTraceTag;
        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        try {
            //调用消息绑定的Handler进行消息处理
            msg.target.dispatchMessage(msg);
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        // Make sure that during the course of dispatching the
        // identity of the thread wasn't corrupted.
        final long newIdent = Binder.clearCallingIdentity();
        if (ident != newIdent) {
            Log.wtf(TAG, "Thread identity changed from 0x"
                    + Long.toHexString(ident) + " to 0x"
                    + Long.toHexString(newIdent) + " while dispatching to "
                    + msg.target.getClass().getName() + " "
                    + msg.callback + " what=" + msg.what);
        }
        msg.recycleUnchecked();
    }
}

检查和判断后,便开启了一个死循环遍历消息,并调用 HandlerdispatchMessage(msg) 方法进行消息处理,这个我们上面追踪 Handler 的时候也提到了。至此 Looper 的工作便是正式开启了,一直到程序运行结束。

之前面试被问到:Looper.loop() 既然是开启了一个死循环,为什么没有把程序给搞死? 这是因为当 MessageQueue 里有消息的时候,Looper 会一直循环取消息,当
MessageQueue 为空的时候,Looper 所在的线程就会阻塞,直到再有消息进入消息队列,Looper 会再次被唤醒

要终止 Looper 循环取消息,可以调用 quit() 方法终止 loop()

//////////////////////////////////////// 华丽的分割线 ////////////////////////////////////////
//////////////////////////////////////// 华丽的分割线 ////////////////////////////////////////
//////////////////////////////////////// 华丽的分割线 ////////////////////////////////////////

Message

所在包:package android.os
Message 的源码相对来说要简单一些,主要包括了 Message 的创建,消息内容和信息的设置以及消息回收,这里简单介绍 Message 消息内容的设置,先来看它的几个属性:

   /**
     * User-defined message code so that the recipient can identify 
     * what this message is about. Each {@link Handler} has its own name-space
     * for message codes, so you do not need to worry about yours conflicting
     * with other handlers.
     */
    public int what;

    /**
     * arg1 and arg2 are lower-cost alternatives to using
     * {@link #setData(Bundle) setData()} if you only need to store a
     * few integer values.
     */
    public int arg1; 

    /**
     * arg1 and arg2 are lower-cost alternatives to using
     * {@link #setData(Bundle) setData()} if you only need to store a
     * few integer values.
     */
    public int arg2;

    /**
     * An arbitrary object to send to the recipient.  When using
     * {@link Messenger} to send the message across processes this can only
     * be non-null if it contains a Parcelable of a framework class (not one
     * implemented by the application).   For other data transfer use
     * {@link #setData}.
     * 
     * <p>Note that Parcelable objects here are not supported prior to
     * the {@link android.os.Build.VERSION_CODES#FROYO} release.
     */
    public Object obj;

注释也描述的很清楚了

  • what int 类型,可以用作消息唯一标识,或自行指定消息类型
  • arg1arg2 int类型,可以保存一些 int 型数据信息
  • object Object类型,这里就可以保存你想让 Message 携带的各种类型信息了

这些都是公共属性,直接访问和复制都可以

除了上面这些公共的属性,Message 还又如下属性:

    /*package*/ int flags;

    /*package*/ long when;
    
    /*package*/ Bundle data;
    
    /*package*/ Handler target;
    
    /*package*/ Runnable callback;
    
    // sometimes we store linked lists of these things
    /*package*/ Message next;
  • flags int 型 ,用于给消息做一些标记
  • data Bundle 类型,可以通过 getData()setData(Bundle data) 来获取和设值
  • when long 类型,有没有觉得有点熟悉?因为我们在 Handler 消息发送的时候说到过,通常用它来记录消息发送的延时时间
  • target 指定消息发送和处理的 Handler,即消息的来源和去向
  • callback Runnable 自定义消息回调

That`s all 如有遗漏,以后再补充..........

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

推荐阅读更多精彩内容