圖解 | 不得錯過的Binder淺析(一)

Framework和Binder的內(nèi)容挺深的活箕,本文還是站在應(yīng)用層開發(fā)者的角度來建立基本認知先壕,能在遇到問題的時候有思路和方向即可嗡害。(本文將帶著關(guān)鍵問題和核心流程展開琢歇,不會面面俱到)

大綱:

  • 背景
    • 為什么要多進程
    • 為什么要Binder
    • Binder簡單架構(gòu)
  • 簡單示例
  • 源碼分析
    • 客戶端與驅(qū)動交互
    • 服務(wù)端與驅(qū)動交互
  • 總結(jié)
  • 細節(jié)補充
    • Binder為什么高效
    • Binder為什么不用shm
  • 提問
  • 參考資料

本文約4.0k字兰怠,閱讀大約17分鐘。

Android源碼基于8.0矿微。

背景

為什么要多進程

Binder是Android系統(tǒng)的一種跨進程通信(IPC)機制痕慢。

在Android系統(tǒng)中,單個進程被分配了有限的內(nèi)存涌矢,多進程可以使用更多內(nèi)存隔離崩潰風險等快骗。

多進程在Android中常見的使用場景有獨立進程的WebView娜庇、推送、狈嚼海活名秀、系統(tǒng)服務(wù)等,既然是多進程場景藕溅,那么就需要跨進程通信了匕得。

為什么要Binder

Linux自帶了一些跨進程通信方式:

  • 管道(pipe):管道描述符是半雙工,單向的巾表,數(shù)據(jù)只能往一個方向流汁掠,想要讀寫需要兩個管道描述符。Linux提供了pipe(fds)來獲取一對描述符集币,一個讀一個寫考阱。匿名管道只能用在具有親緣關(guān)系的父子進程間的通信,有名管道無此限制鞠苟。

  • Socket:全雙工乞榨,可讀可寫秽之。如Zygote進程等待AMS系統(tǒng)服務(wù)發(fā)起socket請求來創(chuàng)建應(yīng)用進程。

  • 共享內(nèi)存(shm吃既,Shared Memory):會映射一段能被多個進程訪問的內(nèi)存考榨,是最高效的IPC方式,他通常需要結(jié)合其他跨進程方式如信號量來同步信息鹦倚。Android基于shm改進得到匿名共享內(nèi)存Ashmem(Anonymous Shared Memory)河质,因高效而適合處理較大的數(shù)據(jù),如應(yīng)用進程通過共享內(nèi)存來讀取SurfaceFlinger進程合成的視圖數(shù)據(jù)申鱼,進行展示愤诱。

  • 內(nèi)存映射(mmap):Linux通過將一個虛擬內(nèi)存區(qū)域與一個磁盤上的文件關(guān)聯(lián)起來,以初始化這個虛擬內(nèi)存區(qū)域的內(nèi)容捐友。通過指針的方式讀寫內(nèi)存淫半,系統(tǒng)會同步進對應(yīng)的磁盤文件。Binder用到了mmap匣砖。

  • 信號(signal):單向的科吭,發(fā)個信號就完事,無返回結(jié)果猴鲫。只能發(fā)信號对人,帶不了參數(shù)。如子進程被殺掉后系統(tǒng)會發(fā)出SIGCHLD信號拂共,父進程會清理子進程在進程表的描述信息防止僵尸進程的發(fā)生牺弄。

另外還有文件共享、消息隊列(Message)等跨進程通信方式...

這些跨進程通信方式都各有優(yōu)劣宜狐,Android最終選擇了自建一套兼顧好用势告、高效、安全的Binder抚恒。

  • 好用:易用的C/S架構(gòu)(借助AIDL后只需編寫業(yè)務(wù)邏輯)
  • 高效:用mmap進行內(nèi)存映射咱台,只需一次拷貝
  • 安全:內(nèi)核態(tài)管理身份標記,每個App有UID來校驗權(quán)限俭驮,同時支持實名(系統(tǒng)服務(wù))和匿名(自己創(chuàng)建的服務(wù))

Binder簡單架構(gòu)

Linux內(nèi)存被分為用戶空間內(nèi)核空間回溺,用戶空間需要經(jīng)過系統(tǒng)調(diào)用才能訪問到內(nèi)核空間。

image

(圖片來源:「寫給Android應(yīng)用工程師的Binder原理剖析」)

Binder整體基于C/S架構(gòu)混萝。運行在內(nèi)核空間的Binder驅(qū)動程序遗遵,會為用戶空間暴露出一個設(shè)備文件/dev/binder,進程間通過該文件來建立通信通道譬圣。

image

Binder的啟動過程:

  1. 打開binder驅(qū)動(open)
  2. 將驅(qū)動文件的描述符(mDriverFD)進行內(nèi)存映射(mmap)瓮恭,分配緩沖區(qū)
  3. 服務(wù)端運行binder線程,把線程注冊到binder驅(qū)動厘熟,進入循環(huán)等待客戶端的指令(兩端通過ioctl與驅(qū)動交互)

簡單示例

AIDL(Android接口定義語言)可以輔助生成Binder的Java類屯蹦,減少重復工作维哈,使用姿勢網(wǎng)上有很多,這里就直接手寫吧登澜,方便理解阔挠。

示例調(diào)用流程如下:

image

代碼不多,大部分是log脑蠕,重點看注釋就行购撼。

客戶端Activity:

//NoAidlActivity.java

protected void onCreate(Bundle savedInstanceState) {
    Intent intent = new Intent(this, MyService.class);

    bindService(intent, new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //1. 從對象池拿到可復用的對象(享元模式)
            Parcel data = Parcel.obtain();
            Parcel reply = Parcel.obtain();

            Log.e("哈利迪", "--- 我是客戶端 NoAidlActivity , pid = "
                  + Process.myPid() + ", thread = "
                  + Thread.currentThread().getName());

            String str = "666";
            Log.e("哈利迪", "客戶端向服務(wù)端發(fā)送:" + str);
            //2. 往data寫數(shù)據(jù),作為請求參數(shù)
            data.writeString(str);

            //3. 拿到服務(wù)端的IBinder句柄谴仙,調(diào)用transact
            //約定行為碼是1迂求;需要服務(wù)端的返回值,所以flags傳0表示同步調(diào)用
            service.transact(1, data, reply, 0);

            Log.e("哈利迪", "--- 我是客戶端 NoAidlActivity , pid = "
                  + Process.myPid() + ", thread = "
                  + Thread.currentThread().getName());

            //4. 從reply讀取服務(wù)端的返回值
            Log.e("哈利迪", "客戶端接收服務(wù)端返回:" + reply.readString());
        }
    }, Context.BIND_AUTO_CREATE);
}

service.transact傳入了flags為0晃跺,表示同步調(diào)用揩局,會阻塞等待服務(wù)端的返回值。如果服務(wù)端進行了耗時操作掀虎,此時用戶操作UI則會引起ANR凌盯。

flags的另一個值是1,表示異步調(diào)用的one way烹玉,不需要等待服務(wù)端的返回結(jié)果驰怎,先忽略。

來看服務(wù)端運行的Service二打,

class MyService extends Service {

    @Override
    public IBinder onBind(Intent intent) {
        //返回服務(wù)端的IBinder句柄
        return new MyBinder();
    }
}

注冊服務(wù)县忌,讓服務(wù)端Service運行在:remote進程,來實現(xiàn)跨進程继效,

<service
         android:name=".binder.no_aidl.MyService"
         android:process=":remote" />

運行在服務(wù)端的Binder對象芹枷,

class MyBinder extends Binder {

    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags){
        if (code == 1) {//如果是約定好的行為碼1
            Log.e("哈利迪", "--- 我是服務(wù)端 MyBinder , pid = "
                  + Process.myPid() + ", thread = "
                  + Thread.currentThread().getName());
            //1. 從data讀取客戶端參數(shù)
            Log.e("哈利迪", "服務(wù)端收到:" + data.readString());

            String str = "777";
            Log.e("哈利迪", "服務(wù)端返回:" + str);
            //2. 從reply向客戶端寫返回值
            reply.writeString(str);

            //3. 處理完成
            return true;
        }
        return super.onTransact(code, data, reply, flags);
    }
}

運行如下,7行日志:

image

由于我們的flags傳入的是0同步調(diào)用莲趣,可以試著在服務(wù)端onTransact里sleep幾秒,會發(fā)現(xiàn)客戶端需要幾秒后才能打印出返回值饱溢。所以如果服務(wù)端需要進行耗時操作喧伞,客戶端則需要在子線程里進行binder調(diào)用湿弦。

延伸:從 IT互聯(lián)網(wǎng)大叔 的「android獲取進程名函數(shù)叉跛,如何優(yōu)化到極致」一文可見裕偿,在使用系統(tǒng)API時勺择,如果有更好的方案期虾,還是建議將跨進程方案getSystemService放到最后作為兜底帝蒿,因為他需要的binder調(diào)用本身有開銷吊说,而且作為應(yīng)用層開發(fā)者也很少會去關(guān)注遠方進程的內(nèi)部實現(xiàn)局服,萬一對方有潛在的耗時操作呢状植?

通過這個例子浊竟,我們可以看出怨喘,Binder機制使用了Parcel來序列化數(shù)據(jù),客戶端在主線程調(diào)用了transact來請求(Parcel data傳參)振定,服務(wù)端在Binder線程調(diào)用onTransact來響應(yīng)(Parcel reply回傳結(jié)果)必怜。

源碼分析

Binder的調(diào)用流程大致如下,native層BpBinder的Bp指的是Binder proxy后频,

image

可見梳庆,需要經(jīng)過如下調(diào)用才能完成一次通信:

  1. 請求:客戶端Java層->客戶端native層->Binder驅(qū)動層->服務(wù)端native層->服務(wù)端Java層
  2. 響應(yīng):服務(wù)端Java層->服務(wù)端native層->Binder驅(qū)動層->客戶端native層->客戶端Java層

即Binder驅(qū)動層充當著一個中轉(zhuǎn)站的作用,有點像網(wǎng)絡(luò)分層模型卑惜。

客戶端與驅(qū)動交互

先來看客戶端與驅(qū)動的交互膏执。因為是跨進程調(diào)用(指定了:remote),示例里onServiceConnected回調(diào)回來的service對象是個BinderProxy代理實例(不跨進程的話會發(fā)生遠程轉(zhuǎn)本地露久,后面講)更米,我們以service.transact(1, data, reply, 0)這行調(diào)用作為入口跟進。

BinderProxy類寫在Binder類文件里面:

//BinderProxy.java

public boolean transact(int code, Parcel data, Parcel reply, int flags){
    //調(diào)用了native方法
    return transactNative(code, data, reply, flags);
}

這個native方法在android_util_Binder.cpp里注冊抱环,

//android_util_Binder.cpp

//JNI注冊
static const JNINativeMethod gBinderProxyMethods[] = {
    { "transactNative",
     "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z",
     (void*)android_os_BinderProxy_transact},
};

//native方法具體實現(xiàn)
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
        jint code, jobject dataObj, jobject replyObj, jint flags){
    //轉(zhuǎn)成native層的Parcel
    Parcel* data = parcelForJavaObject(env, dataObj);
    Parcel* reply = parcelForJavaObject(env, replyObj);
    //拿到native層的句柄BpBinder
    IBinder* target = (IBinder*)
        env->GetLongField(obj, gBinderProxyOffsets.mObject);
    //調(diào)用BpBinder的transact
    status_t err = target->transact(code, *data, reply, flags);
}

繼續(xù)跟BpBinder.cpp壳快,

//BpBinder.cpp

status_t BpBinder::transact(...){
    //交給線程單例處理,驅(qū)動會根據(jù)mHandle值來找到對應(yīng)的binder句柄
    status_t status = IPCThreadState::self()->transact(
        mHandle, code, data, reply, flags);
}

IPCThreadState是一個線程單例镇草,負責與binder驅(qū)動進行具體的指令通信眶痰,跟進IPCThreadState.cpp

//IPCThreadState.cpp

status_t IPCThreadState::transact(...){
    //將數(shù)據(jù)寫入mOut梯啤,見1.1
    err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);

    //...先忽略one way異步調(diào)用的代碼竖伯,只看有返回值的同步調(diào)用
    //跟binder驅(qū)動交互,傳入reply接收返回數(shù)據(jù)因宇,見1.2
    err = waitForResponse(reply);
}

//1.1 將數(shù)據(jù)寫入mOut
status_t IPCThreadState::writeTransactionData(...)
{
    binder_transaction_data tr;
    //...打包各種數(shù)據(jù)(data size七婴、buffer、offsets)
    tr.sender_euid = 0;
    //將BC_TRANSACTION指令寫入mOut
    mOut.writeInt32(cmd);
    //將打包好的binder_transaction_data寫入mOut
    mOut.write(&tr, sizeof(tr));
}

//1.2 跟binder驅(qū)動交互察滑,傳入reply接收返回數(shù)據(jù)
status_t IPCThreadState::waitForResponse(...){
    //這個循環(huán)很重要打厘,客戶端就是在這里休眠等待服務(wù)端返回結(jié)果的
    while (1) {
        //跟驅(qū)動進行數(shù)據(jù)交互,往驅(qū)動寫mOut贺辰,從驅(qū)動讀mIn户盯,見1.3
        talkWithDriver();
        //讀取驅(qū)動回復的指令
        cmd = (uint32_t)mIn.readInt32();
        switch (cmd) {
            case BR_TRANSACTION_COMPLETE:
                //表示驅(qū)動已經(jīng)收到客戶端的transact請求
                //如果是one way異步調(diào)用,到這就可以結(jié)束了
                if (!reply && !acquireResult) goto finish;
                break;
            case BR_REPLY:
                //表示客戶端收到服務(wù)端的返回結(jié)果
                binder_transaction_data tr;
                //把服務(wù)端的數(shù)據(jù)讀出來饲化,打包進tr
                err = mIn.read(&tr, sizeof(tr));
                //再把tr的數(shù)據(jù)透傳進reply
                reply->ipcSetDataReference(...);
                //結(jié)束
                goto finish;
        }
    }
}

//1.3 跟驅(qū)動進行數(shù)據(jù)交互莽鸭,往驅(qū)動寫mOut,從驅(qū)動讀mIn
status_t IPCThreadState::talkWithDriver(bool doReceive){
    binder_write_read bwr;
    //指定寫數(shù)據(jù)大小和寫緩沖區(qū)
    bwr.write_size = outAvail;
    bwr.write_buffer = (uintptr_t)mOut.data();

    //指定讀數(shù)據(jù)大小和讀緩沖區(qū)
    if (doReceive && needRead) {
        bwr.read_size = mIn.dataCapacity();
        bwr.read_buffer = (uintptr_t)mIn.data();
    } else {
        bwr.read_size = 0;
        bwr.read_buffer = 0;
    }

    //ioctl的調(diào)用進入了binder驅(qū)動層的binder_ioctl
    ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr);

    if (bwr.write_consumed > 0) {
        //數(shù)據(jù)已經(jīng)寫入驅(qū)動吃靠,從mOut移除
        if (bwr.write_consumed < mOut.dataSize())
            mOut.remove(0, bwr.write_consumed);
        else
            mOut.setDataSize(0);
    }
    if (bwr.read_consumed > 0) {
        //從驅(qū)動讀出數(shù)據(jù)存入mIn
        mIn.setDataSize(bwr.read_consumed);
        mIn.setDataPosition(0);
    }
}

ioctl的調(diào)用進入了binder驅(qū)動層的binder_ioctl硫眨,驅(qū)動層的代碼先不跟。

服務(wù)端與驅(qū)動交互

從「一圖摸清Android應(yīng)用進程的啟動」一文可知巢块,服務(wù)端創(chuàng)建了一個線程注冊進binder驅(qū)動礁阁,即binder線程巧号,在ProcessState.cpp

//ProcessState.cpp

virtual bool threadLoop()
{   //把binder線程注冊進binder驅(qū)動程序的線程池中
    IPCThreadState::self()->joinThreadPool(mIsMain);
    return false;
}

跟進IPCThreadState.cpp氮兵,

//IPCThreadState.cpp

void IPCThreadState::joinThreadPool(bool isMain){
    //向binder驅(qū)動寫數(shù)據(jù)裂逐,表示當前線程需要注冊進binder驅(qū)動
    mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);
    status_t result;
    do {
        //進入死循環(huán),等待指令的到來泣栈,見1.1
        result = getAndExecuteCommand();
    } while (result != -ECONNREFUSED && result != -EBADF);
    //向binder驅(qū)動寫數(shù)據(jù)(退出循環(huán)卜高,線程結(jié)束)
    mOut.writeInt32(BC_EXIT_LOOPER);
}

//1.1 等待指令的到來
status_t IPCThreadState::getAndExecuteCommand(){
    //跟驅(qū)動進行數(shù)據(jù)交互,驅(qū)動會把指令寫進mIn
    talkWithDriver();
    //從mIn讀出指令
    cmd = mIn.readInt32();
    //執(zhí)行指令南片,見1.2
    result = executeCommand(cmd);
    return result;
}

//1.2 執(zhí)行指令
status_t IPCThreadState::executeCommand(int32_t cmd){
    //客戶端發(fā)請求到驅(qū)動掺涛,驅(qū)動轉(zhuǎn)發(fā)到服務(wù)端
    switch ((uint32_t)cmd) {
        case BR_TRANSACTION:{
            //服務(wù)端收到BR_TRANSACTION指令
            binder_transaction_data tr;
            //讀出客戶端請求的參數(shù)
            result = mIn.read(&tr, sizeof(tr));

            //準備數(shù)據(jù),向上傳給Java層
            Parcel buffer; Parcel reply;
            buffer.ipcSetDataReference(...);

            //cookie保存的是binder實體疼进,對應(yīng)服務(wù)端的native層對象就是BBinder
            reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer,
                                                            &reply, tr.flags);
            //服務(wù)端向驅(qū)動寫返回值薪缆,讓驅(qū)動轉(zhuǎn)發(fā)給客戶端
            sendReply(reply, 0);
        }
    }
}

//1.3 服務(wù)端向驅(qū)動寫返回值,讓驅(qū)動轉(zhuǎn)發(fā)給客戶端
status_t IPCThreadState::sendReply(const Parcel& reply, uint32_t flags){
    err = writeTransactionData(BC_REPLY, flags, -1, 0, reply, &statusBuffer);
    //服務(wù)端返回結(jié)果給客戶端就行伞广,不用等待客戶端拣帽,所以傳NULL
    return waitForResponse(NULL, NULL);
}

然后看下BBinder的transact是怎么向上傳遞到Java層的,在Binder.cpp中嚼锄,

//Binder.cpp

status_t BBinder::transact(uint32_t code, const Parcel& data, 
                           Parcel* reply, uint32_t flags){
    switch (code) {
            //ping指令用來判斷連通性减拭,即binder句柄是否還活著
        case PING_TRANSACTION:
            reply->writeInt32(pingBinder());
            break;
        default:
            //看這,通過JNI調(diào)用到Java層的execTransact区丑,見1.1
            err = onTransact(code, data, reply, flags);
            break;
    }
    return err;
}

//android_util_Binder.cpp

//1.1 通過JNI調(diào)用到Java層的execTransact
virtual status_t onTransact(...){
    JNIEnv* env = javavm_to_jnienv(mVM);
    jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact, ...);
}

回到Java層拧粪,execTransact如下:

//android.os.Binder.java

private boolean execTransact(...) {
    res = onTransact(code, data, reply, flags);
}

至此就回調(diào)到了示例代碼中服務(wù)端MyBinder的onTransact了,我們在示例中處理請求參數(shù)data和返回值reply沧侥,最后由native層的sendReply(reply, 0)真正向驅(qū)動寫返回值可霎,讓驅(qū)動轉(zhuǎn)發(fā)給客戶端。

將調(diào)用代碼和流程圖結(jié)合起來:

image

然后是指令交互圖(非one way模式):

image

binder同步調(diào)用等到服務(wù)端的BR_REPLY指令后就真正結(jié)束宴杀,服務(wù)端則繼續(xù)循環(huán)癣朗,等待下一次請求。

總結(jié)

本文主要介紹了Binder的背景和調(diào)用流程旺罢,將留下3個疑問繼續(xù)探討斯棒。

  1. binder句柄是怎么傳輸和管理的(binder驅(qū)動和ServiceManager進程)
  2. binder句柄的遠程轉(zhuǎn)本地
  3. one way異步模式和他的串行調(diào)用(async_todo)、同步模式的并行調(diào)用

系列文章:

細節(jié)補充

Binder為什么高效

Linux用戶空間是無法直接讀寫磁盤的主经,系統(tǒng)所有的資源管理(讀寫磁盤文件、分配回收內(nèi)存庭惜、從網(wǎng)絡(luò)接口讀寫數(shù)據(jù))都是在內(nèi)核空間完成的罩驻,用戶空間需要通過系統(tǒng)調(diào)用讓內(nèi)核空間完成這些功能。

傳統(tǒng)IPC傳輸數(shù)據(jù):發(fā)送進程需要copy_from_user從用戶到內(nèi)核护赊,接收進程再copy_to_uer從內(nèi)核到用戶惠遏,兩次拷貝砾跃。

而Binder傳輸數(shù)據(jù):用mmap將binder內(nèi)核空間的虛擬內(nèi)存和用戶空間的虛擬內(nèi)存映射到同一塊物理內(nèi)存copy_from_user將數(shù)據(jù)從發(fā)送進程的用戶空間拷貝到接收進程的內(nèi)核空間(一次拷貝)节吮,接收進程通過映射關(guān)系能直接在用戶空間讀取內(nèi)核空間的數(shù)據(jù)抽高。

image

(圖片來源:「寫給Android應(yīng)用工程師的Binder原理剖析」)

Binder為什么不用shm

shm通常需要結(jié)合其他跨進程方式如信號量來同步信息,使用沒有mmap方便透绩。

提問

  • 上期提問: SurfaceFlinger進程為什么不是通過Zygote進程的fork創(chuàng)建翘骂,而是由init進程創(chuàng)建?

參考資料


更多性感文章帚豪,關(guān)注原創(chuàng)技術(shù)公眾號:哈利迪ei

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碳竟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子狸臣,更是在濱河造成了極大的恐慌莹桅,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烛亦,死亡現(xiàn)場離奇詭異诈泼,居然都是意外死亡,警方通過查閱死者的電腦和手機煤禽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門铐达,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人呜师,你說我怎么就攤上這事娶桦。” “怎么了汁汗?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵衷畦,是天一觀的道長。 經(jīng)常有香客問我知牌,道長祈争,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任角寸,我火速辦了婚禮菩混,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘扁藕。我一直安慰自己沮峡,他們只是感情好,可當我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布亿柑。 她就那樣靜靜地躺著邢疙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上疟游,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天呼畸,我揣著相機與錄音,去河邊找鬼颁虐。 笑死蛮原,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的另绩。 我是一名探鬼主播儒陨,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼板熊!你這毒婦竟也來了框全?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤干签,失蹤者是張志新(化名)和其女友劉穎津辩,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體容劳,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡喘沿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了竭贩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚜印。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖留量,靈堂內(nèi)的尸體忽然破棺而出窄赋,到底是詐尸還是另有隱情,我是刑警寧澤楼熄,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布忆绰,位于F島的核電站,受9級特大地震影響可岂,放射性物質(zhì)發(fā)生泄漏错敢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一缕粹、第九天 我趴在偏房一處隱蔽的房頂上張望稚茅。 院中可真熱鬧,春花似錦平斩、人聲如沸亚享。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽虹蒋。三九已至糜芳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間魄衅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工塘辅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留晃虫,地道東北人。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓扣墩,卻偏偏與公主長得像哲银,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子呻惕,可洞房花燭夜當晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容