IPC在Toast中的應用

1、Toast概念和問題的引出

Toast 中文名"土司",應該算是 Android 使用頻率很高的一個 widget 了,一般使用它來做一些操作的提示信息,例如我們在淘寶點擊收藏寶貝之后底部就會出現"收藏成功"這樣的提示信息,這種就是 Toast 的功能了。

先拋出一個問題哦:那就是多個應用或者多個線程都要在顯示 Toast 的時候系統是怎么去處理這個問題的?是串行執行還是并行執行呢?

2、如何使用 Toast

Toast 的使用可以說是非常簡單,簡單到一句話就可以顯示一個 Toast 了,下面是具體的代碼體現。

Toast.makeText(this, "hello Toast", Toast.LENGTH_SHORT).show();

3、構造 Toast 對象

下面就依據上面的那句代碼分析 Toast 顯示到手機屏幕上是需要經過什么過程。

主要入口為兩個:

  • public static Toast makeText(Context context, CharSequence text, int duration)
  • public void show()

3.1、makeText 源碼分析

makeText 方法內部主要做了這樣幾件事:

  • 創建一個 Toast 對象;
  • 加載一個系統布局,這個布局就是用顯示這個 Toast 的;
  • 設置需要顯示的文本內容,就是我們在上面設置的 "hello Toast" 和設置這個 Toast 需要顯示的時間;

其實就根據傳入到 makeText 內部的幾個屬性構造出一個 Toast 對象。

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    Toast result = new Toast(context);
    LayoutInflater inflate = (LayoutInflater)
            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
    TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
    tv.setText(text);
    
    result.mNextView = v;
    result.mDuration = duration;
    return result;
}

3.2、Toast 構造方法

Toast 的構造方法內部很簡單,主要是創建一個 TN 對象,并且賦值給 mTN,這個 TN 對象非常重要,在下面分析中再慢慢說明它的作用。因為 TN 對象 是在 Toast 構造中創建的,因此一個 Toast 對象的創建就對應創建一個 TN 對象,這句話也很重要。

public Toast(Context context) {
    mContext = context;
    //創建一個 TN 對象
    mTN = new TN();
    //設置顯示的位置
    mTN.mY = context.getResources().getDimensionPixelSize(
            com.android.internal.R.dimen.toast_y_offset);
    mTN.mGravity = context.getResources().getInteger(
            com.android.internal.R.integer.config_toastDefaultGravity);
}

4、TN 類的分析

看 TN 類先看它的聲明,學過 AIDL 的同學應該都知道,TN 繼承了 ITransientNotification.Stub 類,說明它是遠程服務真正干活的類。這什么意思呢?其實是這樣的,當客戶端需要顯示一個土司,那么這個要求是要交給遠程服務 NotificationManagerService 去統一調度的【這里是第一次 IPC 過程:用戶進程訪問服務進程 NotificationManagerService 】。

而 NotificationManagerService 調度完成之后是怎么通知用戶進程去 show / hide 土司的呢?遠程服務進程通過 mTN 與用戶進程(當前進程)進行交互(show,hide等操作)【這里是第二次 IPC 過程:服務進程 NotificationManagerService 訪問 用戶進程 TN】。

private static class TN extends ITransientNotification.Stub {
    final Runnable mHide = new Runnable() {
        @Override
        public void run() {
            handleHide();
            // Don't do this in handleHide() because it is also invoked by handleShow()
            mNextView = null;
        }
    };
    private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams()
    final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            IBinder token = (IBinder) msg.obj;
            handleShow(token);
        }
    };
    int mGravity;
    int mX, mY;
    float mHorizontalMargin;
    float mVerticalMargin;
    View mView;
    View mNextView;
    int mDuration;
    WindowManager mWM;
    static final long SHORT_DURATION_TIMEOUT = 5000;
    static final long LONG_DURATION_TIMEOUT = 1000;
    TN() {
        // XXX This should be changed to use a Dialog, with a Theme.Toast
        // defined that sets up the layout params appropriately.
        final WindowManager.LayoutParams params = mParams;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.format = PixelFormat.TRANSLUCENT;
        params.windowAnimations = com.android.internal.R.style.Animation_Toast;
        params.type = WindowManager.LayoutParams.TYPE_TOAST;
        params.setTitle("Toast");
        params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
    }
    /**
     * schedule handleShow into the right thread
     */
    @Override
    public void show(IBinder windowToken) {
        if (localLOGV) Log.v(TAG, "SHOW: " + this);
        mHandler.obtainMessage(0, windowToken).sendToTarget();
    }
    /**
     * schedule handleHide into the right thread
     */
    @Override
    public void hide() {
        if (localLOGV) Log.v(TAG, "HIDE: " + this);
        mHandler.post(mHide);
    }
    public void handleShow(IBinder windowToken) {
        if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                + " mNextView=" + mNextView);
        if (mView != mNextView) {
            // remove the old view if necessary
            handleHide();
            mView = mNextView;
            Context context = mView.getContext().getApplicationContext();
            String packageName = mView.getContext().getOpPackageName();
            if (context == null) {
                context = mView.getContext();
            }
            mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
            // We can resolve the Gravity here by using the Locale for getting
            // the layout direction
            final Configuration config = mView.getContext().getResources().getConfigura
            final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDi
            mParams.gravity = gravity;
            if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL)
                mParams.horizontalWeight = 1.0f;
            }
            if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
                mParams.verticalWeight = 1.0f;
            }
            mParams.x = mX;
            mParams.y = mY;
            mParams.verticalMargin = mVerticalMargin;
            mParams.horizontalMargin = mHorizontalMargin;
            mParams.packageName = packageName;
            mParams.hideTimeoutMilliseconds = mDuration ==
                Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
            mParams.token = windowToken;
            if (mView.getParent() != null) {
                if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                mWM.removeView(mView);
            }
            if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
            mWM.addView(mView, mParams);
            trySendAccessibilityEvent();
        }
    }
    private void trySendAccessibilityEvent() {
        AccessibilityManager accessibilityManager =
                AccessibilityManager.getInstance(mView.getContext());
        if (!accessibilityManager.isEnabled()) {
            return;
        }
        // treat toasts as notifications since they are used to
        // announce a transient piece of information to the user
        AccessibilityEvent event = AccessibilityEvent.obtain(
                AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
        event.setClassName(getClass().getName());
        event.setPackageName(mView.getContext().getPackageName());
        mView.dispatchPopulateAccessibilityEvent(event);
        accessibilityManager.sendAccessibilityEvent(event);
    }        
    public void handleHide() {
        if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
        if (mView != null) {
            // note: checking parent() just to make sure the view has
            // been added...  i have seen cases where we get here when
            // the view isn't yet added, so let's try not to crash.
            if (mView.getParent() != null) {
                if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                mWM.removeViewImmediate(mView);
            }
            mView = null;
        }
    }
}

5、顯示一個Toast

  • 在 show 方法中首先判斷 mNextView 是否為空,這個 mNextView 就是加載出來需要展示 Toast 的布局啦,如果連布局都沒有,那么顯示什么土司,直接就 throw 了。

  • 獲取包名 pkg ,為什么需要包名呢,因為在遠程服務它會拿這個包名去判斷需要顯示的 Toast 是不是來至同一個進程的(一個進程對應一個包名),具體的判斷過程待會再分析。

  • 給 tn 賦值 mTN ,還記得這個 mTN 不?他就是在 Toast.makeText 內部方法中創建的(也就是在Toast構造中創建),我在前面說過一個 Toast 對象對應一個 mTN 對象。

  • 調用 IPC 通知遠程服務 NotificationServiceManager 去enqueueToast,這個過程非常重要。我們看到它將一個 tn 作為參數傳入,這是干嘛用呢?我們先前在介紹 TN 這個類時說過,這個類是用于進程間通訊的,因為顯示和隱藏 Toast 是需要由遠程的 NMS 去統一調度的,它內部維護一個集合存放一個個 Toast ,當到了指定時間可以顯示這個 Toast 了,那么 NMS 就可以通過 TN 這個代理類去通知客戶端去真正顯示一個 Toast 。

public void show() {
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }
    //獲取遠程服務 INotificationManager
    INotificationManager service = getService();
    //獲取包名 pkg 
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    try {
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        // Empty
    }
}

5.1、INotificationManager.Stub.Proxy.enqueueToast

service.enqueueToast(pkg, tn, mDuration) 這個方法的調用實際上是通過
INotificationManager的代理對象去執行的,具體執行代碼就在下面:主要就是將數據寫入到 Parcel _data 中,然后調用 mRemote.transact 進行 RPC 遠程調用操作,之后通過 _reply 獲取返回的數據。

//INotificationManager.Stub.Proxy.enqueueToast 代碼
public void enqueueToast(java.lang.String pkg, android.app.ITransientNotification callback, int duration) throws android.os.RemoteException
{
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        _data.writeString(pkg);
                //callback就是 tn 對象,這里需要傳遞給遠程服務,因此轉化為 Bidner 
        _data.writeStrongBinder((((callback!=null))?(callback.asBinder()):(null)));
        _data.writeInt(duration);
        mRemote.transact(Stub.TRANSACTION_enqueueToast, _data, _reply, 0);
        _reply.readException();
    }
    finally {
        _reply.recycle();
        _data.recycle();
    }
}

6、開始遠程服務調用 Toast

[ NMS 的源碼] (https://android.googlesource.com/platform/frameworks/base/+/f76a50c/services/java/com/android/server/NotificationManagerService.java) 有一個網站 grepcode 是可以查閱源碼的,有需要可以上這里看看源碼。

在 enqueueToast 方法內部主要做了這幾件事:

  • synchronize 加鎖判斷,這里為什么要加鎖?
  • indexOfToastLocked校驗操作;
  • 根據 indexOfToastLocked 的校驗結果進行后續的操作。
public void enqueueToast(String pkg, ITransientNotification callback, int duration) {

    //pkg 和 callback 的空校驗 代碼省略...

    //加鎖判斷
    synchronized (mToastQueue) {
        int callingPid = Binder.getCallingPid();
        long callingId = Binder.clearCallingIdentity();
        try {
            ToastRecord record;
            //查找隊列中是否由該 ToastRecord
            int index = indexOfToastLocked(pkg, callback);
            // If it's already in the queue, we update it in place, we don't
            // move it to the end of the queue.
            //表示在隊列中找到對應的 ToastRecord
            if (index >= 0) {
                //更新這個 ToastRecord 的顯示時間,這種情況是在同一個用用中,使用同一個 Toast 對象,在極短時間內多次調用show方法,就會出現這個情況。           
                record = mToastQueue.get(index);
                record.update(duration);
            } else {
                //沒有找到的情況
                // Limit the number of toasts that any given package except the android
                // package can enqueue.  Prevents DOS attacks and deals with leaks.
                if (!"android".equals(pkg)) {//不是系統彈的土司
                    int count = 0;
                    final int N = mToastQueue.size();
                    for (int i=0; i<N; i++) {
                         final ToastRecord r = mToastQueue.get(i);
                         if (r.pkg.equals(pkg)) {
                             count++;
                            //控制 toast 的次數 50
                             if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                 Slog.e(TAG, "Package has already posted " + count
                                        + " toasts. Not showing more. Package=" + pkg);
                                 return;
                             }
                         }
                    }
                }
                //沒有找到的情況就創建一個 ToastReord 對象。
                record = new ToastRecord(callingPid, pkg, callback, duration);
                //添加到隊列中。
                mToastQueue.add(record);
                
                index = mToastQueue.size() - 1;
                keepProcessAliveLocked(callingPid);
            }
            // If it's at index 0, it's the current toast.  It doesn't matter if it's
            // new or just been updated.  Call back and tell it to show itself.
            // If the callback fails, this will remove it from the list, so don't
            // assume that it's valid after this.
            if (index == 0) {
                showNextToastLocked();
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}

6.1、enqueueToast 加鎖判斷

這里要思考一下為什么這里要加鎖?我們知道鎖就是為了怕并發訪問出現數據的不同步問題,NMS 是系統服務,那么它的對象就只有一個,多個進程(多個 APP) 或者說這多個線程都可以獲取到這個服務,并且可以并發地去調用該服務的 enqueueToast 方法,這就像多個線程操作同一個對象的共享變量出現的數據不同步問題了,因此這里需要加鎖。還有就是 enqueueToast 方法是服務端的方法,它會在Binder 線程池中去調用。

6.2、indexOfToastLocked的校驗操作

檢測當前pag 包名和 callback 是否在 mToastQueue 中已經存在?在分析之前首先了解一下 callback 是什么呢?我們在先前創建 Toast 構造提到過,一個 Toast 對象會對應一個 mTN 對象。這個 callback 就是這個 mTN 對象的代理對象,為什么?因為它是需要進行進程間通訊的,因此需要將 mTN 對象轉化為 Binder 才能傳輸,而在 NMS 中獲取到這個 Binder 之后會將其轉化為 ITransientNotification.Proxy 代理對象。

該方法會返回一個整數,-1表示沒有找到,其他值表示找到了。

private int indexOfToastLocked(String pkg, ITransientNotification callback)
{
    IBinder cbak = callback.asBinder();
    ArrayList<ToastRecord> list = mToastQueue;
    int len = list.size();
    for (int i=0; i<len; i++) {
        ToastRecord r = list.get(i);
         //包名和binder都一樣時才表示是同一個 ToastRecord
        if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) {
            return i;
        }
    }
    return -1;
}

6.3、根據 indexOfToastLocked 的校驗結果進行后續的操作。

1.如果找到了那么返回值不為-1,那么就直接更新一下 duration 并且不會去修改該 ToastRecord 為 mToastQueue 中的位置。意思是什么呢?這里舉個栗子:這里 for 循環 500 次,每次都調用 toast.show() 方法,其運行結果只會顯示一次,為什么呢?因為我們在上面提到過,使用 indexOfToastLocaked 內部是根據 pkg 和 callback 進行判斷的,而這兩個值是一樣的,因此該方法返回的值不為 -1 ,因此就算是 show 500 次其 mToastQueue 中也只有一個 ToastRecord 對象。

Toast toast = Toast.makeText(this, "Toast", Toast.LENGTH_SHORT);
for (int i = 0; i < 500; i++) {
    toast.show();
}

2.如果找到的值為-1表示需要創建一個新的 ToastRecord 并且添加到 mToastQueue 中。因為之前得到的 index 為 -1 ,因此在這里需要重新計算 index = mToastQueue.size()-1,這個 index 就表示當前新創建 ToastRecord 在隊列中的位置,當這個位置為 0 時表示只有隊列中只有一個 ToastRecord ,直接執行 showNextToastLocked 即可。

3.注意這里,如果沒有知道的情況,并且同一個 pkg 下的 ToastRecord 的數量超過了 MAX_PACKAGE_NOTIFICATIONS 那么就直接 return 了,官方表示這是為了避免 DOS attacks。

4.這里注意下面這一種情況,執行結果是 toast 會執行 50 次,多余都被拋棄了,原因看上面第 3 點。但是重點這種寫法跟第 1 點的寫法的區別就是 Toast 的創建時機不一樣,這里是每循環一次就創建一個,也就是 Toast 的構造就會走一次,還記得上面說過 mTN 是在 Toast 構造創建的,每次調用 makeText 都會創建一個新的 Toast 對象,也就是 mTN 也是一個新的對象。那么傳遞給 NMS 的 mTN 就不一樣了,那么在進行 indexOfToastLocked 就會返回 -1 因此這里就是這段代碼的區別。

for (int i = 0; i < 500; i++) {
    Toast toast = Toast.makeText(this, "Toast", Toast.LENGTH_SHORT).show();
}

7、遠程服務端調度顯示和隱藏操作

7.1、showNextToastLocked()

這個方法主要做了一下幾件事:

  • 從隊列中取出一個 ToastRecord 并且通過 IPC 調用用戶進程的 TN 中的 show 方法真正去顯示一個 Toast 。

  • 通過 Handler 發送一個延時任務,時間為 duration ,在這個時間過后就取消顯示這個 Toast。

private void showNextToastLocked() {
    //從隊列中取出第一個 ToastRecord 對象
    ToastRecord record = mToastQueue.get(0);
    //while 循環是怕應用進程出現異常 RemoteException
    while (record != null) {
        if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
        try {
            //通知應用進程去執行顯示操作,實際上會調用 TN 的 show() 方法。
            record.callback.show();
            //在指定時間隱藏這個 Toast
            scheduleTimeoutLocked(record, false);
            return;
        } catch (RemoteException e) {
             //出現異常的情況,那么就從隊列中移除
            // remove it from the list and let the process die
            int index = mToastQueue.indexOf(record);
            if (index >= 0) {
                mToastQueue.remove(index);
            }
            keepProcessAliveLocked(record.pid);
            //從隊列中再取出下一個 ToastRecord 去執行。
            if (mToastQueue.size() > 0) {
                record = mToastQueue.get(0);
            } else {
                //如果沒有就置null,退出循環
                record = null;
            }
        }
    }
}

7.2、record.callback.show();

record.callback 就是用戶進程傳遞過來服務進程的 ITransientNotification 的代理對象,通過調用該代理對象的 show() 方法,這個過程是一個 IPC 的過程,這個最終會調用用戶進程的 TN 中的 show() 方法。

public void show() {
    //通過 handler 去 post 一個任務
    mHandler.post(mShow);
}

final Runnable mShow = new Runnable() {
    public void run() {
        handleShow();
    }
};

//通過 WindowManager 將 View 顯示出來
public void handleShow() {
    if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
            + " mNextView=" + mNextView);
    if (mView != mNextView) {
        // remove the old view if necessary
        handleHide();
        mView = mNextView;
        mWM = WindowManagerImpl.getDefault();
        final int gravity = mGravity;
        mParams.gravity = gravity;
        if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
            mParams.horizontalWeight = 1.0f;
        }
        if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
            mParams.verticalWeight = 1.0f;
        }
        mParams.x = mX;
        mParams.y = mY;
        mParams.verticalMargin = mVerticalMargin;
        mParams.horizontalMargin = mHorizontalMargin;
        if (mView.getParent() != null) {
            if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
            mWM.removeView(mView);
        }
        if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
        mWM.addView(mView, mParams);
        trySendAccessibilityEvent();
    }
}

7.3、 scheduleTimeoutLocked

使用 Handler 發送一個延時消息,時間就是 Toast 設置的 duration 屬性。

private void scheduleTimeoutLocked(ToastRecord r, boolean immediate)
{
    Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
    long delay = immediate ? 0 : (r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY);
    mHandler.removeCallbacksAndMessages(r);
    mHandler.sendMessageDelayed(m, delay);
}

7.4、 handleTimeout

private final class WorkerHandler extends Handler
{
    @Override
    public void handleMessage(Message msg)
    {
        switch (msg.what)
        {
            case MESSAGE_TIMEOUT:
                handleTimeout((ToastRecord)msg.obj);
                break;
        }
    }
}

private void handleTimeout(ToastRecord record)
{
  
    synchronized (mToastQueue) {
        int index = indexOfToastLocked(record.pkg, record.callback);
        if (index >= 0) {
            cancelToastLocked(index);
        }
    }
}

7.5、cancelToastLocked

當通過 Handler 發送的延時任務的時間到了那么就需要通知應用進程去 hide 這個 Toast 。具體操作還是通過 IPC 過程去真正調用 TN 的 hide 方法實現真正的隱藏操作。

當操作完成之后判斷隊列中是否還有 ToastRecord 沒有執行,如果有那么就調用 showNextToastLocked 方法再次的去取出 ToastRecord 去執行。

private void cancelToastLocked(int index) {
    ToastRecord record = mToastQueue.get(index);
    try {
        //遠程調用應用進程的 TN 的 hide() 方法隱藏 toast
        record.callback.hide();
    } catch (RemoteException e) {
        Slog.w(TAG, "Object died trying to hide notification " + record.callback
                + " in package " + record.pkg);
        // don't worry about this, we're about to remove it from
        // the list anyway
    }
    mToastQueue.remove(index);
    keepProcessAliveLocked(record.pid);
    //如果隊列中還有數據那么就再次調用 showNextToastLocked 方法流程跟上面是一樣的。
    if (mToastQueue.size() > 0) {
        // Show the next one. If the callback fails, this will remove
        // it from the list, so don't assume that the list hasn't changed
        // after this point.
        showNextToastLocked();
    }
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容