Android進程間通信

3.5 Android進程間通信

3.5.1 背景知識

傳統IPC

Linux傳統的IPC機制分為如下幾種:管道、消息隊列、共享內存和Socket等。它們總結起來是如下三種方式:

  1. 兩個進程共享文件系統中某個文件上的某些信息,為了訪問這些信息,每個進程都得穿越內核(read、write、lseek等)。當然進程共享的文件(一切皆是文件)可以是硬盤上的實際文件,也可以是虛擬文件,或者外圍設備等,這些虛擬文件只是在文件系統中有個訪問點。
  2. 兩個進程共享駐留于內核中的某些信息,管道是這種共享類型的一個例子,Binder也算是這種類型,進程可以通過訪問/dev/binder(open、mmap、ioctl系統調用)從而實現互相通信。
  3. 兩個進程有一個雙方都能訪問的共享內存區,每個進程一旦設置好該共享內存區,就可以不涉及內核而訪問其中的數據,這種方式是IPC形式中最快的,它依賴于mmap系統調用。

數據類型和對象

對象定義的一致性:進程A中有一個對象ObjectA,進程B想通過RPC訪問這個ObjectA,那么A和B中對該對象的格式必須是一致的,對Java而言,雙方可以引用同一個Java文件中的類,這種對象定義的一致性是雙方通信約定的一部分。

數據類型:進程間通信傳遞基本數據類型這個很容易理解,傳遞一個int到另外一個進程,對方就將其解釋為一個int,但是如何傳遞一個對象?像Java中的序列化可以將一個對象轉換成字節流通過網絡傳遞到另外一個進程或者寫進文件(序列化的這種機制依賴于Java字節碼的平臺無關性和類的動態加載機制)。

Binder通信中可以支持傳遞序列化過的Java字節流,但是它也只是將這串字節流當做普通數據類型來處理,它只負責傳遞,具體如何解釋這些數據由雙方來約定。事實上Binder通信將數據分為三種:基本數據類型,binder數據類型和文件描述符。

3.5.2 Android多進程模式

Android系統會為每個進程分配一個獨立的虛擬機,不同的虛擬機在內存分配上有不同的地址空間。通過給四大組件指定android:process屬性就可以開啟多進程模式,默認進程的進程名是包名packageName,進程名以:開頭的進程屬于當前應用的私有進程,其他應用的組件不可以和它跑在同一個進程中,而進程名不以:開頭的進程屬于全局進程,其他應用通過ShareUID方法可以和它跑在同一個進程中。

android:process=":xyz" //進程名是 packageName:xyz
android:process="aaa.bbb.ccc" //進程名是 aaa.bbb.ccc

Android系統會為每個應用分配一個唯一的UID,具有相同UID的應用才能共享數據。當兩個應用有相同的ShareUID和簽名時,它們可以相互訪問對方的私有數據,比如data目錄、組件信息等,而且可以運行在同一個進程中。當它們運行在同一個進程中,還可以共享內存數據,此時它們看起來就像是一個應用的兩個部分。

3.5.3 Binder通信模型

Binder框架定義了四個角色:Server,Client,ServiceManager(SMgr)以及Binder驅動。其中Server,Client,SMgr運行于用戶空間,驅動運行于內核空間。這四個角色的關系和網絡類似:Server是服務器,Client是客戶端,SMgr是域名服務器(DNS),驅動是路由器。

Binder驅動

雖然名叫“驅動”,實際上和硬件設備沒有任何關系,只是實現方式和設備驅動程序是一樣的:它工作于內核態,提供open(),mmap(),poll(),ioctl()等標準文件操作(沒有read(),write()接口)。驅動負責進程之間Binder通信提供支持:Binder在進程之間的傳遞,Binder引用計數管理,數據包在進程之間的傳遞和交互等。驅動和應用程序之間定義了一套接口協議,主要功能由ioctl()方法實現。Binder驅動的代碼位于linux目錄的drivers/misc/binder.c中。

ServiceManager與具名Binder

和DNS類似,SMgr的作用是將字符形式的Binder名字轉化成Client中對該Binder的引用,使得Client能夠通過Binder名字獲得對Server中Binder實體的引用。注冊了名字的Binder叫具名Binder,就像每個網站除了有IP地址外還有自己的域名。

Server創建了Binder實體,然后將這個Binder實體連同名字通過Binder驅動發送給SMgr,通知SMgr注冊這個Binder。驅動為這個Binder創建位于實體以及該實體的引用,將名字及引用傳遞給SMgr。SMgr收到數據后,從中取出名字和引用填入一張索引表中。

細心的讀者可能會發現其中的蹊蹺:SMgr是一個進程,Server是另一個進程,Server向SMgr注冊Binder必然會涉及進程間通信,當前實現的是進程間通信卻又要用到進程間通信,很典型的雞蛋問題。Binder的處理方式:SMgr提供的Binder比較特殊,它沒有名字也不需要注冊,當一個進程使用BINDER_SET_CONTEXT_MGR命令將自己注冊成SMgr時Binder驅動會自動為它創建Binder實體,然后這個Binder的引用在所有Client中都是固定的(0號引用),即如果一個Server若要向SMgr注冊自己Binder只需通過這個固定引用和SMgr通信。類比網絡通信,約定0號引用就好比域名服務器的地址。

Server向SMgr注冊了Binder實體及其名字后,Client就可以通過名字獲得該Binder的引用了:Client向SMgr請求訪問某個Binder,SMgr收到這個請求后,從請求數據包里獲得Binder的名字,通過改名字在索引表里找到對應的引用,將該引用回復給Client。現在這個Binder對象有了兩個引用:一個位于SMgr中,一個位于發起請求的Client中。如果接下來有更多的Client請求該Binder,系統中就會有更多的引用指向該Binder。

匿名Binder

并不是所有Binder都需要注冊給SMgr,Server端可以通過已經建立的Binder連接將其他Binder實體傳給Client,此時這個Binder沒有向SMgr注冊,是個匿名Binder。Client將會收到這個匿名Binder的引用,通過這個引用向位于Server中的實體發送請求。

下圖展示了參與Binder通信的所有角色,將在以后章節中一一提到。

3.5.4 Binder的含義

考察一次Binder通信的全過程會發現,Binder存在于系統以下幾個部分中:

  • 應用程序:分為Server進程和Client進程(SMgr本身也是Server端)。
  • Binder驅動:管理位于Server端的Binder實體和Client端的引用。
  • 傳輸數據:Binder是可以跨進程傳遞,需要在傳輸數據中予以表述。

在系統不同部分,Binder實現的功能不同,表現形式也不一樣。接下來逐一探討各個部分中Binder的含義。

Binder在應用程序中的含義

Binder本質上只是一種底層通信方式,和具體服務沒有關系。為了提供具體服務,Server必須提供一套接口函數以便Client通過遠程訪問使用各種服務。Android中采用Proxy設計模式:將接口函數定義在一個抽象類中,Server和Client都繼承該抽象類實現所有接口函數,所不同的是Server端是真正的功能實現,而Client端是對這些接口函數遠程調用請求的代理。

Server端的Binder–Binder實體

首先定義一個抽象接口類,其中包含一系列接口函數(業務邏輯)等待Server和Client各自實現。并為這些接口函數一一編號,這樣Server可以根據收到的編號知道需要調用哪個函數。其次就要引入Binder了,Server端定義一個Binder抽象類處理來自Client的請求數據包,其中有一個onTransact方法,該方法分析收到的數據包,調用相應的接口函數處理請求。

接下來采用繼承方式以接口類和Binder抽象類為基類構建Binder在Server中的實體,實現基類里中的接口函數以及onTransact方法,其中接口函數是我們具體的業務邏輯,onTransact方法根據Client的請求數據(binder_transact_data)決定調用對應的接口函數,具體業務執行完畢,如果需要返回數據就再構建一個binder_transaction_data結構返回數據。onTransact方法又是什么時候調用呢?這就需要驅動參與了。驅動收到數據后,從中取出該數據的目標Binder引用(Client端只有目標Binder實體的引用),根據自己的記錄找到該Binder實體,然后將具體請求交付給該Binder實體,即調用其onTransact方法。

Client端的Binder–Binder引用

Client端的Binder也繼承抽象接口類并實現接口函數,但這不是真正的實現,而是將參數序列化,然后向Server端(Binder實體)發送請求并等待返回值。為此Client端要知道Binder實體的相關信息,即對Binder實體的引用,該引用或是由SMgr轉發過來的對實名Binder的引用或是由另一個進程直接發送過來的對匿名Binder的引用。

Client端中,其序列化方式是:創建一個binder_transaction_data數據包,將其對應的函數編碼填入code域,將調用該函數所需的參數填入data.buffer中,并指明數據包的目的地,即目標Binder實體的引用,填入數據包的target.handle中。注意這里和Server的區別:實際上target域是個union,包括ptr和handle兩個成員,前者用于Server端,指向Binder實體對應的內存空間;后者用于Client端,存放Binder實體的引用。數據包準備好后,通過驅動接口發送出去,經過BC_TRANSACTION/BC_REPLY完成函數的遠程調用并得到返回值。

Binder驅動

驅動是Binder通信的核心,其負責管理Binder實體和Binder引用,以及負責將數據傳輸到目的地。驅動將對穿越進程邊界的Binder做如下操作:檢查傳輸結構的type域:

  • 如果是BINDER_TYPE_BINDER或BINDER_TYPE_WEAK_BINDER則創建Binder的實體;
  • 如果是BINDER_TYPE_HANDLE或BINDER_TYPE_WEAK_HANDLE則創建Binder的引用;
  • 如果是BINDER_TYPE_FD則為進程打開文件,無須創建任何數據結構。

傳輸數據中的Binder

Binder本身是可以跨越進程的,這些傳輸中的Binder用結構flat_binder_object表示,如下圖所示:

無論是Binder實體還是對Binder引用都從屬于某個進程,其是不能透明地在進程之間傳輸的,必須經過驅動轉譯。例如當Server把Binder實體傳遞給Client時,在發送數據包中,flat_binder_object中的type是BINDER_TYPE_BINDER,binder指向Server進程地址空間的一個對象。驅動必須對數據包中的這個Binder做轉譯:將type改成BINDER_TYPE_HANDLE;為該Binder實體創建位于內核中的引用并將該引用號填入handle中。對于發送數據包中的Binder引用也要做同樣轉換。經過處理后接收進程從數據流中取得的Binder引用才是有效的,才可以將其填入數據包binder_transaction_data的target.handle域,向Binder實體發送請求。

下圖總結了當flat_binder_object結構穿過驅動時驅動所做的操作:

文件形式的Binder

除了通常意義上用來通信的Binder,還有一種特殊的Binder:文件Binder。這種Binder的基本思想是:將文件看成Binder實體,進程打開的文件號看成Binder的引用。一個進程可以將它打開文件的文件號傳遞給另一個進程,從而另一個進程也打開了同一個文件,就象Binder的引用在進程之間傳遞一樣。

需要注意的是,這和兩個進程分別打開同一個文件是有區別的:這種方式下兩個進程共享同一個文件描述符,一個進程使用read或者write改變了文件指針,另一個進程的文件指針也會改變;而如果兩個進程分別使用同一文件名打開文件則有各自的文件描述符結構,從而各自獨立維護文件指針,互不干擾。

3.5.4 Binder雜談

Binder內存管理

除了共享內存外,Linux中其他的IPC都需要兩次copy:首先是發送方將數據從發送端地址空間copy到內核空間,然后OS將數據從內核空間copy到接收方地址空間,即用戶空間->內核空間->用戶空間。而Binder只需要一次copy,原因是Binder驅動負責管理數據接收緩存,Binder驅動實現了mmap()系統調用,用來創建數據接收的緩存空間。先看mmap()是如何使用的:

fd = open("/dev/binder", O_RDWR);
mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);

這樣接收方就有了一片大小為MAP_SIZE的接收緩存區。mmap()的返回值是內存映射在接收方地址空間的地址,不過這段空間是由驅動管理,用戶不必也不能直接訪問(映射類型為PROT_READ,只讀映射),接收緩存區映射好后就可以接收和存放數據了。這樣通信時數據就只需要一次copy了:相當于直接發送方地址空間copy到接收方地址空間,省去了內核中暫存這個步驟。

需要需要的是,Linux內核并沒有從一個用戶空間到另一個用戶空間直接拷貝的函數,需要先用copy_from_user拷貝到內核空間,再用copy_to_user拷貝到另一個用戶空間。為了實現用戶空間到用戶空間的拷貝,mmap()分配的內存除了映射進了接收方進程里,還映射進了內核空間。所以調用copy_from_user將數據拷貝進內核空間也相當于拷貝進了接收方的用戶空間。

Binder線程管理

對于Server進程S,可能會有許多Client同時發起請求,為了提高效率往往使用線程池并發處理收到的請求。怎樣使用線程池實現并發處理呢?這和具體的IPC機制有關,例如在BIO中,Server端的ServerSocket設置為偵聽模式,有一個專門的偵聽線程使用該ServerSocket偵聽來自Client的連接請求,即阻塞在accept()上,一旦收到來自Client的連接請求就會創建新連接,并從線程池中啟動一個工作線程并將這個連接交給該線程,這個連接上的后續業務就由該線程完成。

對于Binder來說,沒有偵聽模式也不會建立連接Socket,怎樣管理線程池呢?一種簡單的做法是,先創建一堆線程,這些線程會阻塞任務隊列上,一旦有來自Client的數據驅動會從隊列中喚醒一個線程來處理。這樣做簡單直觀,但一開始就創建一堆線程有點浪費資源,于是Binder協議引入了專門命令幫助用戶管理線程池,包括:

INDER_SET_MAX_THREADS //線程池的大小
BC_REGISTER_LOOP //創建主循環
BC_ENTER_LOOP //進入主循環
BC_EXIT_LOOP //退出主循環
BR_SPAWN_LOOPER //啟動新線程

首先要管理線程池就要知道池子有多大,應用程序通過INDER_SET_MAX_THREADS告訴驅動最多可以創建幾個線程。以后每個線程在創建,進入主循環,退出主循環時都要分別使用BC_REGISTER_LOOP,BC_ENTER_LOOP,BC_EXIT_LOOP告知驅動,以便驅動收集和記錄當前線程池的狀態。每當驅動接收到一個數據包(新任務到來),如果沒有閑置線程了,而且當前線程總數小于線程池最大線程數,就會再啟動一個。

任務隊列管理

一般線程池實現時有兩個隊列:任務隊列和工作線程隊列。使用者將任務發送到任務隊列中,然后工作線程從任務隊列中取出任務并執行。而Binder線程池中每個線程也有自己私有的任務隊列,存放發送給該線程的任務。由于發送時沒有特別標記,驅動怎么判斷將數據包發送到全局任務隊列還是某個線程私有的任務隊列呢,這里有兩條規則:

  1. 一般Client發給Server的請求數據包都提交到Server進程的全局任務隊列。
  2. 對同步請求的返回數據包(由BC_REPLY發送的包)都發送到發起請求的線程的私有任務隊列中。即進程P1的線程T1發給進程P2的線程T2的是同步請求,那么T2返回的數據包將送進T1的私有任務隊列而不會提交到P1的全局任務隊列。

數據包進入接收隊列的規則也就決定了線程如何接收任務的規則,即一個線程只要不接收返回數據包則應該在全局等待隊列中等待新任務,否則就應該在其私有任務隊列中等待對端的返回數據包。還是上面的例子,T1在向T2發送同步請求后就必須等待在它私有任務隊列中,而不是在P1的全局任務隊列中排隊,否則將得不到T2的返回的數據。

這些規則體現在應用程序上就是同步請求交互過程中的線程一致性:

  1. Client端,等待返回包的線程必須是發送請求的線程,而不能由一個線程發送請求包,另一個線程等待接收包,否則Client端將會線程紊亂;
  2. Server端,發送對應返回數據包的線程必須是收到請求數據包的線程,否則返回的數據包將無法送交發送請求的線程。

接下來探討一下Binder驅動是如何遞交同步請求和異步請求的。對于這兩種請求,驅動并沒有簡單地將其直接投遞到接收端的全局任務隊列中,而是對異步請求做了限流,優先保證同步請求。具體做法是:對于某個Binder實體,只要有一個異步請求沒有處理完畢,例如正在被某個線程處理或還在任意一個任務隊列中排隊,那么接下來發給該Binder的異步請求將不再投遞到這個任務隊列中,而是阻塞在驅動為該實體開辟的異步請求接收隊列(Binder節點的async_todo域)中,但這期間同步請求仍然可以進入任務隊列等待處理,一直到該異步請求處理完畢下一個異步請求才可以脫離異步請求隊列進入任務隊列中。這里用專門任務隊列將過多的異步請求暫存起來,以免突發大量異步請求擠占Server端的處理能力或耗盡線程池里的線程,進而阻塞同步請求。

3.5.6 深入理解Java層的Binder

IBinder/IInterface/Binder/BinderProxy

我們使用AIDL接口的時候,經常會接觸到這些類,那么這每個類代表的是什么呢?

  • IBinder代表的是一種跨進程傳輸的能力,只要實現了這個接口,就能將這個對象進行跨進程傳遞。這是驅動底層支持的,在跨進程數據流經驅動的時候,驅動會識別IBinder類型的數據,從而完成不同進程間Binder本地對象以及Binder代理對象的轉換。
  • IInterface代表的是Server端提供的能力。
  • Binder類,代表的其實就是Binder本地對象;BinderProxy類是Binder類的一個內部類,它代表遠程進程的Binder對象的本地代理;這兩個類都繼承自IBinder,因而都具有跨進程傳輸的能力。

AIDL通信過程

現在我們通過一個AIDL的使用,分析一下整個通信過程中,各個角色到底做了什么,AIDL到底是如何完成通信的。
首先定義一個簡單的aidl接口:

interface ICompute {
    int add(int a, int b);
}

編譯后,可以得到對應的ICompute.java類,看看系統給我們生成的代碼:

public interface ICompute extends android.os.IInterface {
    public static abstract class Stub extends android.os.Binder implements com.example.test.app.ICompute {
        private static final java.lang.String DESCRIPTOR = "com.example.test.app.ICompute";

        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        public static com.example.test.app.ICompute asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.example.test.app.ICompute))) {
                return ((com.example.test.app.ICompute) iin);
            }
            return new com.example.test.app.ICompute.Stub.Proxy(obj);
        }

        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_add: {
                    data.enforceInterface(DESCRIPTOR);
                    int _arg0;
                    _arg0 = data.readInt();
                    int _arg1;
                    _arg1 = data.readInt();
                    int _result = this.add(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        private static class Proxy implements com.example.test.app.ICompute {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            @Override
            public int add(int a, int b) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(a);
                    _data.writeInt(b);
                    mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }
        static final int TRANSACTION_add = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    }

    public int add(int a, int b) throws android.os.RemoteException;
}

下面我們簡單分析一下這段代碼:
首先,它聲明了幾個接口方法,同時還聲明了幾個整型的id用于標識這些方法,id用于標識在transact過程中客戶端所請求的到底是哪個方法以及一個內部類Stub和代理類Proxy。

  1. asInterface():用于將服務端返回的Binder對象轉換成客戶端所需的AIDL接口類型的對象,這種轉換過程是區分進程的,如果客戶端和服務端是在同一個進程中,那么這個方法返回的是服務端的Stub對象本身,否則返回的是系統封裝的Stub.Proxy對象,其決定了之后的請求是否跨進程。
  2. asBinder():返回當前Binder對象。
  3. onTransact():這個方法運行在服務端中的Binder線程池中,當客戶端發起跨進程請求時,遠程請求會通過系統底層封裝后交由此方法來處理。
  4. Stub類繼承自Binder,意味著這個Stub其實自己是一個Binder本地對象,并且其實現了ICompute接口,ICompute本身是一個IInterface,因此它實現了Client需要的能力(這里是方法add)。
  5. Proxy#[Method]:代理類中的接口方法,當客戶端調用這些方法時,它首先封裝參數,接著調用transact方法來發起RPC請求,同時當前線程掛起(即binder通信是同步阻塞的);然后服務端的onTransact方法會被調用,直到RPC過程返回后,當前線程繼續執行,獲取執行結果。

3.5.6 注意事項

需要注意的是使用多進程會有以下幾個問題:

  • 靜態成員和單例模式完全失效;
  • 線程同步機制完全失效,無論鎖對象還是鎖全局對象都無法保證線程同步;
  • SharedPreferences的可靠性下降,SharedPreferences不支持并發讀寫;
  • Application會多次創建,當一個組件跑在一個新的進程的時候,系統要在創建新的進程的同時分配獨立的虛擬機,應用會重新啟動一次,也就會創建新的Application。同一個應用的不同組件,如果它們運行在不同進程中,那么和它們分別屬于兩個應用沒有本質區別。

linkToDeath和unlinkToDeath

這里我們看兩個App開發時的重要方法:linkToDeath和unlinkToDeath。如果Binder服務端異常終止了,則會導致客戶端的遠程調用失敗,所以Binder提供了兩個配對的方法linkToDeath和unlinkToDeath,通過linkToDeath方法可以給Binder設置一個死亡代理,當Binder死亡的時候客戶端就會收到通知,然后就可以重新發起連接請求從而恢復連接了。代碼如下所示:

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
    @Override
    public void binderDied() {
        if (mRemoteBookManager == null) {
            return;
        }
        mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
        mRemoteBookManager = null;
        // TODO:這里重新綁定遠程Service
    }
};
mRemoteBookManager.asBinder().linkToDeath(mDeathRecipient, 0);

Binder連接池

當項目規模很大的時候,不應該創建多個Service,而是將所有的AIDL放在同一個Service中去管理。整個工作機制是:每個業務模塊創建自己的AIDL接口并實現此接口,這個時候不同業務模塊之間是不能有耦合的,所有實現細節我們要單獨開來,然后向服務端提供自己的唯一標識和其對應的Binder對象;對于服務端來說,只需要一個Service,服務端提供一個queryBinder接口,這個接口能夠根據業務模塊的特征來返回相應的Binder對象給它們,不同的業務模塊拿到所需的Binder對象后就可以進行遠程方法調用了。Binder連接池的主要作用就是將每個業務模塊的Binder請求統一轉發到遠程Service去執行,從而避免了重復創建Service的過程。

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

推薦閱讀更多精彩內容