《Android 開發(fā)藝術(shù)探索》筆記2--IPC機(jī)制

IPC機(jī)制.png

Android IPC簡(jiǎn)介

IPC是Inter-Process Communication縮寫,含義為進(jìn)程間通信. 按照操作系統(tǒng)中的描述,線程是cpu調(diào)度的最小單元,而進(jìn)程一般指一個(gè)執(zhí)行單元. 進(jìn)程中可以有一個(gè)或者多個(gè)線程.

不同的操作系統(tǒng)有著不同的IPC機(jī)制:

  • Windows: 通過剪切板, 管道, 信號(hào)量來進(jìn)行進(jìn)程間通信
  • Linux: 通過命名管道, 共享內(nèi)存, 信號(hào)量等來進(jìn)行進(jìn)行進(jìn)程間通信
  • android: 雖然基于Linux內(nèi)核,但是使用了獨(dú)有的Binder機(jī)制, 也可以Socket進(jìn)行通信

使用場(chǎng)景: 可能有些模塊因?yàn)樘厥庠蛐枰\(yùn)行在單獨(dú)的進(jìn)程中; 或者為了加大一個(gè)應(yīng)用可使用的內(nèi)存; 又或者我們需要去另外一個(gè)進(jìn)程去獲取數(shù)據(jù),必然需要跨進(jìn)程.

Android中的多進(jìn)程的模式

開啟多進(jìn)程模式

如果你想在一個(gè)應(yīng)用中使用多個(gè)進(jìn)程,通過清單文件給四大組件添加android:process屬性,就可以很方便的開啟多進(jìn)程.

還有一種非常規(guī)的創(chuàng)建方式,通過JNI在native層去fork一個(gè)新的進(jìn)程.這種只做了解.

image

例如這樣,當(dāng)我們依次打開MainActivity, SecondActivity, ThirdActivity.此時(shí)應(yīng)該打開了三個(gè)進(jìn)程.

我們來檢測(cè)一下, 你可以直接使用DDMS來查看進(jìn)程,這里使用命令行來測(cè)試

$ adb shell ps | grep com.szysky 你可以直接使用adb shell ps這會(huì)把系統(tǒng)所有進(jìn)程展示出來, 你可以加上過濾信息| grep xxx xxx替換你需要過濾出來信息即可

image

你可能已經(jīng)發(fā)現(xiàn)在創(chuàng)建新進(jìn)程的時(shí)候使用兩種不同的方式

  • 當(dāng)以:開頭的進(jìn)程,屬于當(dāng)前應(yīng)用的私有進(jìn)程,其他應(yīng)用的組件不可以和它跑在同一個(gè)進(jìn)程
  • 當(dāng)不以:開頭,那么進(jìn)程屬于全局進(jìn)程,其他應(yīng)用通過ShareUID方法可以和它跑在同一個(gè)進(jìn)程

Android系統(tǒng)會(huì)為每一個(gè)應(yīng)用分配唯一的UID. 相同UID的應(yīng)用才能共享數(shù)據(jù). 但是兩個(gè)應(yīng)用通過ShareUID跑在同一個(gè)進(jìn)程是有要求的. 除了具有相同的ShareUID并且還要簽名相同才可以. 這時(shí)如果不在同一進(jìn)程他們之間可以共享data目錄,組件信息等. 如果還在同一進(jìn)程, 那么他們還能共享內(nèi)存數(shù)據(jù).

進(jìn)程模式的運(yùn)行機(jī)制

開啟多進(jìn)程簡(jiǎn)單,但是如果不能處理好其中的特性,那么受傷的總會(huì)是你.

先說第一個(gè)比較嚴(yán)重的問題. 靜態(tài)變量不在共享. 還是直接三個(gè)類的例子,如果在mainActivity中對(duì)靜態(tài)變量進(jìn)行修改, 在SecondActivity取出這個(gè)靜態(tài)發(fā)現(xiàn)是main沒修改之前的. 這說明兩個(gè)進(jìn)程間即使是靜態(tài)屬性也是無法共享.

其實(shí)這是因?yàn)檫@兩個(gè)類運(yùn)行在兩個(gè)進(jìn)程間,而每個(gè)單獨(dú)的進(jìn)程又會(huì)分配一個(gè)獨(dú)立的虛擬機(jī), 所以每個(gè)虛擬機(jī)在內(nèi)存分配上有不同的地址空間.對(duì)于不同虛擬機(jī)訪問同一個(gè)對(duì)象就會(huì)產(chǎn)生多份副本. 副本之間互相獨(dú)立不干擾彼此.

一般情況下多進(jìn)程可能面臨的問題:

  1. 靜態(tài)成員和單例模式完全失效
  2. 線程同步機(jī)制完全失效
  3. SharedPreferences的可靠性下降
  4. Application會(huì)多次創(chuàng)建

2中因?yàn)椴皇且粔K內(nèi)存,所以不管是鎖對(duì)象還是鎖全局都無法保證線程同步,因?yàn)椴皇峭粋€(gè)對(duì)象. 3中因?yàn)镾p不支持兩個(gè)進(jìn)程同時(shí)讀寫,因?yàn)榈讓邮峭ㄟ^讀寫XML文件實(shí)現(xiàn)的,并發(fā)可能會(huì)觸發(fā)異常. 4中運(yùn)行在多個(gè)進(jìn)程中,那么就會(huì)創(chuàng)建多個(gè)虛擬機(jī),每個(gè)虛擬機(jī)都有一個(gè)對(duì)應(yīng)Application并需要啟動(dòng)加載這個(gè)文件.

一個(gè)應(yīng)用的多進(jìn)程:它就相當(dāng)于兩個(gè)不同的應(yīng)用采用了ShareUID的模式. 每個(gè)進(jìn)程都會(huì)擁有獨(dú)立的虛擬機(jī), Application以及內(nèi)存空間

IPC基礎(chǔ)概念

關(guān)于IPC主要包含三方面的內(nèi)容: Serializable接口, Parcelable接口, 以及Binder

Serializable接口

Serializable是Java提供的一個(gè)序列化接口,這個(gè)一個(gè)空接口. 如果我們想使用只需要實(shí)現(xiàn)Serializable接口,并聲明一個(gè)long類型的常量serialVersionUID(不聲明也是可以,但是在反序列化會(huì)出現(xiàn)錯(cuò)誤).

javabean 的實(shí)現(xiàn)

public class Student implements Serializable{

    public static final long serialVersionUID = 123456789L;

    //.....省略創(chuàng)建屬性,打印等操作

}

序列化的代碼如下圖,并附上結(jié)果.


image.png

好了說一下serialVersionUID這個(gè)屬性. 即使我們不聲明系統(tǒng)會(huì)根據(jù)當(dāng)前類結(jié)構(gòu)(成員變量等)生成一個(gè)hash為serialVersionUID, 雖然這樣也可以但是如果在你把一個(gè)對(duì)象序列化的到磁盤的一個(gè)文件的時(shí)候. 對(duì)這個(gè)對(duì)象增加了一個(gè)成員變量,那么在反序列的時(shí)候就會(huì)報(bào)錯(cuò). 因?yàn)楫?dāng)你反序列化的時(shí)候?qū)ο笕绻麤]有serialVersionUID還會(huì)重新計(jì)算.這時(shí)反序列化的hash和序列化的hash就不一致了.

關(guān)于根據(jù)當(dāng)前類結(jié)構(gòu)計(jì)算hash值,有兩點(diǎn)需要注意:

  • 靜態(tài)成員變量屬于類不屬于對(duì)象,所以不參與序列化的過程
  • 其次用transient關(guān)鍵字標(biāo)記的成員變量不參與序列化的過程.

系統(tǒng)默認(rèn)的序列化過程是可以改變的,通過實(shí)現(xiàn)writeObjectreadObject可以重寫默認(rèn)的序列化和反序列化過程. 這里就不詳細(xì)說明

Parcelable接口

系統(tǒng)已經(jīng)為我們提供了很多實(shí)現(xiàn)了Parcelable接口的類,他們都可以直接序列化. 例如intent, Bundle, Bitmap, 同時(shí)List和Map也可以序列化.前提是他們里面的每個(gè)元素都可以序列化.

實(shí)現(xiàn)Parcelable接口主要復(fù)寫四個(gè), 我們可以直接定義好javabean直接讓AS幫我們實(shí)現(xiàn).

  • writeToParcel() 主要完成序列化功能
  • CREATOR 主要完成反序列化
  • 接收參數(shù)parcel的構(gòu)造函數(shù) 用于從序列化后的對(duì)象中創(chuàng)建原始對(duì)象
  • describeContents() 幾乎所有情況下都返回0,只有當(dāng)前對(duì)象中存在文件描述符時(shí)返回1

關(guān)于Parcelable和Serializable的取舍

  • Serializable: 適合序列化到設(shè)備或者序列化后通過網(wǎng)絡(luò)傳輸.
  • Parcelable: 主要用在內(nèi)存序列化上. 不需要大量的I/O操作,所以在內(nèi)存中使用高效.

了解Binder[]

image.png
  • 代碼層面: Binder是Android中的一個(gè)類,它實(shí)現(xiàn)了IBinder接口
  • IPC角度: Binder是Android中的一種跨進(jìn)程通信方式.
  • 物理設(shè)備角度: Binder也可以認(rèn)為是一種虛擬的物理設(shè)備,設(shè)備驅(qū)動(dòng)是/dev/binder
  • Framework角度: Binder是ServiceManager連接各種Manager和相應(yīng)的ManagerService的橋梁.
  • Android應(yīng)用角度:Binder是客戶端和服務(wù)端進(jìn)行通信的媒介.

日常開發(fā)中,Binder主要用在Service包括AIDLMessenger. 而普通的Service中的Binder不涉及進(jìn)程間的通信,無法觸及Binder的核心. 而Messenger底層其實(shí)就是AIDL.所以我們利用AIDL來分析Binder的工作機(jī)制

創(chuàng)建AIDL實(shí)例

新建三個(gè)文件 Book.java Book.aidlIBookManager.aidl

首先創(chuàng)建一個(gè)Book類并實(shí)現(xiàn)Parcelable接口,然后在這個(gè)類所在的包上右鍵,如圖所示

image

如果名字不能為Book,可以先隨便寫一個(gè),創(chuàng)建之后修改. 然后按圖修改,

image

文件聲明完,我們只需要重新Make一下工程就可以.Build --> Rebuild Project或者M(jìn)ake Project

image.png

Make之后會(huì)在app -> build -> generated -> source -> aidl -> debug -> … 出現(xiàn)系統(tǒng)自動(dòng)生成好的java類. 我們需要對(duì)其進(jìn)行分析

看下圖了解一個(gè)大體結(jié)構(gòu)

image

上圖圈出了兩個(gè)部分,部分二應(yīng)該很清楚就是我們定義在aidl中的兩個(gè)抽象方法. 而部分一在圖上的內(nèi)部類Stub寫了說明. 我們自定義的兩個(gè)抽象方法,在內(nèi)部類中用了兩個(gè)整形int值來標(biāo)識(shí)兩個(gè)抽象方法,用在transact()中可以識(shí)別客戶端請(qǐng)求哪個(gè)方法.

這個(gè)繼承了IInterface的接口的核心實(shí)現(xiàn):就是內(nèi)部類Stub和Stub的內(nèi)部代理Proxy

先看一下內(nèi)部類的結(jié)構(gòu)圖:

image

說明直接放圖,寫在代碼上,在git上的根目錄的aidl的java類說明文件夾也有添加了注釋的類.

image

有兩點(diǎn)需要注意:

  1. 客戶端發(fā)起遠(yuǎn)程請(qǐng)求時(shí),當(dāng)前線程會(huì)被掛起直到服務(wù)器進(jìn)程返回?cái)?shù)據(jù),所以注意線程是否在意耗時(shí)
  2. 由于服務(wù)端Binder方法運(yùn)行在Binder線程池中,所以不管Binder方法是否耗時(shí)都應(yīng)該采用同步方式,因?yàn)橐呀?jīng)在一個(gè)線程中了

我們也可以手動(dòng)實(shí)現(xiàn)Binder類,這里不再細(xì)說,在git倉庫的項(xiàng)目中有一個(gè)manual包里面是關(guān)于手動(dòng)實(shí)現(xiàn)Binder的代碼.

其實(shí) 不管是手動(dòng)實(shí)現(xiàn)Binder也好,或者AIDL文件實(shí)現(xiàn)Binder也好. 其實(shí)兩者的工作原理都是一樣的, AIDL文件的存在意義是系統(tǒng)為我們提供了一種快速實(shí)現(xiàn)Binder的工具,僅此而已.

Binder生命狀態(tài)的監(jiān)聽

由于Binder是運(yùn)行在服務(wù)端,如果服務(wù)端進(jìn)程異常終止,那么我們到服務(wù)端的Binder連接也就斷裂(Binder死亡).就會(huì)導(dǎo)致調(diào)用失敗,所里系統(tǒng)提供了死亡代理的方法 就是當(dāng)Binder死亡時(shí),我們就會(huì)收到通知,這個(gè)時(shí)候就可以重新發(fā)起連接請(qǐng)求而恢復(fù)連接

首先創(chuàng)建監(jiān)聽的DeathRecipient對(duì)象

IBinder.DeathRecipient mDeat = new IBinder.DeathRecipient() {

            // 當(dāng)Binder死亡的時(shí)候,系統(tǒng)會(huì)回調(diào)binderDied()方法

            @Override

            public void binderDied() {

                if (mBookManager == null)

                    return ;

                //清除掉已經(jīng)無用的Binder連接

                mBookManager.asBinder().unlinkToDeath(mDeat,0);

                mBookManager == null;

                //TODO  進(jìn)行重新綁定遠(yuǎn)程服務(wù)

            }

        };

當(dāng)客戶端綁定遠(yuǎn)程服務(wù)成功的時(shí)候,給binder設(shè)置死亡代理

binder.linkToDeath(mDeat,0);

linkToDeath的第二個(gè)參數(shù)是個(gè)標(biāo)志位,直接設(shè)0即可. 另外也可以通過Binder的方法isBindAlive也可以判斷Binder是否死亡.

Android的幾種跨進(jìn)程的方式

使用Bundle

由于Bundle實(shí)現(xiàn)了Parcelable接口,所以在四大組件中的三大組件(Activity, Service, Receiver)都支持在Intent中傳遞Bundle.

所以如果在一個(gè)進(jìn)程中啟動(dòng)了另一個(gè)進(jìn)程的三大組件,就可以在Bundle中附加我們需要的信息通過Intent發(fā)送出去. 當(dāng)然傳遞的類型必須是能夠被序列化的, 例如基本數(shù)據(jù)類型,實(shí)現(xiàn)了Parcelable和Serializable接口的對(duì)象和一些Android支持的特殊對(duì)象.

使用文件共享

文件共享適合在對(duì)數(shù)據(jù)同步要求不高的進(jìn)程之間進(jìn)行通信,并且要妥善的處理并發(fā)讀寫的問題.

兩個(gè)進(jìn)程通過讀/寫同一個(gè)文件來交換數(shù)據(jù). 例如進(jìn)程A把數(shù)據(jù)寫入文件中,而進(jìn)程B從文件中讀取出來數(shù)據(jù).

Android是基于Linux系統(tǒng), 所以對(duì)于并發(fā)讀寫文件可以沒有限制的執(zhí)行. 這里不像Windows系統(tǒng),對(duì)于一個(gè)文件如果加了排斥鎖將會(huì)導(dǎo)致其他線程無法對(duì)其進(jìn)行訪問.

關(guān)于這部分的練習(xí), 在之前練習(xí)Binder的時(shí)候已經(jīng)練習(xí)過了. 代碼在項(xiàng)目中的MainActivity中

雖然序列化反序列達(dá)到的效果是可以恢復(fù)對(duì)象里面的屬性值,但是反序列每回都是一個(gè)新的對(duì)象.

SharePreferencess是Android提供的一個(gè)輕量級(jí)方案,通過鍵值對(duì)存儲(chǔ)數(shù)據(jù),底層采用XML文件來進(jìn)行存儲(chǔ). 存儲(chǔ)路徑/data/data/package name/shared_prefs目錄下. 也屬于文件的一種,但是由于系統(tǒng)對(duì)SP的讀寫存在一定的緩存策略,內(nèi)存中會(huì)有一份緩存,所以多進(jìn)程下,系統(tǒng)對(duì)它的讀寫也就變得不可靠.

使用Messenger

Messenger(信使). 不同的進(jìn)程中可以傳遞Message對(duì)象, 在Message中放入我們需要傳遞的數(shù)據(jù),就可實(shí)現(xiàn)進(jìn)程間傳遞. Messenger是一種輕量級(jí)的IPC方案,它的底層實(shí)現(xiàn)AIDL. 看一些構(gòu)造函數(shù)

public Messenger(Handler target) {

        mTarget = target.getIMessenger();

    }

public Messenger(IBinder target) {

   mTarget = IMessenger.Stub.asInterface(target);

}

無論是IMessenger還是Stub.asInterface. 可以明顯看出AIDL的痕跡.

因?yàn)镸essenger對(duì)AIDL進(jìn)行了封裝,使得在使用時(shí)更加簡(jiǎn)單,并且它的處理方式是一次處理一個(gè)請(qǐng)求,因此服務(wù)器端不用考慮線程同步因?yàn)榉?wù)端不存在并發(fā)執(zhí)行的情形.

具體實(shí)現(xiàn)Messenger

1.服務(wù)端

創(chuàng)建一個(gè)Service作為服務(wù)端來處理客戶端的請(qǐng)求, 同時(shí)創(chuàng)建一個(gè)Handle并通過它來創(chuàng)建一個(gè)Messenger對(duì)象,然后在Service的onBind()方法中返回這個(gè)Messenger對(duì)象底層的Binder.
最后這個(gè)組件在清單文件中聲明加上android:process="com.szysky.test"屬性.已達(dá)到模擬多進(jìn)程的場(chǎng)景

public class MessengerService extends Service {

    /**

     * 編寫一個(gè)類繼承Handler,并對(duì)客戶端發(fā)來的消息進(jìn)行處理操作進(jìn)行添加

     */

    private static  class MessengerHandler extends Handler{

        private static final String TAG = "MessengerHandler";

        @Override

        public void handleMessage(Message msg) {

            switch (msg.what){

                //客戶端發(fā)來的信息標(biāo)識(shí)

                case MessengerActivity.FROM_CLIENT:

                    Log.d(TAG, "handleMessage: receive msg form clinet-->" +msg.getData().getString("msg"));

                    //對(duì)客戶端進(jìn)行reply回答

                    // 1\. 通過接收到的到客戶端的Message對(duì)象獲取到Messenger信使

                    Messenger client = msg.replyTo;

                    // 2\. 創(chuàng)建一個(gè)信息Message對(duì)象,并把一些數(shù)據(jù)加入到這個(gè)對(duì)象中

                    Message replyMessage = Message.obtain(null, MessengerActivity.FROM_SERVICE);

                    Bundle bundle = new Bundle();

                    bundle.putString("reply", "我是服務(wù)端發(fā)送的消息,我已經(jīng)接收到你的消息了,你應(yīng)該在你的客戶端可以看到");

                    replyMessage.setData(bundle);

                    // 3\. 通過信使Messenger發(fā)送封裝好的Message信息

                    try {

                        client.send(replyMessage);

                    } catch (RemoteException e) {

                        e.printStackTrace();

                    }

                    break;

                default:

                    super.handleMessage(msg);

            }

        }

    }

    /**

     * 創(chuàng)建一個(gè)Messenger信使

     */

    private final Messenger mMessenger = new Messenger(new MessengerHandler());

    @Nullable

    @Override

    public IBinder onBind(Intent intent) {

        return mMessenger.getBinder();

    }

}

別忘了清單文件

<service android:name=".message.MessengerService"

            android:process="com.szysky.test"/>

2.客戶端

直接用一個(gè)activity作為客戶端, 首先綁定之前創(chuàng)建的服務(wù)端的Service, 綁定成功時(shí)通過ServiceConnection對(duì)象接收到服務(wù)端返回的IBinder,用IBinder對(duì)象創(chuàng)建一個(gè)Messenger. 通過這個(gè)Messenger就可以往服務(wù)端發(fā)送消息. 如果我們需要服務(wù)端也能夠回應(yīng)客戶端. 那么就要在客戶端同之前服務(wù)端一樣通過Handle創(chuàng)建一個(gè)Messenger對(duì)象, 并把這個(gè)Messenger在通過連接成功返回的IBinder創(chuàng)建的Message對(duì)象通過replyTo參數(shù)傳遞給服務(wù)器. 這個(gè)服務(wù)器就可以通過replyTo參數(shù)來回應(yīng)客戶端.


/**

* 聲明一個(gè)本進(jìn)程的信使 用來監(jiān)聽并處理服務(wù)端傳入的消息

*/

private Messenger mGetReplyMessenger =  new Messenger(new Handler(){

   @Override

   public void handleMessage(Message msg) {

       switch (msg.what){

           case FROM_SERVICE:

               Log.d(TAG, "handleMessage: 這里是客戶端:::"+msg.getData().getString("reply"));

               break;

           default:

               super.handleMessage(msg);

       }

   }

});

/**

* 創(chuàng)建一個(gè)服務(wù)監(jiān)聽連接對(duì)象 并在成功的時(shí)候給服務(wù)器發(fā)送一條消息

*/

private ServiceConnection mConnection = new ServiceConnection() {

   //綁定成功回調(diào)

   @Override

   public void onServiceConnected(ComponentName name, IBinder service) {

       // 利用服務(wù)端返回的binder對(duì)象創(chuàng)建Messenger并使用此對(duì)象想服務(wù)端發(fā)送消息

       Messenger mService = new Messenger(service);

       Message obtain = Message.obtain(null, FROM_CLIENT);

       Bundle bundle = new Bundle();

       bundle.putString("msg", "你好啊,  我是從客戶端來");

       obtain.setData(bundle);

       // 需要把接收服務(wù)端回復(fù)的Messenger通過Message的replyTo傳遞給服務(wù)端

       obtain.replyTo = mGetReplyMessenger;

       try {

           mService.send(obtain);

       } catch (RemoteException e) {

           e.printStackTrace();

       }

   }

   @Override

   public void onServiceDisconnected(ComponentName name) {

   }

};

//進(jìn)行遠(yuǎn)端服務(wù)的連接

 Intent intent = new Intent(MessengerActivity.this, MessengerService.class);

 bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

本例測(cè)試代碼在倉庫對(duì)應(yīng)項(xiàng)目的message包中

在使用Messenger進(jìn)行數(shù)據(jù)傳遞必須將數(shù)據(jù)放入到Message中. 而Messenger和Message都實(shí)現(xiàn)了序列化接口. 所以可以在進(jìn)程間通信.

Message的能使用的載體只有what, arg1, arg2, Bundle 以及replyTo. 這里有一個(gè)載體需要注意object,它在同一個(gè)進(jìn)程很實(shí)用,但是在版本2.2之前是不支持跨進(jìn)程的,雖然進(jìn)行了改進(jìn)之后,但是也只是支持系統(tǒng)提供實(shí)現(xiàn)的某些對(duì)象才可以. 所以使用的時(shí)候需要注意.

順便繪制了一個(gè)Messenger通信的流程圖, 可以對(duì)代碼的調(diào)用順序理解的更清楚.

image

使用AIDL

雖然Messenger使用方便, 但是要清楚它是以串行的方式處理客戶端發(fā)來的消息,如果有大量并發(fā)的請(qǐng)求. 或者需求是跨進(jìn)程調(diào)用服務(wù)端的方法時(shí). 就無法使用Messenger. 這個(gè)時(shí)候就該AIDL

對(duì)于使用AIDL的流程簡(jiǎn)單梳理一遍

服務(wù)端

服務(wù)端創(chuàng)建一個(gè)Service用來監(jiān)聽客戶端的連接請(qǐng)求, 然后創(chuàng)建一個(gè)AIDL文件,將暴露給客戶端的接口在這個(gè)AIDL文件中聲明,最后在Service中實(shí)現(xiàn)這個(gè)AIDL接口并在onBind()返回即可.

客戶端

綁定服務(wù)端的Service,綁定成功后,將服務(wù)端返回來的Binder對(duì)象轉(zhuǎn)成AIDL接口所屬的類型,接著就可以直接調(diào)用AIDL中的方法了.

AIDL中所支持的類型

  • 基本數(shù)據(jù)類型
  • String 和 CharSequence
  • List: 只支持ArrayList, 里面每個(gè)元素都必須能被AIDL支持
  • Map: 只支持HashMap, 里面的每個(gè)元素都必須被AIDL支持
  • Parcelable: 所有實(shí)現(xiàn)了Parcelable接口的對(duì)象
  • AIDL: 所有的AIDL接口本身也可以在AIDL文件中使用

這里請(qǐng)注意,上面支持類型中Parcelable和AIDL比較特殊,自定義的Parcelable對(duì)象和AIDL對(duì)象必須要顯示的import引入, 這是AIDL的規(guī)范需要遵循, 如下Book類


import com.szysky.note.androiddevseek_02.aidl.Book;//必須Book的全限定名

interface IBookManager {

   List<Book> getBookList();

   void addBook(in Book book);  //這里標(biāo)明輸入型

}

如果用到了自定義對(duì)象實(shí)現(xiàn)了Parcelable那么就需要?jiǎng)?chuàng)建一個(gè)同名的aidl文件

package com.szysky.note.androiddevseek_02.aidl;

parcelable Book;

AIDL中除了基本數(shù)據(jù)類型外,其他類型的參數(shù)必須標(biāo)上方向out , in, inout.分別表示輸入,輸出,輸入輸出型. 按需而定可以節(jié)省不必要的操作在底層實(shí)現(xiàn)的開銷. 最后一點(diǎn)AIDL接口中只支持方法,不支持聲明靜態(tài)常量.

在服務(wù)端用了CopyOnWriteArrayList數(shù)組來保存所有書籍. 這個(gè)集合的特性是支持并發(fā)讀寫. 在說Binder的時(shí)候提到過, AIDL方法是在服務(wù)端Binder線程池中執(zhí)行的, 所以當(dāng)多個(gè)客戶端同時(shí)連接,會(huì)存在多線程并發(fā)的問題. 所以使用CopyOnWriteArrayList集合可以進(jìn)行自動(dòng)的線程同步.與之相似的還有ConcurrentHashMap這個(gè)在LRU機(jī)制中使用到過

這里有知識(shí)點(diǎn). 之前說過AIDL中能過使用的只有ArrayList. 而CopyOnWriteArrayList也并不是ArrayList的子類. 其實(shí)AIDL所支持的是抽象的List, 而List只是一個(gè)接口, 雖然服務(wù)端返回的是CopyOnWriteArrayList,但是在Binder中會(huì)按照List的規(guī)范去訪問數(shù)據(jù)并最終形成一個(gè)新的ArrayList傳遞給客戶端.

看下面的log圖在客戶端接收返回的CopyOnWriteArrayList實(shí)際上是ArrayList類型

image

git倉庫的代碼的aidl包中 最后保存的是實(shí)現(xiàn)了客戶端和服務(wù)端的觀察者模式(可以通過git版本切換之前代碼), 通過客戶端注冊(cè)監(jiān)聽接口,
在服務(wù)端每當(dāng)有新書來的時(shí)候,通知已經(jīng)注冊(cè)了的客戶端.

需要注意的幾點(diǎn)

線程問題

當(dāng)有新書的時(shí)候,服務(wù)端回調(diào)的是客戶端實(shí)現(xiàn)的接口里面的方法. 這個(gè)方法實(shí)際是在客戶端的線程池中執(zhí)行的. 所以要處理處理UI的問題, 解決方案可以創(chuàng)建一個(gè)Handler,將其切換到客戶端的主線程中

private INewBookArrivedListener mNewBookListener = new INewBookArrivedListener.Stub() {

        @Override

        public void onNewBookArrived(Book book) throws RemoteException {

            // 如果有新書 那么此方法會(huì)被回調(diào),  并且由于調(diào)用處服務(wù)端的Binder線程池, 所以給主線程的Handler發(fā)送消息,以切換線程

            mhandler.obtainMessage(NEW_BOOK_ARRIVED, book).sendToTarget();

        }

    };

對(duì)象不一致,導(dǎo)致接觸綁定失敗

服務(wù)端不能再用CopyOnWriteArrayList來記錄綁定過的客戶端. 因?yàn)檫@里一定要清楚對(duì)象是不能跨進(jìn)程的當(dāng)我們客戶端注冊(cè)監(jiān)聽傳入一個(gè)監(jiān)聽對(duì)象到服務(wù)端, 在解綁的時(shí)候再次傳入一個(gè)進(jìn)行判斷與注冊(cè)時(shí)相同的對(duì)象時(shí)刪除達(dá)到解除綁定效果時(shí)是無效的. 因?yàn)榉?wù)端在注冊(cè)和解綁的時(shí)候是兩個(gè)反序列化的對(duì)象完全不一致.

RemoteCallbackList是系統(tǒng)專門提供的用于刪除跨進(jìn)程listener的接口. 接收的是一個(gè)泛型,支持管理任意的AIDL接口,從聲明就可以看出, 因?yàn)锳IDL接口都繼承IInterface

內(nèi)部實(shí)現(xiàn)是一個(gè)Map結(jié)構(gòu) key是IBinder類型, value是Callback類型.


ArrayMap<IBinder, Callback> mCallbacks= new ArrayMap<IBinder, Callback>();

IBinder key = listener.asBinder();

Callback value = new Callback(listener, cookie);    //這里的Callback封裝了真正的監(jiān)聽對(duì)象

不管是注冊(cè)還是解注冊(cè),多進(jìn)程到服務(wù)端都會(huì)生成不同的對(duì)象. 但是這些不同的對(duì)象有一個(gè)共同點(diǎn), 底層的Binder對(duì)象是同一個(gè), 利用這個(gè)特性可解決上面的問題.

RemoteCallbackList 當(dāng)客戶端進(jìn)程終止后, 它能夠自動(dòng)移出客戶端所注冊(cè)的listener. 并且內(nèi)部實(shí)現(xiàn)了線程同步的功能, 所以在注冊(cè)和解注冊(cè)的時(shí)候不需要做額外的線程工作.

在使用的使用,雖然名字有List但是他并不是一個(gè)List我們要遍歷的通知監(jiān)聽者的時(shí)候,要使用bigenBroadcastfinishBroadcase成對(duì)出現(xiàn).

//遍歷集合  去調(diào)用客戶端方法

int N = mListeners.beginBroadcast();

for (int i = 0; i<N; i++){

  INewBookArrivedListener listener = mListeners.getBroadcastItem(i);

  if (listener != null){

      listener.onNewBookArrived(newBook);

  }

}

mListeners.finishBroadcast();

當(dāng)客戶端調(diào)用遠(yuǎn)程服務(wù)的方法,被調(diào)用的方法運(yùn)行在服務(wù)端的Binder線程池中,同時(shí)客戶端會(huì)被掛起, 所以你如果在主線程(客戶端的onServiceConnectedonServiceDisconnected就是UI線程)調(diào)用服務(wù)端的耗時(shí)方法, 你多點(diǎn)幾次就很容易出現(xiàn)ANR. 比方說在服務(wù)端的getBookList()睡上十秒,可以復(fù)現(xiàn)ANR.

監(jiān)聽死亡狀態(tài): 在整理Binder的時(shí)候有說了一種DeathRecipient的方式,下兩種都可以

  • onServiceDisconnected() UI線程被回調(diào)
  • binderDied() 在客戶端的Binder線程池中被回調(diào)

還記得在綁定的時(shí)候bindService(intent,mConnection, Context.BIND_AUTO_CREATE);其中參數(shù)3如果設(shè)置這個(gè)模式, 當(dāng)服務(wù)或線程死亡,還會(huì)重新啟動(dòng)的.

權(quán)限驗(yàn)證

  1. 在服務(wù)端的onBinder()回調(diào)中判斷權(quán)限.
  2. 在服務(wù)端實(shí)現(xiàn)的AIDL接口中的onTransact()進(jìn)行包名判斷或者權(quán)限

第一種:

先清單文件中注冊(cè)一個(gè)自定義的權(quán)限

<permission

        android:name="com.szysky.permission.ACCESS_BOOK_SERVICE"

        android:protectionLevel="normal"/>

在清單文件中添加這個(gè)權(quán)限的使用資格

<uses-permission android:name="com.szysky.permission.ACCESS_BOOK_SERVICE"/>

然后在onBinder()進(jìn)行判斷,如果沒有那么就返回null, 這樣客戶端是無法綁定服務(wù)的

public IBinder onBind(Intent intent) {

    //做一下權(quán)限的驗(yàn)證  在清單文件中聲明了一個(gè),  并添加了使用權(quán)限

    int check = checkCallingOrSelfPermission("com.szysky.permission.ACCESS_BOOK_SERVICE");

    if (check == PackageManager.PERMISSION_DENIED){

      return  null;

    }

    return mBinder;

}

第二種

可以判斷客戶端的包名是否滿足我們的需求,這里用com.szysky開頭為例. 如果不符合方法返回false.那么調(diào)用服務(wù)的方法也會(huì)失效


@Override

public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {

  String packageName = null;

  String[] packagesForUid = getPackageManager().getPackagesForUid(getCallingUid());

//下面是獲得客戶端的包名

  if (packagesForUid != null && packagesForUid.length >0){

      packageName = packagesForUid[0];

  }

  Log.e(TAG, "onTransact: -----------------------------" + packageName);

  if (!packageName.startsWith("com.szysky")){

      return false;

  }

  return super.onTransact(code, data, reply, flags);

}

使用ContentProvider"

相關(guān)代碼在倉庫項(xiàng)目的java路徑下的provider包中

ContentProvider是Android提供專門用于不用應(yīng)用進(jìn)行數(shù)據(jù)共享的方式. 它的底層同樣也是Binder. 因?yàn)橄到y(tǒng)封裝, 所以它的使用比起AIDL要簡(jiǎn)單很多.

要實(shí)現(xiàn)一個(gè)內(nèi)容提供者, 只需要寫一個(gè)類繼承ContentProvider,并復(fù)寫六個(gè)抽象方法. 其中有四個(gè)是CURD操作方法. 一個(gè)onCreate()用來做初始化. 一個(gè)getType()用來返回一個(gè)Uri請(qǐng)求所對(duì)應(yīng)的MIME類型,比如圖片還是視頻等. 如果我們不關(guān)心那么可是直接返回NULL或者*/*.

這六個(gè)方法根據(jù)Binder工作原理,都是運(yùn)行在ContentProvider的進(jìn)程中. 除了onCreate()是被系統(tǒng)回調(diào)運(yùn)行在主線程, 其余的都在Binder的線程池中.

主要存儲(chǔ)方式是表格的形式, 也可以支持文件格式,例如圖片視頻, 可以返回這類文件的句柄給外界來訪問ContentProvider中的文件信息.

<provider

           android:authorities="com.szysky.note.androiddevseek_02.provider"

           android:name=".provider.BookProvider"

           android:permission="com.szysky.PROVIDER"/>

  • authorities: 后面的值是指定這個(gè)ContentProvider的唯一標(biāo)識(shí).
  • permission: 添加一個(gè)權(quán)限認(rèn)證, 對(duì)于訪問者必須添加了這個(gè)使用權(quán)限的聲明.

查詢的時(shí)候通過Uri對(duì)authorities聲明值得解析就可以找到對(duì)應(yīng)的ContentProvider

Uri uri = Uri.parse("content://com.szysky.note.androiddevseek_02.provider");

getContentResolver().query(uri, null, null, null, null);

為了后續(xù)操作, 這里利用SQLiteOpenHelper來管理數(shù)據(jù)庫,并創(chuàng)建兩個(gè)表userbook,代碼在倉庫有,這里不寫實(shí)現(xiàn)過程.

由于有兩個(gè)表支持被訪問, 所以應(yīng)該為每一個(gè)不同的表設(shè)定單獨(dú)的Uri和Uri_Code 并將其關(guān)聯(lián). 這樣外界訪問的時(shí)候可以根據(jù)Uri得到Uri_Code. 也就在ContentProvider知道要處理的具體事件.

在新建的ContentProvider類中進(jìn)行關(guān)聯(lián), 如下


private static final String AUTHORITY = "com.szysky.note.androiddevseek_02.provider";

    /**

     * 指定兩個(gè)操作的Uri

     */

    private static final Uri BOOK_CONTENT_URI = Uri.parse("content://" +AUTHORITY + "/book");

    private static final Uri USER_CONTENT_URI = Uri.parse("content://" +AUTHORITY + "/user");

    /**

     * 創(chuàng)建Uri對(duì)應(yīng)的Uri_Code

     */

    private static final int BOOK_URI_CODE = 1;

    private static final int USER_URI_CODE = 2;

    /**

     * 創(chuàng)建一個(gè)管理Uri和Uri_Code的對(duì)象

     */

    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {

        //進(jìn)行關(guān)聯(lián)

        sUriMatcher.addURI(AUTHORITY, "book",BOOK_URI_CODE);

        sUriMatcher.addURI(AUTHORITY, "user",USER_URI_CODE);

    }

針對(duì)query方法進(jìn)行演示,其他三個(gè)類似,代碼有全部實(shí)現(xiàn)的例子, 在自定義Provider文件中.

/**

* 通過自動(dòng)以的Uri來判斷對(duì)應(yīng)的數(shù)據(jù)庫表名

*/

private String getTableName(Uri uri){

   String tableName = null;

   switch (sUriMatcher.match(uri)){

       case BOOK_URI_CODE:

           tableName = DbHelper.BOOK_TABLE_NAME;

           break;

       case USER_URI_CODE:

           tableName = DbHelper.USER_TABLE_NAME;

           break;

       default:break;

   }

   return tableName;

}

/**

 *   在query中, 獲取到Uri傳入要查詢的具體表名, 使用SQLiteOpenHelper來進(jìn)行query的查詢,并把結(jié)果返回

 */

public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {

        //獲取表名

        String tableName = getTableName(uri);

        if (tableName == null)

            throw new IllegalArgumentException("不被支持的Uri參數(shù)-->"+uri );

        return mDb.query(tableName, projection, selection, selectionArgs, null, null, sortOrder,null);

    }

如果需要監(jiān)聽Provider內(nèi)容的變化, 那么可以在Provider中update, delete, insert. 中操作完數(shù)據(jù)庫之后. 使用getContentResolver().notifyChange(uri,null);來通知外界當(dāng)前ContentProvider中數(shù)據(jù)已經(jīng)發(fā)生改變. 而外部要想觀察其變化. 使用ContentResolverrigisterContentObserver方法來注冊(cè)觀察者.

線程安全問題

如果只有一個(gè)SQLiteDataBase對(duì)象被使用, 那么增刪改查不會(huì)出現(xiàn)線程安全問題, 因?yàn)槠鋬?nèi)部對(duì)數(shù)據(jù)庫的操作是有同步處理. 但是如果多個(gè)SQLiteDataBase對(duì)象來操作數(shù)據(jù)庫就無法保證其線程安全. 這個(gè)時(shí)候就要注意了.

使用Socket

Socket也稱為套接字. 是網(wǎng)絡(luò)通信中的概念, 它分為流式套接字和用戶數(shù)據(jù)包套接字兩種. 分別對(duì)應(yīng)于網(wǎng)絡(luò)的傳輸控制層中TCP和UDP協(xié)議.

使用Socket通信, 需要在清單文件添加權(quán)限的申請(qǐng)

<!--Socket通信額外需要的線程-->

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<uses-permission android:name="android.permission.INTERNET"/>

服務(wù)端 需要啟動(dòng)服務(wù), 并且在線程中建立TCP服務(wù), 然后監(jiān)聽一個(gè)端口. 當(dāng)客戶端建立連接的時(shí)候就會(huì)生成一個(gè)Socket流. 可以保持后續(xù)的持續(xù)通信. 每一個(gè)客戶端都對(duì)應(yīng)一個(gè)Socket. 如果客戶端斷開連接. 服務(wù)端需要做好相應(yīng)的Socket流關(guān)閉并結(jié)束通話線程. 可以通過在客戶端斷開的時(shí)候服務(wù)端的接收字節(jié)流會(huì)是null來判斷連接是否還存活.

首先需要開啟一個(gè)新線程, Runnable接口這樣實(shí)現(xiàn), 以下是偽代碼, 沒有捕捉異常

// 監(jiān)聽 3333 端口

 ServerSocket serverSocket = new ServerSocket(3333);

// 判斷服務(wù)是否斷開 沒有斷開就繼續(xù)監(jiān)聽端口 

  while (!mIsServiceDestoryed) {

        //這個(gè)是阻塞方法, 當(dāng)有新的客戶端連接,才會(huì)返回Socket值

        final Socket accept = serverSocket.accept();

        // 有了新的客戶端 那就需要?jiǎng)?chuàng)建一個(gè)新的線程去維護(hù)

        new Thread() {

            public void run() {

                //  這里做對(duì)一個(gè)Socket的具體操作

                responseClient(accept);

            }}.start();

  }

private void responseClient(Socket client) throws IOException {

        //接收客戶端消息

        BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));

        //發(fā)送到客戶端 , 設(shè)置true參數(shù)就不需要手動(dòng)的刷新輸出流

        PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())),true);

        out.println("歡迎來到直播間");

        //判斷服務(wù)標(biāo)志是否銷毀, 沒有銷毀那么就一直監(jiān)聽此鏈接的Socket流

        while (!mIsServiceDestoryed) {

            String str = in.readLine(); //這是一個(gè)阻塞方法

            //判斷如果取出來的是null,那么就說明連接已經(jīng)斷開

            if (str == null)

                break;

            // 對(duì)客戶端進(jìn)行回復(fù)

            out.println("我是回復(fù)消息");

        }

        //準(zhǔn)備關(guān)閉一系列的流

        ......

    }

最后就是要在onDestroy()中把循環(huán)中判斷服務(wù)存活的標(biāo)識(shí)置為false, 讓開啟的線程都能自動(dòng)走完關(guān)閉.

客戶端

onCreate()先startService開啟TCP的服務(wù), 然后開啟一個(gè)線程準(zhǔn)備連接Socket. 可以加上失敗重連的的機(jī)制. 只要獲取到了Socket, 就和之前服務(wù)端一樣獲取輸入輸出流進(jìn)行對(duì)應(yīng)的操作.

貼出客戶端的核心代碼, Runnable接口實(shí)現(xiàn)的 ,同樣是偽代碼

Socket socket = null;

// 試圖連接服務(wù)器, 如果失敗休眠一秒重試

while(socket == null){

  try {

      // 如果可以連接 3333 端口成功那么socket就不為null, 此循環(huán)也就結(jié)束

      socket = new Socket("localhost", 3333);

      mClientSocket = socket;

      // 獲得輸出流, 并設(shè)置true,自動(dòng)刷新輸出流里面的內(nèi)容

      mPrintWrite = new PrintWriter(new BufferedWriter(new OutputStreamWriter(mClientSocket.getOutputStream())),true );

  } catch (IOException e) {

      SystemClock.sleep(1000);

      e.printStackTrace();

  }

}

//準(zhǔn)備接收服務(wù)器的消息.

 BufferedReader in = new BufferedReader(new InputStreamReader(mClientSocket.getInputStream()));

  //獲得了socket流的讀入段  只要activity不關(guān)閉一直循環(huán)讀

  while(!SocketActivity.this.isFinishing()){

      // readLine()同樣也是阻塞方法

      String strLine = in.readLine();

      if (strLine != null){

          //TODO 獲取到了服務(wù)端發(fā)來的數(shù)據(jù), 做一些事情

          //....

      }

  }

//走到這里 說明界面已經(jīng)不存在, 進(jìn)行掃尾動(dòng)作

in.close();

mPrintWrite.close();

socket.close();

demo圖例Socket相關(guān)代碼存在倉庫的socket包中

image

各種IPC的差異以及選擇

名稱 優(yōu)點(diǎn) 缺點(diǎn) 使用場(chǎng)景
Bundle 簡(jiǎn)單易用 只能傳輸Bundle支持的數(shù)據(jù)類型 四大組件間的進(jìn)程間通信
文件共享 簡(jiǎn)單易用 不適合高并發(fā)場(chǎng)景,并且無法做到進(jìn)程間的即時(shí)通 無法并發(fā)訪問情形, 交換簡(jiǎn)單的數(shù)據(jù)實(shí)時(shí)性不高的場(chǎng)景
AIDL 功能強(qiáng)大 使用稍復(fù)雜,需要處理好線程同步 一對(duì)多通信且有RPC需求
ContentProvider 在數(shù)據(jù)源訪問方面功能強(qiáng)大,支持一對(duì)多并發(fā)數(shù)據(jù)共享 可以理解為受約束的AIDL,主要提供數(shù)據(jù)源的CRUD操作 一對(duì)多的進(jìn)程間的數(shù)據(jù)共享
Messenger 功能一般, 支持一對(duì)多串行通信,支持實(shí)時(shí)通信 不能很好處理高并發(fā),不支持RPC,數(shù)據(jù)通過Message進(jìn)行傳輸, 因此只能傳輸Bundle支持的數(shù)據(jù)類型 低并發(fā)的一對(duì)多即時(shí)通信,無RPC需求,或者無需要返回結(jié)果的RPC需求
Socket 功能強(qiáng)大,可以通過網(wǎng)絡(luò)傳輸字節(jié)流,支持一對(duì)多并發(fā)實(shí)時(shí)通信 實(shí)現(xiàn)細(xì)節(jié)稍微有點(diǎn)繁瑣,不支持直接的 網(wǎng)絡(luò)數(shù)據(jù)交換

參看文章

《Android 開發(fā)藝術(shù)探索》書集
《Android 開發(fā)藝術(shù)探索》 02-IPC機(jī)制
https://github.com/feiwodev/AndroidDevelopmentArt

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌意推,老刑警劉巖竿报,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件光涂,死亡現(xiàn)場(chǎng)離奇詭異铺董,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)外潜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挠唆,“玉大人橡卤,你說我怎么就攤上這事∷鸢幔” “怎么了碧库?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)巧勤。 經(jīng)常有香客問我嵌灰,道長(zhǎng),這世上最難降的妖魔是什么颅悉? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任沽瞭,我火速辦了婚禮,結(jié)果婚禮上剩瓶,老公的妹妹穿的比我還像新娘驹溃。我一直安慰自己城丧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布豌鹤。 她就那樣靜靜地躺著亡哄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪布疙。 梳的紋絲不亂的頭發(fā)上蚊惯,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音灵临,去河邊找鬼截型。 笑死,一個(gè)胖子當(dāng)著我的面吹牛儒溉,可吹牛的內(nèi)容都是我干的宦焦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼顿涣,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼赶诊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起园骆,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤舔痪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后锌唾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锄码,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年晌涕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滋捶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡余黎,死狀恐怖重窟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情惧财,我是刑警寧澤巡扇,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站垮衷,受9級(jí)特大地震影響厅翔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜搀突,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一刀闷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦甸昏、人聲如沸顽分。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卒蘸。三九已至,卻和暖如春花墩,著一層夾襖步出監(jiān)牢的瞬間悬秉,已是汗流浹背澄步。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工冰蘑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人村缸。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓祠肥,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親梯皿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子仇箱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345