Android跨進程通信IPC之19——AIDL

移步系列Android跨進程通信IPC系列

  • 1 AIDL簡介
  • 2 為什么要設置AIDL
  • 3 AIDL的注意事項
  • 4 AIDL的使用
  • 5 源碼跟蹤
  • 6 AIDL的設計給我們的思考
  • 7 總結

1 AIDL簡介

  • AIDL是一個縮寫,全程是Android Interface Definition Language,也是android接口定義語言。
  • 準確的來說,它是用于定義客戶端/服務器通信接口的一種描述語言。它其實一種IDL語言,可以拿來生成用于IPC的代碼。從某種意義上說它其實是一個模板。

2 為什么要設置AIDL

2.1 IPC的角度

  • 設計這門語言的目的是為了實現進程間通信,尤其是在涉及多進程并發情況的下的進程間通信IPC。
  • 通過AIDL,可以讓本地調用遠程服務器的接口就像調用本地接口那么簡單,讓用戶無需關注內部細節,只需要實現自己的業務邏輯接口,內部復雜的參數序列化發送、接收、客戶端調用服務端的邏輯,你都不需要去關心了。

2.2 方便角度

在Android process 之間不能用通常的方式去訪問彼此的內存數據。他們把需要傳遞的數據解析成基礎對象,使得系統能夠識別并處理這些對象。因為這個處理過程很難寫,所以Android使用AIDL來解決這個問題

3 使用場景

  • 多個客戶端,多個線程并發的情況下要使用AIDL
  • 如果你的IPC不需要適用多個客戶端,就用Binder。
  • 如果你想要IPC,但是不需要多線程,那就選擇Messager

4 AIDL支持以下數據類型

  • Java基本類型,即int、long、char等;
  • String;
  • CharSequence;
  • List
    • List中的所有元素都必須是AIDL支持的數據類型、其他AIDL接口或你之前聲明的Parcelable實現類。
  • Map
    • Map中的所有元素都必須是AIDL支持的數據類型、其他AIDL接口或你之前聲明的Parcelable實現類。
  • 其他類型,必須要有import語句,即使它跟.aidl是同一個包下。
  • 關于復雜對象
  • 傳遞的復雜對象必須要實現Parcelable接口,這是因為Parcelable允許Android系統將復雜對象分解成基本類型以便在進程間傳輸.
  • 若客戶端組件和服務分開在不同APP,必須要把該Parcelable實現類.java文件拷貝到客戶端所在的APP,包路徑要一致。
  • 另外,需要為這個Parcelable實現類定義一個相應的.aidl文件。與AIDL服務接口.aidl同理,客戶端所在APP的/src/<SourceSet>/aidl目錄下也要有這份副本。
TIM截圖20180821173719.png

5 AIDL中的方法和變量

  • 方法可有零、一或多個參數,可有返回值或void。
  • 所有非基本類型的參數都需要標簽來表明這個數據的去向
    • in,表示此變量由客戶端設置;
    • out,表示此變量由服務端設置;
    • inout,表示此變量可由客戶端和服務端設置;
    • 基本類型只能是in。
  • AIDL中的定向tag表示在跨進程通信中數據 流向
  • in表示數據只能由客戶端流向服務端,out表示數據只能由服務端流行客戶端,而inout則表示數據可在服務端與客戶端之間雙向流通。
  • 其中,數據流向是針對客戶端中的那個傳入方法的對象而言。
  • in為定向tag的話,表現為服務端將會接受到一個那個對象的完整數據,但是客戶端的那個對象不會因為服務端傳參修改而發生變動;
  • out的話表現為服務端將會接收到那個對象的參數為空的對象,但是在服務端對接收到的空對象有任何修改之后客戶端將會同步變動;
  • inout為定向tag的情況下,服務端將會接收到客戶端傳來對象的完整信息,并且客戶端將會同步服務對該對象的任何變動。

6 AIDL的使用(復雜對象為例)

6.1 服務端

我們以在Android Studio為例進行講解

6.1.1 創建復雜對象

public class MessageData implements Parcelable {

    public long id;  //消息的id
    public String content; //消息的內容
    public long time;  //時間

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", content='" + content + '\'' +
                ", time=" + time +
                '}';
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(id);
        dest.writeString(content);
        dest.writeLong(time);
    }

    public MessageData(Parcel source) {
        id = source.readLong();
        content = source.readString();
        time = source.readLong();
    }

    public MessageData() {

    }

    public void readFromParcel(Parcel in) {
        id = in.readLong();
        content = in.readString();
        time = in.readLong();
    }


    public static final Creator<MessageData> CREATOR = new Creator<MessageData>() {
        @Override
        public MessageData createFromParcel(Parcel source) {
            return new MessageData(source);
        }

        @Override
        public MessageData[] newArray(int size) {
            return new MessageData[size];
        }
    };
}

6.1.2 創建AIDL文件夾

TIM截圖20180821160304.png

6.1.3 創建AIDL文件(復雜對象及服務)

復雜對象
[MessageData.aidl]

// MessageData.aidl
package com.kai.ling.myapplication;

// Declare any non-default types here with import statements
parcelable MessageData;

服務

// IMyAidlInterface.aidl
package com.kai.ling.myapplication;
// Declare any non-default types here with import statements
import com.kai.ling.myapplication.MessageData;

interface IMyAidlInterface {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);

    void connect();

    void sendMessage(inout MessageData message);
}

編譯后會生成對應的aidl文件


TIM截圖20180821174715.png

6.1.4 書寫服務端server繼承IMyAidlInterface.Stub

public class MyServer extends IMyAidlInterface.Stub {

    private static final String TAG = "GEBILAOLITOU";


    @Override
    public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

    }

    @Override
    public void connect() throws RemoteException {
        Log.i(TAG, "connect");
    }

    @Override
    public void sendMessage(MessageData message) throws RemoteException {
        Log.i(TAG, "MyServer ** sendInMessage **" + message.toString());
    }

}

6.1.5 service中返回server 并設置多進程

public class PushService extends Service {

    private MyServer myServer=new MyServer();


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return myServer;
    }
}
        <service
            android:name=".server.PushService"
            android:enabled="true"
            android:exported="true"
            android:process=":push"/>

6.2 客戶端

管理類

public class PushManager {

    private static final String TAG = "GEBILAOLITOU";
    
    private PushManager() {
    }

    private IMyAidlInterface iMyAidlInterface;
    
    private static PushManager instance = new PushManager();
    
    //單例
    public static PushManager getInstance() {
        return instance;
    }


    //綁定服務
    public void init(Context context) {
        //定義intent
        Intent intent = new Intent(context, PushService.class);
        //綁定服務
        context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //成功連接
            Log.d(TAG, "pushManager ***************成功連接***************");
            //通過asInterface獲取
            iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            //斷開連接調用
            Log.d(TAG, "pushManager ***************連接已經斷開***************");
        }
    };

    //遠程調用connect()方法
    public void connect() {
        try {
            Log.d(TAG, "pushManager ***************start Remote***************");
            iMyAidlInterface.connect();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    //遠程調用sendMessage()方法
    public void sendMessage(String content) {
        MessageData messageData = new MessageData();
        messageData.content = content;
        try {
            iMyAidlInterface.sendMessage(messageData);
        } catch (RemoteException e) {
            e.printStackTrace();
            Log.d(TAG, "pushManager ***************RemoteException***************");
        }
    }
}
public class MainActivity extends AppCompatActivity {

    private boolean isConnected;
    private EditText content;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化
        PushManager.getInstance().init(this);

        content = findViewById(R.id.content);

        findViewById(R.id.connect).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PushManager.getInstance().connect();
                isConnected = true;
            }
        });

        findViewById(R.id.send).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!isConnected) {
                    Toast.makeText(MainActivity.this, "請連接", Toast.LENGTH_LONG).show();
                }
                if (TextUtils.isEmpty(content.getText().toString().trim())) {
                    Toast.makeText(MainActivity.this, "請輸入", Toast.LENGTH_LONG).show();
                }
                PushManager.getInstance().sendMessage(content.getText().toString().trim());
            }
        });
    }
}

7 源碼跟蹤

  • 在寫完AIDL文件后,編譯器會幫我們自動生成一個同名的.java文件。
  • 在我們實際編寫客戶端和服務端代碼的過程中,真正協助我們工作的其實就是這個文件,而.aidl文件從頭到尾都沒有出現過。
  • 這個.aidl文件就是為了生成這個對應的.java文件。事實上,就算我們不寫AIDL文件,直接按照它生成的.java文件這樣寫一個.java文件出來。在服務端和客戶端也可以照常使用這個.java類進行跨進程通信。
  • AIDL語言只是在簡化我們寫這個.java文件而已,而要研究AIDL是符合幫助我們進行跨境進程通信的,其實就是研究這個生成的.java文件是如何工作的

7.1 .java文件位置

TIM截圖20180822155143.png
  • 它的完整路徑是:app->build->generated->source->aidl->debug->com->gebilaolitou->android->aidl->IMyAidlInterface.java(其中 com.gebilaolitou.android.aidl
    是包名,相對應的AIDL文件為 IMyAidlInterface.aidl )。

7.2 IMyAidlInterface .java類分析

結構圖

TIM截圖20180822155826.png

  • 編譯的后IMyAidlInterface.java文件是一個接口,繼承自android.os.IInterface
  • IMyAidlInterface內部代碼主要分成兩部分,一個是抽象類Stub 和 原來aidl聲明的basicTypes()、connect()和sendInMessage()方法

7.3 Stub類分析

TIM截圖20180822160212.png
  • Stub類基本結構
  • 靜態方法 asInterface(android.os.IBinder obj)
  • 靜態內部類 Proxy
  • 方法 onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
  • 方法 asBinder()
  • private的String類型常量DESCRIPTOR
  • private的int類型常量TRANSACTION_connect
  • private的int類型常量TRANSACTION_sendInMessage

7.3.1 靜態方法 asInterface(android.os.IBinder obj)

public static com.kai.ling.myapplication.IMyAidlInterface asInterface(android.os.IBinder obj) {
    //非空判斷
    if ((obj == null)) {
        return null;
    }
    // DESCRIPTOR是常量為"com.gebilaolitou.android.aidl.IMyAidlInterface"
    // queryLocalInterface是Binder的方法,搜索本地是否有可用的對象
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    //如果有,則強制類型轉換并返回
    if (((iin != null) && (iin instanceof com.kai.ling.myapplication.IMyAidlInterface))) {
        return ((com.kai.ling.myapplication.IMyAidlInterface) iin);
    }
    //如果沒有,則構造一個IMyAidlInterface.Stub.Proxy對象
    return new com.kai.ling.myapplication.IMyAidlInterface.Stub.Proxy(obj);
}
  • 主要的作用就是根據傳入的Binder對象轉換成客戶端需要的IMyAidlInterface接口。
    • 如果客戶端和服務端處于同一進程,那么queryLocalInterface()方法返回就是服務端Stub對象本身;
    • 如果是跨進程,則返回一個封裝過的Stub.Proxy,也是一個代理類,在這個代理中實現跨進程通信。那么讓我們來看下Stub.Proxy類

7.3.2 onTransact()方法解析

onTransact()方法是根據code參數來處理,這里面會調用真正的業務實現類

  • 在onTransact()方法中,根據傳入的code值回去執行服務端相應的方法。其中常量TRANSACTION_connect和常量TRANSACTION_sendInMessage就是code值(在AIDL文件中聲明了多少個方法就有多少個對應的code)。
  • 其中data就是服務端方法需要的的參數,執行完,最后把方法的返回結果放入reply中傳遞給客戶端。如果該方法返回false,那么客戶端請求失敗。

7.3.3 靜態類Stub.Proxy

private static class Proxy implements com.kai.ling.myapplication.IMyAidlInterface {
    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;
    }

    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    @Override
    public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) 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.writeInt(anInt);
            _data.writeLong(aLong);
            _data.writeInt(((aBoolean) ? (1) : (0)));
            _data.writeFloat(aFloat);
            _data.writeDouble(aDouble);
            _data.writeString(aString);
            mRemote.transact(Stub.TRANSACTION_basicTypes, _data, _reply, 0);
            _reply.readException();
        } finally {
            _reply.recycle();
            _data.recycle();
        }
    }

    @Override
    public void connect() throws android.os.RemoteException {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        try {
            _data.writeInterfaceToken(DESCRIPTOR);
            mRemote.transact(Stub.TRANSACTION_connect, _data, _reply, 0);
            _reply.readException();
        } finally {
            _reply.recycle();
            _data.recycle();
        }
    }
    //發送消息  客戶端——> 服務器
    @Override
    public void sendMessage(com.kai.ling.myapplication.MessageData message) throws android.os.RemoteException {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        try {
            _data.writeInterfaceToken(DESCRIPTOR);
            if ((message != null)) {
                _data.writeInt(1);
                message.writeToParcel(_data, 0);
            } else {
                _data.writeInt(0);
            }
            mRemote.transact(Stub.TRANSACTION_sendMessage, _data, _reply, 0);
            _reply.readException();
            if ((0 != _reply.readInt())) {
                message.readFromParcel(_reply);
            }
        } finally {
            _reply.recycle();
            _data.recycle();
        }
    }
}

  • 1、Proxy 實現了 com.gebilaolitou.android.aidl.IMyAidlInterfac接口,所以他內部有IMyAidlInterface接口的兩個抽象方法
  • 2、Proxy的asBinder()方法返回的mRemote,而這個mRemote是什么時候被賦值的?是在構造函數里面被賦值的。

7.3.3.1 靜態類Stub.Proxy的connect()方法

  • 1、 通過閱讀靜態類Stub.Proxy的connect()方法,我們容易分析出來里面的兩個android.os.Parcel_data_reply是用來進行跨進程傳輸的"載體"。而且通過字面的意思,很容易猜到,*_data用來存儲 客戶端流向服務端 的數據,_reply用來存儲 服務端流向客戶端 的數據。
  • 2、通過mRemote. transact()方法,將_data和_reply傳過去
  • 3、通過_reply.readException()來讀取服務端執行方法的結果。
  • 4、最后通過finally回收l_data和_reply

7.3.3.2 connect() 相關參數

  • 關于 transact()方法:這是客戶端和和服務端通信的核心方法,也是IMyAidlInterface.Stub繼承android.os.Binder而重寫的一個方法。
  • 調起這個方法之后,客戶端將會掛起當前線程,等候服務端執行完相關任務后,通知并接收返回的_reply數據流。
  • 1 方法ID:transact()方法第一個參數是一個方法ID,這個是客戶端和服務端約定好的給方法的編碼,彼此一一對應。在AIDL文件轉話為.java時候,系統會自動給AIDL里面的每一個方法自動分配一個方法ID。而這個ID就是咱們說的常量TRANSACTION_connect和TRANSACTION_sendInMessage這些常量生成了遞增的ID,是根據你在aidl文件的方法順序而來,然后在IMyAidlInterface.Stub中的onTransact()方法里面switch根據第一個參數code即我們說的ID而來。
  • 2最后的一個參數:transact()方法最后一個參數是一個int值,代表是單向的還是雙向的。具體大家請參考我們前面的文章Android跨進程通信IPC之8——Binder的三大接口中關于IBinder部分。我這里直接說結論:0表示雙向流通,即_reply可以正常的攜帶數據回來。如果為1的話,那么數據只能單向流程,從服務端回來的數據_reply不攜帶任何數據。注意:AIDL生成的.java文件,這個參數均為0

7.3.4 asBinder()方法

該方法就是返回當前的Binder方法

8 IMyAidlInterface .java流程分析

以上面的例子為例,那么先從客戶端開始

8.1 客戶端

8.1.1 獲取IMyAidlInterface對象

public void init(Context context){
        //定義intent
        Intent intent = new Intent(context,PushService.class);
        //綁定服務
        context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //成功連接
            Log.i(TAG,"PushManager ***************成功連接***************");
            iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            //斷開連接調用
            Log.i(TAG,"PushManager ***************連接已經斷開***************");
        }
    };
  • 客戶端中通過Intent去綁定一個服務端的Service。
  • onServiceConnected(ComponentName name, IBinder service)方法中通過返回service可以得到AIDL接口的實例。
  • 這是調用了asInterface(android.os.IBinder) 方法完成的。在asInterface(android.os.IBinder)我們知道是調用的new com.gebilaolitou.android.aidl.IMyAidlInterface.Stub.Proxy(obj)構造的一個Proxy對象。
  • 所以可以這么說在PushManager中的變量IMyAidlInterface其實是一個IMyAidlInterface.Stub.Proxy對象。

8.1.2 調用connect()方法

PushManager類中的iMyAidlInterface其實IMyAidlInterface.Stub.Proxy對象,所以調用connect()方法其實是IMyAidlInterface.Stub.Proxy的connect()方法。

  • 1、這里面主要是生成了_data和_reply數據流,并向_data中存入客戶端的數據。
  • 2、通過 transact()方法將他們傳遞給服務端,并請求服務指定的方法
  • 3、接收_reply數據,并且從中讀取服務端傳回的數據。

通過上面客戶端的所有行為,我們會發現,其實通過ServiceConnection類中onServiceConnected(ComponentName name, IBinder service)中第二個參數service很重要,因為我們最后是用它的transact() 方法,將客戶端的數據和請求發送給服務端去。從這個角度來看,這個service就像是服務端在客戶端的代理一樣,而IMyAidlInterface.Stub.Proxy對象更像一個二級代理,我們在外部通過調用這個二級代理來間接調用service這個一級代理

8.2 服務端流程

在前面幾篇文章中我們知道Binder傳輸中,客戶端調用transact()對應的是服務端的onTransact()函數,我們在IMyAidlInterface.java中看到
查看onTransact()

       @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_basicTypes: {
                    data.enforceInterface(DESCRIPTOR);
                    int _arg0;
                    _arg0 = data.readInt();
                    long _arg1;
                    _arg1 = data.readLong();
                    boolean _arg2;
                    _arg2 = (0 != data.readInt());
                    float _arg3;
                    _arg3 = data.readFloat();
                    double _arg4;
                    _arg4 = data.readDouble();
                    java.lang.String _arg5;
                    _arg5 = data.readString();
                    this.basicTypes(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_connect: {
                    data.enforceInterface(DESCRIPTOR);
                    this.connect();
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_sendMessage: {
                    data.enforceInterface(DESCRIPTOR);
                    com.kai.ling.myapplication.MessageData _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.kai.ling.myapplication.MessageData.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.sendMessage(_arg0);
                    reply.writeNoException();
                    if ((_arg0 != null)) {
                        reply.writeInt(1);
                        _arg0.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
                    } else {
                        reply.writeInt(0);
                    }
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

在收到客戶端的 transact()方法后,直接調用了switch選擇,根據ID執行不同操作,因為我們知道是調用的connect()方法,所以對應的code是TRANSACTION_connect,所以我們下case TRANSACTION_connect:的內容,如下:

case TRANSACTION_connect: {
    data.enforceInterface(DESCRIPTOR);
    this.connect();
    reply.writeNoException();
    return true;
}

這里面十分簡單了,就是直接調用服務端的connect()方法。

9 整體流程如下:

5713484-298c59e740ccf7e3.png

5713484-2c51ce26ad1e82a9.png

參考

Android跨進程通信IPC之11——AIDL

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

推薦閱讀更多精彩內容