IPC機(jī)制

1运提、IPC簡(jiǎn)介

IPC是Inter-Process Communication的縮寫俯在,進(jìn)程間通信或者跨進(jìn)程通信粒督,是指兩進(jìn)程之間進(jìn)行數(shù)據(jù)交換的過程杆烁。在Android中牙丽,UI是主線程,其可以操作界面元素兔魂,但耗時(shí)操作放在UI線程處理會(huì)導(dǎo)致ANR錯(cuò)誤烤芦。

2、多進(jìn)程模式
2.1入热、開啟多進(jìn)程

通過四大組件制定android:process屬性拍棕,可以開啟多進(jìn)程模式,例如:

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

    <activity android:name=".SecondActivity"
        android:process=":test"/>
    <activity android:name=".ThirdActivity"
        android:process="com.fomin.ipc.test"/>
</application>

上面分別為SecondActivity和ThirdActivity增加了process屬性勺良,可以看出绰播,他們的命名是不一樣的,首先尚困,“:”的含義是指要在當(dāng)前的進(jìn)程名前面附加上當(dāng)前的包名蠢箩,簡(jiǎn)寫的寫法;其次事甜,“:”開頭的進(jìn)程屬于當(dāng)前應(yīng)用的私有名稱谬泌,其它應(yīng)用的組件不可以和它跑在同一個(gè)進(jìn)程中,而進(jìn)程名不以":"開頭的進(jìn)程屬于全局進(jìn)程逻谦,可以通過ShareUID和它跑在同一個(gè)進(jìn)程中(要求簽名相同既可以)掌实。

2.2、運(yùn)行機(jī)制

Android為每個(gè)應(yīng)用都分配了獨(dú)立的虛擬機(jī)邦马,也可以說為每個(gè)進(jìn)程分配獨(dú)立虛擬機(jī)贱鼻,不同的虛擬機(jī)在內(nèi)存中分配上有不同的地址空間,導(dǎo)致不同的虛擬機(jī)中訪問一個(gè)類的對(duì)象會(huì)產(chǎn)生多個(gè)副本滋将。

一般來說邻悬,使用多進(jìn)程是產(chǎn)生下面的問題:

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

Binder是Android中的一個(gè)類随闽,實(shí)現(xiàn)了IBinder接口父丰,從IPC角度來說,它是Android的一種跨進(jìn)程通信方式掘宪。主要在Service中蛾扇,包括AIDL和Messenger攘烛,其中普通Service中的Binder不涉及進(jìn)程間通信的。

public interface IBookManager extends android.os.IInterface {
    /**
     * Local-side IPC implementation stub class.
     */
    public static abstract class Stub extends android.os.Binder implements com.fomin.ipc.IBookManager {
        private static final java.lang.String DESCRIPTOR = "com.fomin.ipc.IBookManager";

        /**
         * Construct the stub at attach it to the interface.
         */
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        /**
         * Cast an IBinder object into an com.fomin.ipc.IBookManager interface,
         * generating a proxy if needed.
         */
        public static com.fomin.ipc.IBookManager asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.fomin.ipc.IBookManager))) {
                return ((com.fomin.ipc.IBookManager) iin);
            }
            return new com.fomin.ipc.IBookManager.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_getBookList: {
                    data.enforceInterface(DESCRIPTOR);
                    java.util.List<com.fomin.ipc.Book> _result = this.getBookList();
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
                case TRANSACTION_addBook: {
                    data.enforceInterface(DESCRIPTOR);
                    com.fomin.ipc.Book _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.fomin.ipc.Book.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.addBook(_arg0);
                    reply.writeNoException();
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        private static class Proxy implements com.fomin.ipc.IBookManager {
            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 java.util.List<com.fomin.ipc.Book> getBookList() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<com.fomin.ipc.Book> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.createTypedArrayList(com.fomin.ipc.Book.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public void addBook(com.fomin.ipc.Book book) 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 ((book != null)) {
                        _data.writeInt(1);
                        book.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }

        static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

    public java.util.List<com.fomin.ipc.Book> getBookList() throws android.os.RemoteException;

    public void addBook(com.fomin.ipc.Book book) throws android.os.RemoteException;
}

上面的類是通過AIDL自動(dòng)生成的屁桑,介紹下相關(guān)的方法含義医寿。

  • DESCRIPTOR:Binder的唯一標(biāo)識(shí),一般用在當(dāng)前Binder的類名表示蘑斧。
  • asInterface(android.os.IBinder obj):用于將服務(wù)端的Binder對(duì)象轉(zhuǎn)換成客戶端的AIDL接口類型對(duì)象靖秩,轉(zhuǎn)換過程是區(qū)分進(jìn)程的,如果客戶端和服務(wù)端位于同一進(jìn)程竖瘾,那么此方法返回的就是服務(wù)端的Stub對(duì)象本身沟突,否則返回的系統(tǒng)封裝后的Stub.proxy對(duì)象。
  • asBinder:返回當(dāng)前的Binder對(duì)象
  • onTransact:方法運(yùn)行在服務(wù)端中的Binder線程池中捕传,當(dāng)客戶端發(fā)起跨進(jìn)程請(qǐng)求時(shí)惠拭,遠(yuǎn)程請(qǐng)求會(huì)通過系統(tǒng)底層封裝后交由此方法來處理。
  • Proxy#getBookList:自定義的API
  • Proxy#addBook:自定義的API
Binder的工作機(jī)制
4庸论、Android中的IPC方式
4.1职辅、使用文件共享

共享文件是一種不錯(cuò)的進(jìn)程間通信方式,兩個(gè)進(jìn)程通過讀/寫一個(gè)文件來交換數(shù)據(jù)聂示。在Android中域携,SharedPreferences是輕量級(jí)的存儲(chǔ)方案,由于系統(tǒng)對(duì)它的讀/寫有一定的緩存策略鱼喉,即在內(nèi)存中會(huì)有一份SharedPreferences的文件緩存秀鞭,因此在多線程下,系統(tǒng)對(duì)讀/寫變得不可靠扛禽,面對(duì)高并發(fā)的讀/寫數(shù)據(jù)锋边,有很大的幾率會(huì)丟失數(shù)據(jù),不建議進(jìn)程間通信使用SharedPreferences编曼。

進(jìn)程一寫數(shù)據(jù):
fun saveDataToFile() {
    Thread(Runnable {
        var book: Book = Book(1, "Android")
        val dir = File(path)
        if (!dir.exists()) {
            dir.mkdirs()
        }
        val cachedFile = File(cachePath)
        var stream: ObjectOutputStream? = null
        try {
            stream = ObjectOutputStream(FileOutputStream(cachedFile))
            stream.writeObject(book)
        } catch (e: IOException) {
            e.printStackTrace();
        } finally {
            stream?.close();
        }
    }).start()
}
進(jìn)程二讀取數(shù)據(jù):
fun readFromFile() {
    Thread(Runnable {
        var book: Book? = null
        val cachedFile = File(cachePath)
        if (cachedFile.exists()) {
            var stream: ObjectInputStream? = null
            try {
                stream = ObjectInputStream(FileInputStream(cachedFile))
                book = stream.readObject() as Book
            } catch (e: IOException) {
                e.printStackTrace();
            } finally {
                stream?.close();
            }
        }
    }).start()
}
4.2豆巨、使用Messager

Messager中進(jìn)行的數(shù)據(jù)傳遞必須將數(shù)據(jù)傳入Message中,而Messager和Message都實(shí)現(xiàn)了Parcelable接口掐场。在Message中有what往扔、arg1、arg2刻肄、Bundle和replyTo瓤球,而另外一個(gè)object字段只能使用系統(tǒng)提供的Parcelable對(duì)象才能傳輸融欧,自定義的Parcelable對(duì)象是無法通過傳輸?shù)拿羝O旅嫱ㄟ^例子來看看它是怎樣使用的。

class MessageService : Service() {

    private val TAG = "MessageService"

    private val mMessenger = Messenger(MessageHandler())

    private inner class MessageHandler : Handler() {
        override fun handleMessage(msg: Message) {
            when (msg.what) {
                1001 -> {
                    Log.d(TAG, msg.data.getString("key"))
                    val client = msg.replyTo
                    val reply = Message.obtain(null, 1002)
                    val bundle = Bundle()
                    bundle.putString("key", "消息收到")
                    reply.data = bundle
                    try {
                        client.send(reply)
                    } catch (e: RemoteException) {
                        e.printStackTrace()
                    }
                }
                else -> super.handleMessage(msg)
            }
        }
    }

override fun onBind(intent: Intent): IBinder? {
        return mMessenger.binder
    }
}

注冊(cè)service

<service android:name=".MessageService"
    android:process=":process1"/>

客戶端使用

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val intent = Intent(this, MessageService::class.java)
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
}
private val TAG = "MainActivity"
private var mService: Messenger? = null
private val mMessenger = Messenger(MessengerHandler())

private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName, service: IBinder) {
        mService = Messenger(service)
        val msg = Message.obtain(null, 1001)
        val data = Bundle()
        data.putString("msg", "客戶端發(fā)送消息")
        msg.data = data
        msg.replyTo = mMessenger
        try {
            mService?.send(msg)
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun onServiceDisconnected(name: ComponentName) {

    }
}

private inner class MessengerHandler : Handler() {
    override fun handleMessage(msg: Message) {
        when (msg.what) {
            1002 -> Log.i(TAG, msg.data.getString("key"))
            else -> super.handleMessage(msg)
        }
    }
}
4.3噪馏、使用AIDL

AIDL首先需要?jiǎng)?chuàng)建一個(gè)Service監(jiān)聽客戶端的連接請(qǐng)求麦到;再次需要?jiǎng)?chuàng)建客戶端綁定Service绿饵。下面來看看AIDL接口創(chuàng)建(IBookManager.aidl和Book.aidl)

//IBookManager.aidl
import com.fomin.ipc.Book;
// Declare any non-default types here with import statements

interface IBookManager {
    List<Book> getBookList();
    void addBook(in Book book);
}

// IBook.aidl
packag// IBook.aidl
package com.fomin.ipc;

// Declare any non-default types here with import statements

parcelable Book;

支持以下數(shù)據(jù)類型:

  • 基本數(shù)據(jù)類型(int、long瓶颠、char拟赊、boolean、double等)
  • String和CharSequence
  • List:只支持ArraryList
  • Map:只支持HashMap
  • Parcelable:所有實(shí)現(xiàn)Parcelable的對(duì)象
  • AIDL:所有AIDL本身也可以使用

創(chuàng)建Service:

class BookManagerService : Service() {

    private val TAG = "BookManagerService"
    private val mBookList = CopyOnWriteArrayList<Book>()

    private val mBinder = object : IBookManager.Stub() {
        @Throws(RemoteException::class)
        override fun getBookList(): List<Book>? {
            return mBookList
        }

        @Throws(RemoteException::class)
        override fun addBook(book: Book) {
            mBookList.add(book)
        }
    }
    override fun onBind(intent: Intent?): IBinder {
        return mBinder
    }
}

注冊(cè)service

<service android:name=".BookManagerService"
    android:process=":process2"/>

創(chuàng)建客戶端

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val intent = Intent(this, BookManagerService::class.java)
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
}

override fun onDestroy() {
    unbindService(mConnection)
    super.onDestroy()
}

private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName, service: IBinder) {
        val bookManager = IBookManager.Stub.asInterface(service)
        try {
           val bookList = bookManager.bookList
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun onServiceDisconnected(name: ComponentName) {

    }
}

在這有個(gè)問題粹淋,UI線程執(zhí)行耗時(shí)的操作會(huì)導(dǎo)致ANR吸祟,所以客戶端在getBookList的時(shí)候需要在非UI線程執(zhí)行

4.4、使用ContentProvider

ContentProvider底層也是實(shí)現(xiàn)Binder桃移,主要以表格形式來組織數(shù)據(jù)屋匕,可以包含多個(gè)表,支持文件數(shù)據(jù)(圖片借杰、視頻等)过吻。需要注意的是query、update蔗衡、insert纤虽、delete四大方法是存在多線程并發(fā)訪問的,因此內(nèi)部需要做好線程同步绞惦。

//數(shù)據(jù)庫(kù)操作工具
class DbHelper(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {

    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL("")//創(chuàng)建表
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {

    }

    companion object {
        private val DB_NAME = "book.db"
        private val DB_VERSION = 1
    }
}
//數(shù)據(jù)操作
class BookProvider : ContentProvider() {

    private var mDb: SQLiteDatabase? = null

    override fun onCreate(): Boolean {
        mDb = DbHelper(getContext()).writableDatabase
        return true
    }

    override fun insert(uri: Uri?, values: ContentValues?): Uri {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun query(uri: Uri?, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun delete(uri: Uri?, selection: String?, selectionArgs: Array<out String>?): Int {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun getType(uri: Uri?): String {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
}

注冊(cè)ContentProvider逼纸,android:authorities必須是唯一的,外界想使用BookProvider必須聲明com.fomin.PROVIDER這權(quán)限

<provider
    android:authorities="com.fomin.ipc"
    android:name=".BookProvider"
    android:permission="com.fomin.PROVIDER"
    android:process=":provider"/>

客戶端使用:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val bookUri = Uri.parse("content://com.fomin.ipc/book")
    val contentValues = ContentValues()
    contentValues.put("id", 1)
    contentValues.put("name", "Android")
    contentResolver.insert(bookUri, contentValues)
}
4.5翩隧、使用socket

使用socket通信樊展,首先需要聲明權(quán)限:

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

其次不能再主線程中訪問網(wǎng)絡(luò),網(wǎng)絡(luò)操作可能是耗時(shí)的堆生,影響程序的效率专缠,而且在4.0系統(tǒng)會(huì)報(bào)錯(cuò)android.os.NetworkOnMainThreadException.

5、選用合適的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ā)訪問情形
AIDL 功能強(qiáng)大涝婉,支持一對(duì)多并發(fā)通信,支持實(shí)時(shí)通信 使用復(fù)雜蔗怠,需要處理好線程間的同步 一對(duì)多通信且有RPC需求
Messenger 功能一般墩弯,支持一對(duì)多串行通信,支持實(shí)時(shí)通信 不能很好的處理高并發(fā)的情形寞射,不支持RPC渔工,數(shù)據(jù)通過Message進(jìn)行傳輸,因此只能傳輸Bundle支持的數(shù)據(jù) 低并發(fā)的一對(duì)多即時(shí)通信桥温,無RPC需求引矩,或者不需要返回結(jié)果的RPC需求
ContentProvider 數(shù)據(jù)源訪問功能強(qiáng)大,支持一對(duì)多并發(fā)數(shù)據(jù)共享 主要提供數(shù)據(jù)源的CRUD操作 一對(duì)多進(jìn)程間的數(shù)據(jù)共享
Socket 功能強(qiáng)大,通過網(wǎng)絡(luò)傳輸字節(jié)流旺韭,支持一對(duì)多并發(fā)實(shí)時(shí)通信 實(shí)現(xiàn)繁瑣氛谜,不支持直接的RPC 網(wǎng)絡(luò)數(shù)據(jù)交換
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市区端,隨后出現(xiàn)的幾起案子值漫,更是在濱河造成了極大的恐慌,老刑警劉巖织盼,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杨何,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡沥邻,警方通過查閱死者的電腦和手機(jī)晚吞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門计济,熙熙樓的掌柜王于貴愁眉苦臉地迎上來熙含,“玉大人,你說我怎么就攤上這事并鸵÷” “怎么了捌蚊?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)近弟。 經(jīng)常有香客問我缅糟,道長(zhǎng),這世上最難降的妖魔是什么祷愉? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任窗宦,我火速辦了婚禮,結(jié)果婚禮上二鳄,老公的妹妹穿的比我還像新娘赴涵。我一直安慰自己,他們只是感情好订讼,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布髓窜。 她就那樣靜靜地躺著,像睡著了一般欺殿。 火紅的嫁衣襯著肌膚如雪寄纵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天脖苏,我揣著相機(jī)與錄音程拭,去河邊找鬼。 笑死棍潘,一個(gè)胖子當(dāng)著我的面吹牛恃鞋,可吹牛的內(nèi)容都是我干的屋吨。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼山宾,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了鳍徽?” 一聲冷哼從身側(cè)響起资锰,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎阶祭,沒想到半個(gè)月后绷杜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡濒募,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年鞭盟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瑰剃。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡齿诉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出晌姚,到底是詐尸還是另有隱情粤剧,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布挥唠,位于F島的核電站抵恋,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏宝磨。R本人自食惡果不足惜弧关,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望唤锉。 院中可真熱鬧世囊,春花似錦、人聲如沸窿祥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)壁肋。三九已至号胚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浸遗,已是汗流浹背猫胁。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留跛锌,地道東北人弃秆。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓届惋,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親菠赚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子脑豹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

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