結(jié)合 category 工作原理分析 OC2.0 中的 runtime

絕大多數(shù) iOS 開(kāi)發(fā)者在學(xué)習(xí) runtime 時(shí)都閱讀過(guò) runtime.h 文件中的這段代碼:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class   OBJC2_UNAVAILABLE;
    const char *name    OBJC2_UNAVAILABLE;
    long version      OBJC2_UNAVAILABLE;
    long info        OBJC2_UNAVAILABLE;
    long instance_size   OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars   OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists  OBJC2_UNAVAILABLE;
    struct objc_cache *cache  OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols  OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

可以看到其中保存了類的實(shí)例變量,方法列表等信息爱致。

不知道有多少讀者思考過(guò) OBJC2_UNAVAILABLE 意味著什么糠悯。其實(shí)早在 2006 年逢防,蘋(píng)果在 WWDC 大會(huì)上就發(fā)布了 Objective-C 2.0忘朝,其中的改動(dòng)包括 Max OS X 平臺(tái)上的垃圾回收機(jī)制(現(xiàn)已廢棄)判帮,runtime 性能優(yōu)化等晦墙。

這意味著上述代碼晌畅,以及任何帶有 OBJC2_UNAVAILABLE 標(biāo)記的內(nèi)容,都已經(jīng)在 2006 年就永遠(yuǎn)的告別了我們棋凳,只停留在歷史的文檔中剩岳。

Category 的原理

雖然上述代碼已經(jīng)過(guò)時(shí)拍棕,但仍具備一定的參考意義勺良,比如 methodLists 作為一個(gè)二級(jí)指針尚困,其中每個(gè)元素都是一個(gè)數(shù)組,數(shù)組中的每個(gè)元素則是一個(gè)方法示弓。

接下來(lái)就介紹一下 category 的工作原理奏属,在美團(tuán)的技術(shù)博客 深入理解Objective-C:Category 中已經(jīng)有了非常詳細(xì)的解釋潮峦,然而可能由于時(shí)間問(wèn)題忱嘹,其中的不少內(nèi)容已經(jīng)過(guò)時(shí)拘悦,我根據(jù)目前最新的版本(objc-680) 做一些簡(jiǎn)單的分析础米,為了便于閱讀,在不影響代碼邏輯的前提下有可能刪除部分無(wú)關(guān)緊要的內(nèi)容医寿。

概述

首先 runtime 依賴于 dyld 動(dòng)態(tài)加載靖秩,在 objc-os.mm 文件中可以找到入口盆偿,它的調(diào)用棧簡(jiǎn)單整理如下:

void _objc_init(void)
└──const char *map_2_images(...)
    └──const char *map_images_nolock(...)
        └──void _read_images(header_info **hList, uint32_t hCount)

以上四個(gè)方法可以理解為 runtime 的初始化過(guò)程事扭,我們暫且不深究乐横。在 _read_images 方法中有如下代碼:

if (cat->classMethods  ||  cat->protocols
    /* ||  cat->classProperties */) {
    addUnattachedCategoryForClass(cat, cls->ISA(), hi);
    if (cls->ISA()->isRealized()) {
        remethodizeClass(cls->ISA());
    }
}

根據(jù)注釋可見(jiàn)蘋(píng)果曾經(jīng)計(jì)劃利用 category 來(lái)添加屬性葡公。在 addUnattachedCategoryForClass 方法中會(huì)找到當(dāng)前類的所有 category催什,然后在 remethodizeClass 真正的去做處理。不過(guò)到目前為止還沒(méi)有接觸到相關(guān)的 category 處理拆内,我們繼續(xù)沿著調(diào)用棧向下走:

void _read_images(header_info **hList, uint32_t hCount)
└──static void remethodizeClass(Class cls)
    └──static void attachCategories(Class cls, category_list *cats, bool flush_caches)

這里的 attachCategories 就是處理 category 的核心所在麸恍,不過(guò)在閱讀這段代碼之前,我們有必要先熟悉一下相關(guān)的數(shù)據(jù)結(jié)構(gòu)搀矫。

Category 相關(guān)的數(shù)據(jù)結(jié)構(gòu)

首先來(lái)了解一下一個(gè) Category 是如何存儲(chǔ)的抹沪,在 objc-runtime-new.h 中可以看到如下定義,我只列出了其中成員變量:

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};

可見(jiàn)一個(gè) category 持有了一個(gè) method_list_t 類型的數(shù)組瓤球,method_list_t 又繼承自 entsize_list_tt融欧,這是一種泛型容器:

struct method_list_t : entsize_list_tt {
    // 成員變量和方法
};
template  struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;
};

這里的 entsize_list_tt 可以理解為一個(gè)容器,擁有自己的迭代器用于遍歷所有元素卦羡。 Element 表示元素類型噪馏,List 用于指定容器類型虹茶,最后一個(gè)參數(shù)為標(biāo)記位。

雖然這段代碼實(shí)現(xiàn)比較復(fù)雜隅要,但仍可了解到 method_list_t 是一個(gè)存儲(chǔ) method_t 類型元素的容器蝴罪。method_t 結(jié)構(gòu)體的定義如下:

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

最后,我們還有一個(gè)結(jié)構(gòu)體 category_list 用來(lái)存儲(chǔ)所有的 category步清,它的定義如下:

struct locstamped_category_list_t {
    uint32_t count;
    locstamped_category_t list[0];
};
struct locstamped_category_t {
    category_t *cat;
    struct header_info *hi;
};
typedef locstamped_category_list_t category_list;

除了標(biāo)記存儲(chǔ)的 category 的數(shù)量外要门,locstamped_category_list_t 結(jié)構(gòu)體還聲明了一個(gè)長(zhǎng)度為零的數(shù)組,這其實(shí)是 C99 中的一種寫(xiě)法廓啊,允許我們?cè)谶\(yùn)行期動(dòng)態(tài)的申請(qǐng)內(nèi)存欢搜。

以上就是相關(guān)的數(shù)據(jù)結(jié)構(gòu),只要了解到這個(gè)程度就可以繼續(xù)讀源碼了谴轮。

處理 Category

對(duì) Category 中方法的解析并不復(fù)雜炒瘟,首先來(lái)看一下 attachCategories 的簡(jiǎn)化版代碼:

static void attachCategories(Class cls, category_list *cats, bool flush_caches) {
    if (!cats) return;
    bool isMeta = cls->isMetaClass();
    method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));
    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int i = cats->count;
    while (i--) {
        auto& entry = cats->list[i];
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
        }
    }
    auto rw = cls->data();
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
}

首先,通過(guò) while 循環(huán)第步,我們遍歷所有的 category疮装,也就是參數(shù) cats 中的 list 屬性。對(duì)于每一個(gè) category粘都,得到它的方法列表 mlist 并存入 mlists 中廓推。

換句話說(shuō),我們將所有 category 中的方法拼接到了一個(gè)大的二維數(shù)組中翩隧,數(shù)組的每一個(gè)元素都是裝有一個(gè) category 所有方法的容器樊展。這句話比較繞,但你可以把 mlists 理解為文章開(kāi)頭所說(shuō),舊版本的 objc_method_list **methodLists专缠。

在 while 循環(huán)外雷酪,我們得到了拼接成的方法,此時(shí)需要與類原來(lái)的方法合并:

auto rw = cls->data();
rw->methods.attachLists(mlists, mcount);

這兩行代碼讀不懂是必然的藤肢,因?yàn)樵?Objective-C 2.0 時(shí)代太闺,對(duì)象的內(nèi)存布局已經(jīng)發(fā)生了一些變化。我們需要先了解對(duì)象的布局模型才能理解這段代碼嘁圈。

Objective-C 2.0 對(duì)象布局模型

  • objc_class

相信讀到這里的大部分讀者都學(xué)習(xí)過(guò)文章開(kāi)頭所說(shuō)的對(duì)象布局模型省骂,因此在這一部分,我們采用類比的方法最住,來(lái)看看 Objective-C 2.0 下發(fā)生了哪些改變钞澳。

首先,Class 和 id 指針的定義并沒(méi)有發(fā)生改變涨缚,他們一個(gè)指向類對(duì)應(yīng)的結(jié)構(gòu)體轧粟,一個(gè)指向?qū)ο髮?duì)應(yīng)的結(jié)構(gòu)體:

// objc.h
typedef struct objc_class *Class;
typedef struct objc_object *id;

比較有意思的一點(diǎn)是,objc_class 結(jié)構(gòu)體是繼承自 objc_object 的:

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() {
        return bits.data();
    }
};

這一點(diǎn)也很容易理解脓魏,早在 Objective-C 1.0 時(shí)代兰吟,我們就知道一個(gè)對(duì)象的結(jié)構(gòu)體只有 isa 指針,指向它所屬的類茂翔。而類的結(jié)構(gòu)體也有 isa 指針指向它的元類混蔼。因此讓類結(jié)構(gòu)體繼承自對(duì)象結(jié)構(gòu)體就很容易理解了。

可見(jiàn) Objective-C 1.0 的布局模型中珊燎,cache 和 super_class 被原封不動(dòng)的移過(guò)來(lái)了惭嚣,而剩下的屬性則似乎消失不見(jiàn)。取而代之的是一個(gè) bits 屬性悔政,以及 data() 方法晚吞,這個(gè)方法調(diào)用的其實(shí)是 bits 屬性的 data() 方法,并返回了一個(gè) class_rw_t 類型的結(jié)構(gòu)體指針谋国。

  • class_data_bits_t

以下是簡(jiǎn)化版 class_data_bits_t 結(jié)構(gòu)體的定義:

struct class_data_bits_t {
    uintptr_t bits;
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
}

可見(jiàn)這個(gè)結(jié)構(gòu)體只有一個(gè) 64 位的 bits 成員槽地,存儲(chǔ)了一個(gè)指向 class_rw_t 結(jié)構(gòu)體的指針和三個(gè)標(biāo)志位。它實(shí)際上由三部分組成芦瘾。首先由于 Mac OS X 只使用 47 位內(nèi)存地址闷盔,所以前 17 位空余出來(lái),提供給 retain/release 和 alloc/dealloc 方法使用旅急,做一些優(yōu)化逢勾。其次,由于內(nèi)存對(duì)齊藐吮,指針地址的后三位都是 0溺拱,因此可以用來(lái)做標(biāo)志位:

// class is a Swift class
#define FAST_IS_SWIFT           (1UL<<0)
// class or superclass has default retain/release/autorelease/retainCount/
//   _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR     (1UL<<1)
// class's instances requires raw isa
#define FAST_REQUIRES_RAW_ISA   (1UL<<2)
// data pointer
#define FAST_DATA_MASK          0x00007ffffffffff8UL

如果計(jì)算一下就會(huì)發(fā)現(xiàn)逃贝,F(xiàn)AST_DATA_MASK 這個(gè) 16 進(jìn)制常量的二進(jìn)制表示恰好后三位為0,且長(zhǎng)度為47位: 11111111111111111111111111111111111111111111000迫摔,我們通過(guò)這個(gè)掩碼做按位與運(yùn)算即可取出正確的指針地址沐扳。

引用 Draveness 在 深入解析 Objective-C 中方法的結(jié)構(gòu) 中的圖片做一個(gè)總結(jié):

1470280896667574.png

bits 示意圖

  • class_rw_t

bits 中包含了一個(gè)指向 class_rw_t 結(jié)構(gòu)體的指針,它的定義如下:

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
}

注意到有一個(gè)名字很類似的結(jié)構(gòu)體 class_ro_t句占,這里的 'rw' 和 ro' 分別表示 'readwrite' 和 'readonly'沪摄。因?yàn)? class_ro_t 存儲(chǔ)了一些由編譯器生成的常量。

These are emitted by the compiler and are part of the ABI.

正是由于 class_ro_t 中的兩個(gè)屬性 instanceStart 和 instanceSize 的存在纱烘,保證了 Objective-C2.0 的 ABI 穩(wěn)定性杨拐。因?yàn)榧词垢割愒黾臃椒ǎ宇愐部梢栽谶\(yùn)行時(shí)重新計(jì)算 ivar 的偏移量擂啥,從而避免重新編譯哄陶。

關(guān)于 ABI 穩(wěn)定性的問(wèn)題,本文不做贅述哺壶,讀者可以參考 Non Fragile ivars屋吨。

如果閱讀 class_ro_t 結(jié)構(gòu)體的定義就會(huì)發(fā)現(xiàn),舊版本實(shí)現(xiàn)中類結(jié)構(gòu)體中的大部分成員變量現(xiàn)在都定義在 class_ro_t 和 class_rw_t 這兩個(gè)結(jié)構(gòu)體中了山宾。感興趣的讀者可以自行對(duì)比至扰,本文不再贅述。

class_rw_t 結(jié)構(gòu)體中還有一個(gè) methods 成員變量资锰,它的類型是 method_array_t敢课,繼承自 list_array_tt。

list_array_tt 是一個(gè)泛型結(jié)構(gòu)體台妆,用于存儲(chǔ)一些元數(shù)據(jù)翎猛,而它實(shí)際上是元數(shù)據(jù)的二維數(shù)組:

{
    struct array_t {
        uint32_t count;
        List* lists[0];
    };
}
class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_list_t **beginCategoryMethodLists() {
        return beginLists();
    }
    
    method_list_t **endCategoryMethodLists(Class cls);

    method_array_t duplicate() {
        return Super::duplicate<method_array_t>();
    }
};

其中 Element 表示元數(shù)據(jù)的類型胖翰,比如 method_t接剩,而 List 則表示用于存儲(chǔ)元數(shù)據(jù)的一維數(shù)組,比如 method_list_t萨咳。

list_array_tt 有三種狀態(tài):

  • 自身為空懊缺,可以類比為 [[]]

  • 它只有一個(gè)指針,指向一個(gè)元數(shù)據(jù)的集合培他,可以類比為 [[1, 2]]

  • 它有多個(gè)指針鹃两,指向多個(gè)元數(shù)據(jù)的集合,可以類比為 [[1, 2], [3, 4]]

當(dāng)一個(gè)類剛創(chuàng)建時(shí)舀凛,它可能處于狀態(tài) 1 或 2俊扳,但如果使用 class_addMethod 或者 category 來(lái)添加方法,就會(huì)進(jìn)入狀態(tài) 3猛遍,而且一旦進(jìn)入狀態(tài) 3 就再也不可能回到其他狀態(tài)馋记,即使新增的方法后來(lái)又被移除掉号坡。

方法合并

掌握了這些 runtime 的基礎(chǔ)只是以后就可以繼續(xù)鉆研剩下的 category 的代碼了:

auto rw = cls->data();
rw->methods.attachLists(mlists, mcount);

這是剛剛卡住的地方,現(xiàn)在來(lái)看梯醒,rw 是一個(gè) class_rw_t 類型的結(jié)構(gòu)體指針宽堆。根據(jù) runtime 中的數(shù)據(jù)結(jié)構(gòu),它有一個(gè) methods 結(jié)構(gòu)體成員茸习,并從父類繼承了 attachLists 方法畜隶,用來(lái)合并 category 中的方法:

void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;
    uint32_t oldCount = array()->count;
    uint32_t newCount = oldCount + addedCount;
    setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
    array()->count = newCount;
    memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
    memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
}

這段代碼很簡(jiǎn)單,其實(shí)就是先調(diào)用 realloc() 函數(shù)將原來(lái)的空間拓展号胚,然后把原來(lái)的數(shù)組復(fù)制到后面籽慢,最后再把新數(shù)組復(fù)制到前面。

在實(shí)際代碼中涕刚,比上面略復(fù)雜一些嗡综。因?yàn)闉榱颂岣咝阅埽O(píng)果做了一些優(yōu)化杜漠,比如當(dāng) List 處于第二種狀態(tài)(只有一個(gè)指針极景,指向一個(gè)元數(shù)據(jù)的集合)時(shí),其實(shí)并不需要在原地?cái)U(kuò)容空間驾茴,而是只要重新申請(qǐng)一塊內(nèi)存盼樟,并將最后一個(gè)位置留給原來(lái)的集合即可。

這樣只多花費(fèi)了很少的內(nèi)存空間锈至,也就是原來(lái)二維數(shù)組占用的內(nèi)存空間晨缴,但是 malloc() 的性能優(yōu)勢(shì)會(huì)更加明顯,這其實(shí)是一個(gè)空間換時(shí)間的權(quán)衡問(wèn)題峡捡。

需要注意的是击碗,無(wú)論執(zhí)行哪種邏輯,參數(shù)列表中的方法都會(huì)被添加到二維數(shù)組的前面们拙。而我們簡(jiǎn)單的看一下 runtime 在查找方法時(shí)的邏輯:

static method_t *getMethodNoSuper_nolock(Class cls, SEL sel){
    for (auto mlists = cls->data()->methods.beginLists(),
              end = cls->data()->methods.endLists();
         mlists != end;
         ++mlists) {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}
static method_t *search_method_list(const method_list_t *mlist, SEL sel) {
    for (auto& meth : *mlist) {
        if (meth.name == sel) return &meth;
    }
}

可見(jiàn)搜索的過(guò)程是按照從前向后的順序進(jìn)行的稍途,一旦找到了就會(huì)停止循環(huán)。因此 category 中定義的同名方法不會(huì)替換類中原有的方法砚婆,但是對(duì)原方法的調(diào)用實(shí)際上會(huì)調(diào)用 category 中的方法械拍。

相關(guān)文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市装盯,隨后出現(xiàn)的幾起案子坷虑,更是在濱河造成了極大的恐慌,老刑警劉巖埂奈,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迄损,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡账磺,警方通過(guò)查閱死者的電腦和手機(jī)芹敌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)共屈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人党窜,你說(shuō)我怎么就攤上這事拗引。” “怎么了幌衣?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵矾削,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我豁护,道長(zhǎng)哼凯,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任楚里,我火速辦了婚禮断部,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘班缎。我一直安慰自己蝴光,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布达址。 她就那樣靜靜地躺著蔑祟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沉唠。 梳的紋絲不亂的頭發(fā)上疆虚,一...
    開(kāi)封第一講書(shū)人閱讀 49,829評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音满葛,去河邊找鬼径簿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛嘀韧,可吹牛的內(nèi)容都是我干的篇亭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼乳蛾,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼暗赶!你這毒婦竟也來(lái)了鄙币?” 一聲冷哼從身側(cè)響起肃叶,我...
    開(kāi)封第一講書(shū)人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎十嘿,沒(méi)想到半個(gè)月后因惭,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绩衷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年蹦魔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了激率。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡勿决,死狀恐怖乒躺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情低缩,我是刑警寧澤嘉冒,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站咆繁,受9級(jí)特大地震影響讳推,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜玩般,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一银觅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坏为,春花似錦究驴、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至帘撰,卻和暖如春跑慕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背摧找。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工核行, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蹬耘。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓芝雪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親综苔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子惩系,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

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