Android消息机制分析

在初学Android的时候我们都碰过这个问题,要从服务端获取数据,这时候,我们知道在主线程不能做耗时操作,否则会引起ANR,更不能在主线程中联网,Android3.0以后会报一异常,或者在子线程中更新UI报出了一个经典异常,我们都知道解决方法是在子线程用Handler发消息给主线程进行更新就可以解决上述两个问题,但是我们会不会疑惑,Handler干了什么事情就可以自由切换线程执行,由于Android帮我们封装好了我们日常开发的时候只要跟Handler打交道就可以办成这些事,但是了解整个Android消息机制的原理有助于提升自己对整个Android的了解,所以我就写了这个博客,把我自己对Android消息传递机制的理解记录下来。

Android消息机制简叙
Android的消息机制主要指Handler、MessengeQueue和Looper的工作机制,有时我们会发现当我们主线程中进行联网操作时,会发现可能在2.3运行时是正常,但是在3.0以后的版本之后运行就会报一个android.os.NewWorkOnMainTHreadException,这是因为Android在API Level9之后就对这个进行了处理,如下所示。

/** 
         * For apps targetting SDK Honeycomb or later, we don't allow 
         * network usage on the main event loop / UI thread. 
         * 
         * Note to those grepping:  this is what ultimately throws 
         * NetworkOnMainThreadException ... 
         */  
        if (data.appInfo.targetSdkVersion > 9) {  
            StrictMode.enableDeathOnNetwork();  
        }  

Android的设计者并不希望也不允许我们在主线程进行进行联网操作,接着就是在子线程中进行更新UI的操作,我举个栗子,如下所示:

public class MainActivity extends AppCompatActivity {  
  
    private TextView tv;  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        tv = (TextView) findViewById(R.id.tv_hello);  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
//                SystemClock.sleep(3000);  
                    tv.setText("aa" + i);  
            }  
        }).start();  
    }  
      
}  

我们发现这时候竟然是可以运行,不是说不能在子线程中更新UI吗,其实是这样的这个线程是在onCreate里创建的,当线程运行结束的时候界面还没有出来,所以就可以更新UI了,要证明这个只要在子线程添加一个sleep方法睡一下,之后就会报出这个经典异常:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
这是由于在ViewRootImpl的checkThread方法进行了验证,如下:

void checkThread() {  
        if (mThread != Thread.currentThread()) {  
            throw new CalledFromWrongThreadException(  
                    "Only the original thread that created a view hierarchy can touch its views.");  
        }  
    } 

这个异常基本上是每个Android开发者都曾碰过的,其实想想Android这样设计也是很靠谱的,我们知道Android的UI控件并不是线程安全的,如果多线程并发访问可能会产生不可预估的问题,举个栗子,比如一款游戏,打怪掉血跟吃血瓶操作都放在了子线程中进行,他们也都要更新血条这个UI,这时候如果子线程可以操作血条,那血一直掉,用户就吃了血瓶,但是我们知道线程是有不确定性的,有可能我是先吃的血瓶但是线程执行却没有掉血的快,等到血瓶执行更新UI的时候角色已经死了等等...,由于上面的两个矛盾,如果交由我们自己处理怕是处理不来,所以Android提供了一套基于Handler的运行机制去解决在子线程中无法访问UI的矛盾。还是上面这个例子,当角色被怪打了,这时掉血线程发个信息告诉UI线程让UI线程去更新血条,喝血瓶这个线程要更新血条也发信息让UI线程去更新,这样就解决了子线程并发修改UI控件的矛盾了。
虽然使用Handler进行UI更新操作并不复杂,但是还是用个示例来说明:

public class MainActivity extends AppCompatActivity {  
    private ProgressBar pb;  
    private TextView tvPb;  
    private static final int FLAGS = 1;  
  
    private Handler mHandler = new Handler() {  
        @Override  
        public void handleMessage(Message msg) {  
            switch (msg.what) {  
                case FLAGS:  
                    pb.setProgress(msg.arg1);  
                    tvPb.setText(String.valueOf(msg.arg1));  
                    break;  
                default:  
                    break;  
            }  
  
        }  
    };  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        pb = (ProgressBar) findViewById(R.id.pb);  
        tvPb = (TextView) findViewById(R.id.tv_pb);  
  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                for (int i = 0; i <= 100; i++) {  
                    Message msg = mHandler.obtainMessage();  
                    SystemClock.sleep(200);  
                    msg.what = FLAGS;  
                    msg.arg1 = i;  
                    mHandler.sendMessage(msg);  
                }  
            }  
        }).start();  
    }  
}  

示例很简单,就是通过Handler去sendMessage把要更新progressBar和TextView的操作发送给handleMessage来执行,当然有一点就是Message对象可以自己new出来也可以使用handler.obtainMessage去获取这个对象,因为使用.obtainMessage去获取一个对象的性能比new出来的要好,是因为.obtainMessage是从消息池中取出(如果有),而不是每次都直接new,省去了创建对象的内存消耗,所以就用了这个方法效果如图

20160524154105927.png

Android消息机制分析
前面说了Android消息机制主要指Handler及其附带的MessageQueue、Looper以及Message的工作流程,下面一个个分析

(1).Message
消息,里面可以包含消息处理对象和处理数据等等,由MessageQueue进行统一的排列,然后交由Handler进行处理.


(2).MessageQueue
MessageQueue主要有插入和读取两个操作,
插入:在MessageQueue中插入操作由enqueueMessage这个方法来执行,enqueueMessage方法源码如下:

boolean enqueueMessage(Message msg, long when) {  
    if (msg.target == null) {  
        throw new IllegalArgumentException("Message must have a target.");  
    }  
    if (msg.isInUse()) {  
        throw new IllegalStateException(msg + " This message is already in use.");  
    }  
  
    synchronized (this) {  
        if (mQuitting) {  
            IllegalStateException e = new IllegalStateException(  
                    msg.target + " sending message to a Handler on a dead thread");  
            Log.w(TAG, e.getMessage(), e);  
            msg.recycle();  
            return false;  
        }  
  
        msg.markInUse();  
        msg.when = when;  
        Message p = mMessages;  
        boolean needWake;  
        if (p == null || when == 0 || when < p.when) {  
            // New head, wake up the event queue if blocked.  
            msg.next = p;  
            mMessages = msg;  
            needWake = mBlocked;  
        } else {  
            // Inserted within the middle of the queue.  Usually we don't have to wake  
            // up the event queue unless there is a barrier at the head of the queue  
            // and the message is the earliest asynchronous message in the queue.  
            needWake = mBlocked && p.target == null && msg.isAsynchronous();  
            Message prev;  
            for (;;) {  
                prev = p;  
                p = p.next;  
                if (p == null || when < p.when) {  
                    break;  
                }  
                if (needWake && p.isAsynchronous()) {  
                    needWake = false;  
                }  
            }  
            msg.next = p; // invariant: p == prev.next  
            prev.next = msg;  
        }  
  
        // We can assume mPtr != 0 because mQuitting is false.  
        if (needWake) {  
            nativeWake(mPtr);  
        }  
    }  
    return true;  
}  

从源码可以看出,这个方法主要就是对单链表进行插入操作,当然我们平常开发可能会碰到一个异常就是从这里报的,比如第一个例子

new Thread(new Runnable() {  
         @Override  
         public void run() {  
             for (int i = 0; i <= 100; i++) {  
                 SystemClock.sleep(200);  
                 Message msg = new Message();  
                 msg.what = FLAGS;  
                 msg.arg1 = i;  
                 mHandler.sendMessage(msg);  
             }  
         }  
     }).start();  

当我们把

Message msg = mHandler.obtainMessage();  

这段代码放到for循环外面时,就会报
Java.lang.IllegalStateException: { when=-205ms what=1 arg1=1 target=com.sjr.handlerdemo.MainActivity$1 } This message is already in use.
这个异常,这是因为当把代码放到循环体外面的时候sendMessage可能会和handleMessage并发同时操作一个对象,所以Android直接抛了这个异常出来,当然这是题外话。
读取:读取操作的执行方法是next方法,next的源码如下:

Message next() {  
        // Return here if the message loop has already quit and been disposed.  
        // This can happen if the application tries to restart a looper after quit  
        // which is not supported.  
        final long ptr = mPtr;  
        if (ptr == 0) {  
            return null;  
        }  
  
        int pendingIdleHandlerCount = -1; // -1 only during first iteration  
        int nextPollTimeoutMillis = 0;  
        for (;;) {//无限循环  
            if (nextPollTimeoutMillis != 0) {  
                Binder.flushPendingCommands();  
            }  
  
            nativePollOnce(ptr, nextPollTimeoutMillis);  
  
            synchronized (this) {  
                // Try to retrieve the next message.  Return if found.  
                final long now = SystemClock.uptimeMillis();  
                Message prevMsg = null;  
                Message msg = mMessages;  
                if (msg != null && msg.target == null) {  
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.  
                    do {  
                        prevMsg = msg;  
                        msg = msg.next;  
                    } while (msg != null && !msg.isAsynchronous());  
                }  
                if (msg != null) {  
                    if (now < msg.when) {  
                        // Next message is not ready.  Set a timeout to wake up when it is ready.  
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);  
                    } else {  
                        // Got a message.  
                        mBlocked = false;  
                        if (prevMsg != null) {  
                            prevMsg.next = msg.next;  
                        } else {  
                            mMessages = msg.next;  
                        }  
                        msg.next = null;  
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);  
                        msg.markInUse();  
                        return msg;  
                    }  
                } else {  
                    // No more messages.  
                    nextPollTimeoutMillis = -1;  
                }  
  
                // Process the quit message now that all pending messages have been handled.  
                if (mQuitting) {  
                    dispose();  
                    return null;  
                }  
  
                // If first time idle, then get the number of idlers to run.  
                // Idle handles only run if the queue is empty or if the first message  
                // in the queue (possibly a barrier) is due to be handled in the future.  
                if (pendingIdleHandlerCount < 0  
                        && (mMessages == null || now < mMessages.when)) {  
                    pendingIdleHandlerCount = mIdleHandlers.size();  
                }  
                if (pendingIdleHandlerCount <= 0) {  
                    // No idle handlers to run.  Loop and wait some more.  
                    mBlocked = true;  
                    continue;  
                }  
  
                if (mPendingIdleHandlers == null) {  
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];  
                }  
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);  
            }  
  
            // Run the idle handlers.  
            // We only ever reach this code block during the first iteration.  
            for (int i = 0; i < pendingIdleHandlerCount; i++) {  
                final IdleHandler idler = mPendingIdleHandlers[i];  
                mPendingIdleHandlers[i] = null; // release the reference to the handler  
  
                boolean keep = false;  
                try {  
                    keep = idler.queueIdle();  
                } catch (Throwable t) {  
                    Log.wtf(TAG, "IdleHandler threw exception", t);  
                }  
  
                if (!keep) {  
                    synchronized (this) {  
                        mIdleHandlers.remove(idler);  
                    }  
                }  
            }  
  
            // Reset the idle handler count to 0 so we do not run them again.  
            pendingIdleHandlerCount = 0;  
  
            // While calling an idle handler, a new message could have been delivered  
            // so go back and look again for a pending message without waiting.  
            nextPollTimeoutMillis = 0;  
        }  
    }  

从源码可以看到,next方法里面有个死循环方法,当消息队列里没有消息时next方法就会一直阻塞,当有消息时就返回这条消息然后将消息从消息队列中移除。
(3).Looper
Looper在这套消息机制里面可以称为一个轮询器,它会不断的从MessageQueue中轮询查看是否有新消息,如果有新消息这个轮询器就会处理,创建一个Looper的方法为Looper.prepare();源码中Looper创建的方法为:

/** 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);  
    } 

然后它进行了一系列的逻辑操作来创建一个Looper,介于篇幅有限就不再详细点进去研究了,当然主线程有一个专门的创建方法prepareMainLooper,它最终也是通过prepare来实现,由于主线程Looper比较特殊,所以Android帮我们封装了一个方法,通过getMainLooper可以获取到主线程的Looper。如果不需要使用Looper时应该终止它,通过quit()或quitSafely()方法可以终止Looper,两个方法的区别是一个前者一旦调用就直接退出,后者是把消息队列中存在的消息处理完之后才会退出。
我们前面说了Looper会一直轮询MessageQueue,接下来的方法就是起轮询作用的方法,有了这个方法Looper才会去轮询,这个方法是loop();源码为:

/** 
     * Run the message queue in this thread. Be sure to call 
     * {@link #quit()} to end the loop. 
     */  
    public static void loop() {  
        final Looper me = myLooper();  
        if (me == null) {  
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");  
        }  
        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  
            Printer logging = me.mLogging;  
            if (logging != null) {  
                logging.println(">>>>> Dispatching to " + msg.target + " " +  
                        msg.callback + ": " + msg.what);  
            }  
  
            msg.target.dispatchMessage(msg);  
  
            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();  
        }  
    }  

下面是源码的大略解析首先是

final Looper me = myLooper();  

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();  
   }  

我们发现他调用的是ThreadLocal的get方法,ThreadLocal是一个线程内部数据存储类,通过它可以在指定线程中存储数据然后只能在只能的线程中才能获取存储的数据。它通过get和set方法去获取和设置当前线程的localValues对象的table数组,由于它们对ThreadLocal的读/写操作仅限于各自线程内部,所以ThreadLocal可以在多个线程中互不干扰地存储和修改数据。这个类在我们平常开发中用得比较少,这里只是因为涉及到其中的get方法就提一下,get方法源码为:

/** 
    * Returns the value of this variable for the current thread. If an entry 
    * doesn't yet exist for this variable on this thread, this method will 
    * create an entry, populating the value with the result of 
    * {@link #initialValue()}. 
    * 
    * @return the current value of the variable for the calling thread. 
    */  
   @SuppressWarnings("unchecked")  
   public T get() {  
       // Optimized for the fast path.  
       Thread currentThread = Thread.currentThread();  
       Values values = values(currentThread);  
       if (values != null) {//如果不为空就取出它的table数组  
           Object[] table = values.table;  
           int index = hash & values.mask;  
           if (this.reference == table[index]) {//找出ThreadLocal的reference对象在table数组中的位置  
               return (T) table[index + 1];  
           }  
       } else {  
           values = initializeValues(currentThread);//如果对象为空就返回初始值  
       }  
  
       return (T) values.getAfterMiss(this);  
   }  

从源码可以看出,这是一个取出当前线程的localValues对象,如果这个对象不为空就取出它的table数组并找出ThreadLocal的reference对象在数组中的额位置,table数组中的下一个位置所存储的数据就是ThreadLocal的值。回到loop()的源码,可以看到如果ThreadLocal对象为空Android直接抛出一个异常,然后是一个死循环,跳出循环的方法是只能是MessageQueue.next()返回null,当Looper的quit或quitSafely方法被调用时,这个方法会调用MessageQueue的quit方法来让消息队列退出,当消息队列为退出状态时,它的next方法返回null,然后loop中的死循环就会终止,如果MessageQueue的next方法有新消息Looper就会通过

msg.target.dispatchMessage(msg);  

去处理这条消息,这里的msg.target是发送这条消息的Handler对象,所以通过这段代码我们就可以解开我们前面的疑惑,为什么同一个Handler对象可以在子线程中发送消息然后在主线程中通过自己的handleMessage去处理这条消息,然后这里不同的是,Handler的dispatchMessage方法是在创建Handler时所使用的Looper执行的,这样就可以将代码逻辑切换到指定的线程中执行了。
(4).Handler
Handler是消息的处理者,它的工作主要是消息的发送和接收以及处理,下面是Handler对消息插入的处理,就是一系列senMessage方法的源码:

public final boolean sendMessage(Message msg)  
    {  
        return sendMessageDelayed(msg, 0);  
    }  
  
    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);  
    }  
  
  
    public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {  
        Message msg = Message.obtain();  
        msg.what = what;  
        return sendMessageAtTime(msg, uptimeMillis);  
    }  
  
     
    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;  
        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);  
    }  
  
    
    public final boolean sendMessageAtFrontOfQueue(Message msg) {  
        MessageQueue queue = mQueue;  
        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, 0);  
    }  

从源码可以看到,send方法最后返回的都是enqueueMessage这个方法,这个方法的源码为:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {  
       msg.target = this;  
       if (mAsynchronous) {  
           msg.setAsynchronous(true);  
       }  
       return queue.enqueueMessage(msg, uptimeMillis);  
   }  

最后返回的是MessageQueued的enqueueMessage方法去往消息队列中插入一条消息,这就是Handler的sendMessage能够插入消息的原理了,然后MessageQueue的next方法发现有消息了就停止阻塞返回这条消息给Looper,Looper收到消息之后就开始进行处理,最后交由Handler的dispatchMessage方法去处理。dispatchMessage的源码为:

/** 
   * Handle system messages here. 
   */  
  public void dispatchMessage(Message msg) {  
      if (msg.callback != null) {  
          handleCallback(msg);  
      } else {  
          if (mCallback != null) {  
              if (mCallback.handleMessage(msg)) {  
                  return;  
              }  
          }  
          handleMessage(msg);  
      }  
  }  

通过这个方法就可以对消息进行处理了,这个方法的逻辑是先检查Message的callback方法是否为null,不为null就调用handleCallback这个方法来处理,Callback是一个接口:

/** 
    * Callback interface you can use when instantiating a Handler to avoid 
    * having to implement your own subclass of Handler. 
    * 
    * @param msg A {@link android.os.Message Message} object 
    * @return True if no further handling is desired 
    */  
   public interface Callback {  
       public boolean handleMessage(Message msg);  
   } 

通过这个接口我们可以初始化一个Handler而且不需要实现Handler的子类,比如前面第一个例子,我们是new了一个Handler类然后重写了handleMessage方法,而Callback这个接口是另一种使用Handler的方式.
回到上面的dispatchMessage方法然后会判断当前Handler的Callback是不是为null,如果不为null就接着判断它的handleMessage方法是否为true,如果为true就直接return,最后调用handleMessage方法,这是空的方法,我们调用时一般是重写这个犯法实现具体的业务逻辑。

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

推荐阅读更多精彩内容