AIDL

目錄

  • 0x10 什么是 AIDL

  • 0x20 什么時候用 AIDL

  • 0x30 需要注意什么

  • 0x40 定義 AIDL 接口

    • 0x41 創(chuàng)建 .aidl 文件
    • 0x42 實現(xiàn)接口
    • 0x43 向客戶端公開該接口
  • 0x50 通過 IPC 傳遞對象

  • 0x51 調用 IPC 方法

可閱讀官方原文,我只是重讀舊文,添加了一點東西,順便記錄,也改了一些翻譯
https://developer.android.google.cn/guide/components/aidl.html

0x10 什么是 AIDL

AIDL(Android Interface Definition Language)是一種IDL 語言,用于生成可以在Android設備上兩個進程之間進行進程間通信(Interprocess Communication, IPC)的代碼。

0x20 什么時候用 AIDL

只有允許不同應用的客戶端用 IPC 方式訪問自己的服務,并且想要在服務中處理多線程時,才有必要使用 AIDL。如果不需要執(zhí)行跨應用的并發(fā) IPC,可以通過 擴展一個 Binder創(chuàng)建接口;或者,想要執(zhí)行 IPC,但根本不需要處理多線程時,可以使用 Messenger 類來實現(xiàn)接口。無論如何,在實現(xiàn) AIDL 之前,務必理解綁定服務

0x30 需要注意什么

在開始設計 AIDL 接口之前,要注意 AIDL 接口的調用是直接函數(shù)調用,不應該假設調用發(fā)生在某個線程。視調用來自本地進程還是遠程進程中的線程,實際情況會有所差異。具體而言:

  • 來自本地進程的調用在發(fā)起調用的同一線程內執(zhí)行。如果該線程是 UI 線程,則 AIDL 接口方法繼續(xù)在該線程中執(zhí)行; 如果該線程是其他線程,則服務中的代碼會在該線程中執(zhí)行。 因此,只有在本地線程訪問服務時,您才能完全控制哪些線程在服務中執(zhí)行(但如果真是這種情況,根本不應該使用 AIDL,而是應該通過擴展 Binder 類創(chuàng)建接口)。
  • 來自遠程進程的調用通過在自有進程內部由平臺維護的線程池調度。必須為來自未知線程的并發(fā)調用做好準備,換言之,AIDL 接口的實現(xiàn)必須是完全線程安全的。
  • oneway 關鍵字用于修改遠程調用的行為。使用該關鍵字時,遠程調用不會阻塞;它只是發(fā)送事務數(shù)據(jù)并立即返回。接口的實現(xiàn)最終接收此調用時,是以正常遠程調用形式將其作為來自 Binder 線程池的常規(guī)調用進行接收。如果 oneway 用于本地調用,則不會有任何影響,調用仍是同步的。

0x40 定義 AIDL 接口

使用 Java 在 .aidl 文件中定義 AIDL 接口,然后將它保存在托管服務的應用以及任何其它綁定到服務的應用的源代碼(src/ 目錄)內。
當編譯每個包含 .aidl 文件的工程時,Android SDK 工具會生成一個基于該 .aidl 文件的 IBinder 接口,并將其保存在項目的 gen/ 目錄中。服務必須視情況實現(xiàn) IBinder 接口。然后客戶端應用便可綁定到該服務,并調用 IBinder 中的方法來執(zhí)行 IPC。
如需使用 AIDL 創(chuàng)建綁定服務,需執(zhí)行以下步驟:

  1. 創(chuàng)建 .aidl 文件
    此文件定義帶有方法簽名的編程接口。
  2. 實現(xiàn)接口
    Android SDK 工具基于 .aidl 文件,使用 Java 語言生成一個接口。此接口具有一個名為 Stub 的內部抽象類,這個內部抽象類擴展了 Binder 類并實現(xiàn) AIDL 接口中的方法。你必須擴展 Stub 類并實現(xiàn)方法。
  3. 向客戶端公開該接口
    實現(xiàn) Service 并重寫 onBind(),在這個方法中返回 Stub 類的實現(xiàn)。

:在 AIDL 接口首次發(fā)布后對其進行的任何更改都必須保持向后兼容性,以避免影響其他應用對您的服務的使用。 也就是說,因為必須將 .aidl 文件復制到其他應用,才能讓這些應用訪問服務的接口,因此必須保留對原始接口的支持。

0x41 創(chuàng)建 .aidl 文件

使用簡單的語法便可聲明包含一個或多個方法的接口,這些方法可以帶有參數(shù)和返回值。參數(shù)和返回值可以是任意類型,甚至可以是其他 AIDL 生成的接口。
必須使用 Java 語言構建 .aidl 文件。每個 .aidl 文件都必須定義單個接口,并且只需包含接口聲明和方法簽名。
默認情況下,AIDL 支持以下數(shù)據(jù)類型:

  • Java 語言中所有的原始類型(如 int、long、char、boolean 等)
  • String
  • CharSequence
  • List
    List 中的元素必須是以上列表中的數(shù)據(jù)類型、其它 AIDL 生成的接口或自己聲明的 Parcelable。選擇將 List 用作“通用”類(例如,List<String>)。另一端實際接收的具體類始終是 ArrayList,但生成的方法使用的是 List。
  • Map
    Map 中的元素必須是以上列表中的數(shù)據(jù)類型、其它 AIDL 生成的接口或自己聲明的 Parcelable。不支持通用 Map(如 Map<String, Integer> 形式的 Map)。另一端實際接收的具體類始終是 HashMap,但生成的方法使用的是 Map。
    必須為以上未列出的每個附加類型加入一個 import 語句,即使這些類型是在與接口定義相同的包中。
    定義服務接口時,請注意:
  • 方法可帶零個或多個參數(shù),返回一個值或 void
  • 所有非原始參數(shù)都需要標出數(shù)據(jù)走向標記,inoutinout
    原始類型默認為 in,不能是其它方向。

  • 應該將方向限定為真正需要的方向,因為編組參數(shù)的開銷極大。
  • AIDL中的數(shù)據(jù)走向標記表示了在跨進程通信中數(shù)據(jù)的流向,其中 in 表示數(shù)據(jù)只能由客戶端流向服務端, out 表示數(shù)據(jù)只能由服務端流向客戶端,而 inout 則表示數(shù)據(jù)可在服務端與客戶端之間雙向流通。其中,數(shù)據(jù)流向是針對在客戶端中的那個傳入方法的參數(shù)對象而言的。in 為定向 tag 的話表現(xiàn)為服務端將會接收到一個參數(shù)對象的完整數(shù)據(jù),但是客戶端的參數(shù)對象不會因為服務端對傳參的修改而發(fā)生變動;out 的話表現(xiàn)為服務端將會接收到空對象,但是在服務端對接收到的空對象有任何修改之后客戶端將會同步變動;inout 為定向 tag 的情況下,服務端將會接收到客戶端傳來對象的完整信息,并且客戶端將會同步服務端對該對象的任何變動。
  • .aidl 文件中包含的所有代碼注釋都包含在生成的 IBinder 接口中(import 和 package 語句之前的注釋除外)
  • 只支持方法聲明,不能公開 AIDL 中的靜態(tài)字段。
    .aidl 文件示例:
// IRemoteService.aidl
package com.example.android;

// 在此聲明非原始類型的 import
 
/** 服務示例 */
interface IRemoteService {
    /** 返回服務的進程 ID */
    int getPid();

    /** 基本類型作為參數(shù)示例 */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString);
}

只需將的 .aidl 文件保存在項目的 src/ 目錄內,當編譯項目時,SDK 工具會在項目的 gen/ 目錄中生成 IBinder 接口文件。生成的文件名與 .aidl 文件名一致,只是使用了 .java 擴展名(例如,IRemoteService.aidl 生成的文件名是 IRemoteService.java)。
如果使用 Android Studio,增量編譯幾乎會立即生成 Binder 類。 如果不使用 Android Studio,則 Gradle 工具會在您下一次編譯項目時生成 Binder 類 — 你應該在編寫完 .aidl 文件后立即用 gradle assembleDebug (或 gradle assembleRelease)編譯項目,以便的代碼能夠鏈接到生成的類。

0x42 實現(xiàn)接口

當開發(fā)應用時,Android SDK 工具會生成一個以 .aidl 文件命名的 .java 接口文件。生成的接口包括一個名為 Stub 的子類,這個子類是其父接口(例如,YourInterface.Stub)的抽象實現(xiàn),用于聲明 .aidl 文件中的所有方法。

:Stub 還定義了幾個幫助方法,其中最引人關注的是 asInterface(),該方法帶有 IBinder 參數(shù)(通常是傳遞給客戶端 onServiceConnected() 回調方法的參數(shù))并返回 stub 接口實例。如需了解如何進行這種轉換的更多詳細信息,請參見后面 調用IPC方法 一節(jié)。
如需實現(xiàn) .aidl 生成的接口,請擴展生成的 Binder 接口(例如,YourInterface.Stub)并實現(xiàn)從 .aidl 文件繼承的方法。
示例:

private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
    public int getPid() {
        return Process.myPid();
    }
    public void basicTypes(int anInt, long aLong, boolean aBoolean,
        float aFloat, double aDouble, String aString) {
        // Does nothing
    }
}

mBinder 是 Stub 類的一個實例(一個 Binder),用于定義服務的 RPC 接口。 在下一步中,將向客戶端公開該實例,以便客戶端能與服務進行交互。
在實現(xiàn) AIDL 接口時應該遵守以下幾個規(guī)則:

  • 由于不能保證在主線程上執(zhí)行傳入調用,因此一開始就需要做好多線程處理準備,并將服務編寫為線程安全的服務。
  • 默認情況下,RPC 調用是同步調用。如果明知服務完成請求的時間不止幾毫秒,就不應該從主線程調用服務,以免引起 ANR,應該從客戶端內單獨線程調用服務。
  • 任何異常都不會回傳給調用方。

0x43 向客戶端公開該接口

為服務實現(xiàn)該接口后,就需要向客戶端公開該接口,以便客戶端進行綁定。要公開接口,需要擴展 Service 并實現(xiàn) onBind(),以返回一個類實例,這個類實現(xiàn)了生成的 Stub。以下為示例:

public class RemoteService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // 返回接口實例
        return mBinder;
    }

    private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
        public int getPid(){
            return Process.myPid();
        }
        public void basicTypes(int anInt, long aLong, boolean aBoolean,
            float aFloat, double aDouble, String aString) {
            // Does nothing
        }
    };
}

現(xiàn)在,當客戶端(如 Activity)調用 bindService() 連接此服務時,客戶端的 onServiceConnected() 回調會接收服務的 onBind() 方法返回的 mBinder 實例。
客戶端還必須具有對 interface 類的訪問權限,因此如果客戶端和服務在不同的應用內,則客戶端項目的 src/ 目錄內必須包含 .aidl 文件的副本。
當客戶端在 onServiceConnected() 回調中收到 IBinder 時,它必須調用 YourServiceInterface.Stub.asInterface(service) 以將返回的參數(shù)轉換成 YourServiceInterface 類型。例如:

IRemoteService mIRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
    // 當與服務的連接建立時調用
    public void onServiceConnected(ComponentName className, IBinder service) {
        // 以上面的示例為例,生成 IRemoteService 的實例
        mIRemoteService = IRemoteService.Stub.asInterface(service);
    }

    // 當服務連接意外斷開時調用
    public void onServiceDisconnected(ComponentName className) {
        Log.e(TAG, "Service has unexpectedly disconnected");
        mIRemoteService = null;
    }
};

0x50 通過 IPC 傳遞對象

通過 IPC 接口把某個類從一個進程發(fā)送到另一個進程是可以實現(xiàn)的。不過,必須保證該類的代碼對 IPC 通道另一端可用,并且該類必須支持 Parcelable 接口。支持 Parcelable 接口很重要,應為 Android 系統(tǒng)可通過它將對象分解成可編組到各進程的原語。
如需創(chuàng)建支持 Parcelable 協(xié)議的類,必須執(zhí)行以下操作:

  1. 讓你的類實現(xiàn) Parcelable 接口。
  2. 實現(xiàn) writeToParcel,它會獲取對象當前的狀態(tài)并將其寫入 Parcel。
  3. 為你的類添加一個名為 CREATOR 的靜態(tài)字段,這個字段是一個實現(xiàn) Parcelable.Creator 接口的對象。
  4. 最后,創(chuàng)建一個聲明了 parcelable 的類的 .aidl 文件。
    如果使用的是自定義編譯進程,切勿在您的編譯中添加 .aidl 文件。 此 .aidl 文件與 C 語言中的頭文件類似,并未編譯。
    AIDL 在它生成的代碼中使用這些方法和字段將你的對象編組和取消編組。
    例如,以下這個 Rect.aidl 文件可創(chuàng)建一個可使用 IPC 傳遞的 Rect 類:
package android.graphics;

// 聲明 Rect 類已經(jīng)實現(xiàn)了 parcelable 協(xié)議,以便 AIDL 可以發(fā)現(xiàn)并使用
parcelable Rect;

以下展示了 Rect 類如何實現(xiàn) Parcelable:

import android.os.Parcel;
import android.os.Parcelable;

public final class Rect implements Parcelable {
    public int left;
    public int top;
    public int right;
    public int bottom;

    public static final Parcelable.Creator<Rect> CREATOR = new
Parcelable.Creator<Rect>() {
        public Rect createFromParcel(Parcel in) {
            return new Rect(in);
        }

        public Rect[] newArray(int size) {
            return new Rect[size];
        }
    };

    public Rect() {
    }

    private Rect(Parcel in) {
        readFromParcel(in);
    }

    public void writeToParcel(Parcel out) {
        out.writeInt(left);
        out.writeInt(top);
        out.writeInt(right);
        out.writeInt(bottom);
    }

    public void readFromParcel(Parcel in) {
        left = in.readInt();
        top = in.readInt();
        right = in.readInt();
        bottom = in.readInt();
    }
}

Rect 類中的編組相當簡單。看一看 Parcel 上的其它方法,了解可以向 Parcel 寫入哪些其他類型的值。

警告:別忘記從其他進程接收數(shù)據(jù)的安全影響。 在本例中, Rect 從 Parcel 讀取四個數(shù)字,但要由您來確保無論調用方目的為何這些數(shù)字都在相應的可接受值范圍內。 如需了解有關如何防止應用受到惡意軟件侵害、保證應用安全的更多信息,請參見安全與權限

0x51 調用 IPC 方法

調用類必須執(zhí)行以下步驟,才能調用使用 AIDL 定義的遠程接口:

  1. 在項目 src/ 目錄中加入 .aidl 文件。
  2. 聲明一個 IBinder 接口實例(基于 AIDL 生成)。
  3. 實現(xiàn) ServiceConnection。
  4. 調用 Context.bingService(),傳入 ServiceConnection 實現(xiàn)。
  5. 在 onServiceConnected() 實現(xiàn)中,你將收到一個 IBinder 實例。調用 YourInterfaceName.Stub.asInterface((IBinder)service),以將返回的參數(shù)轉換為 YourInterface 類型。
  6. 調用在接口上定義的方法。應該始終捕獲 DeadObjectException 異常,它們是在連接中斷時引發(fā)的,這將是遠程方法引發(fā)的唯一異常。
  7. 如需斷開連接,調用 Context.unbindService()。
    有關調用 IPC 服務的幾點說明:
  • 對象的引用計數(shù)是跨進程的。
  • 可以將匿名對象作為方法參數(shù)發(fā)送。
    以下代碼摘自 ApiDemos 項目的遠程服務示例代碼,展示了如何調用 AIDL 創(chuàng)建的服務。
public static class Binding extends Activity {
    /** The primary interface we will be calling on the service. */
    IRemoteService mService = null;
    /** Another interface we use on the service. */
    ISecondary mSecondaryService = null;

    Button mKillButton;
    TextView mCallbackText;

    private boolean mIsBound;

    /**
     * Standard initialization of this activity.  Set up the UI, then wait
     * for the user to poke it before doing anything.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.remote_service_binding);

        // Watch for button clicks.
        Button button = (Button)findViewById(R.id.bind);
        button.setOnClickListener(mBindListener);
        button = (Button)findViewById(R.id.unbind);
        button.setOnClickListener(mUnbindListener);
        mKillButton = (Button)findViewById(R.id.kill);
        mKillButton.setOnClickListener(mKillListener);
        mKillButton.setEnabled(false);

        mCallbackText = (TextView)findViewById(R.id.callback);
        mCallbackText.setText("Not attached.");
    }

    /**
     * Class for interacting with the main interface of the service.
     */
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // This is called when the connection with the service has been
            // established, giving us the service object we can use to
            // interact with the service.  We are communicating with our
            // service through an IDL interface, so get a client-side
            // representation of that from the raw service object.
            mService = IRemoteService.Stub.asInterface(service);
            mKillButton.setEnabled(true);
            mCallbackText.setText("Attached.");

            // We want to monitor the service for as long as we are
            // connected to it.
            try {
                mService.registerCallback(mCallback);
            } catch (RemoteException e) {
                // In this case the service has crashed before we could even
                // do anything with it; we can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
            }

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_connected,
                    Toast.LENGTH_SHORT).show();
        }

        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            mService = null;
            mKillButton.setEnabled(false);
            mCallbackText.setText("Disconnected.");

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_disconnected,
                    Toast.LENGTH_SHORT).show();
        }
    };

    /**
     * Class for interacting with the secondary interface of the service.
     */
    private ServiceConnection mSecondaryConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // Connecting to a secondary interface is the same as any
            // other interface.
            mSecondaryService = ISecondary.Stub.asInterface(service);
            mKillButton.setEnabled(true);
        }

        public void onServiceDisconnected(ComponentName className) {
            mSecondaryService = null;
            mKillButton.setEnabled(false);
        }
    };

    private OnClickListener mBindListener = new OnClickListener() {
        public void onClick(View v) {
            // Establish a couple connections with the service, binding
            // by interface names.  This allows other applications to be
            // installed that replace the remote service by implementing
            // the same interface.
            Intent intent = new Intent(Binding.this, RemoteService.class);
            intent.setAction(IRemoteService.class.getName());
            bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
            intent.setAction(ISecondary.class.getName());
            bindService(intent, mSecondaryConnection, Context.BIND_AUTO_CREATE);
            mIsBound = true;
            mCallbackText.setText("Binding.");
        }
    };

    private OnClickListener mUnbindListener = new OnClickListener() {
        public void onClick(View v) {
            if (mIsBound) {
                // If we have received the service, and hence registered with
                // it, then now is the time to unregister.
                if (mService != null) {
                    try {
                        mService.unregisterCallback(mCallback);
                    } catch (RemoteException e) {
                        // There is nothing special we need to do if the service
                        // has crashed.
                    }
                }

                // Detach our existing connection.
                unbindService(mConnection);
                unbindService(mSecondaryConnection);
                mKillButton.setEnabled(false);
                mIsBound = false;
                mCallbackText.setText("Unbinding.");
            }
        }
    };

    private OnClickListener mKillListener = new OnClickListener() {
        public void onClick(View v) {
            // To kill the process hosting our service, we need to know its
            // PID.  Conveniently our service has a call that will return
            // to us that information.
            if (mSecondaryService != null) {
                try {
                    int pid = mSecondaryService.getPid();
                    // Note that, though this API allows us to request to
                    // kill any process based on its PID, the kernel will
                    // still impose standard restrictions on which PIDs you
                    // are actually able to kill.  Typically this means only
                    // the process running your application and any additional
                    // processes created by that app as shown here; packages
                    // sharing a common UID will also be able to kill each
                    // other's processes.
                    Process.killProcess(pid);
                    mCallbackText.setText("Killed service process.");
                } catch (RemoteException ex) {
                    // Recover gracefully from the process hosting the
                    // server dying.
                    // Just for purposes of the sample, put up a notification.
                    Toast.makeText(Binding.this,
                            R.string.remote_call_failed,
                            Toast.LENGTH_SHORT).show();
                }
            }
        }
    };

    // ----------------------------------------------------------------------
    // Code showing how to deal with callbacks.
    // ----------------------------------------------------------------------

    /**
     * This implementation is used to receive callbacks from the remote
     * service.
     */
    private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {
        /**
         * This is called by the remote service regularly to tell us about
         * new values.  Note that IPC calls are dispatched through a thread
         * pool running in each process, so the code executing here will
         * NOT be running in our main thread like most other things -- so,
         * to update the UI, we need to use a Handler to hop over there.
         */
        public void valueChanged(int value) {
            mHandler.sendMessage(mHandler.obtainMessage(BUMP_MSG, value, 0));
        }
    };

    private static final int BUMP_MSG = 1;

    private Handler mHandler = new Handler() {
        @Override public void handleMessage(Message msg) {
            switch (msg.what) {
                case BUMP_MSG:
                    mCallbackText.setText("Received from service: " + msg.arg1);
                    break;
                default:
                    super.handleMessage(msg);
            }
        }

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

推薦閱讀更多精彩內容