iOS底層原理 15 :類的加載(下)

上一篇中若厚,我們介紹了類是如何從mach-o中加載到內(nèi)存的,分析了read_images方法蜒什,readClass方法, realizeClassWithoutSwift方法测秸,methodizeClass以及attachLists方法

接下來(lái)我們探索分類的加載,在探索之前我們需要知道分類的結(jié)構(gòu)

category_t結(jié)構(gòu)

1灾常、探索category_t的底層結(jié)構(gòu)
  • 首先我們?cè)?code>mian.m函數(shù)里面去定義一個(gè)分類
@interface NSObject (LGB)
@property (nonatomic,strong) NSString * cate_name;

- (void)cate_instanceMethod1;
- (void)cate_instanceMethod2;
- (void)cate_instanceMethod3;
- (void)cate_instanceMethod4;
+ (void)cate_instanceMethod;
@end

@implementation NSObject (LGB)
- (void)cate_instanceMethod1{
    NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod2{
    NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod3{
    NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod4{
    NSLog(@"%s",__func__);
}
+ (void)cate_instanceMethod{
    NSLog(@"%s",__func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSObject *obj = [[NSObject alloc]init];
        
        LGPerson *person = [[LGPerson alloc] init];
        
        NSLog(@"%ld - %ld",sizeof(person),class_getInstanceSize(person.class));
    }
    return 0;
}
  • 使用 clang rewrite-objc main.m -o mian.cpp 終端命令去編譯mian.m得到mian.cpp文件

  • mian.cpp里面,我們得到了分類的底層結(jié)構(gòu), instance_methods表示實(shí)例方法列表霎冯,class_methods表示類方法列表

struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};
  • 其中instance_methods表示實(shí)例方法列表class_methods表示類方法列表
  • 我們發(fā)現(xiàn)了一個(gè)問(wèn)題:查看看_prop_list_t钞瀑,明明分類中定義了屬性沈撞,但是在底層編譯中并沒(méi)有看到屬性,如下圖所示雕什,這是因?yàn)?code>分類中定義的屬性沒(méi)有相應(yīng)的set缠俺、get方法显晶,我們可以通過(guò)關(guān)聯(lián)對(duì)象來(lái)設(shè)置
static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_NSObject_$_LGB __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"cate_name","T@\"NSString\",&,N"}}
};

當(dāng)然我們通過(guò)objc源碼搜索 category_t,我們也能得到分類category_t的結(jié)構(gòu):

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;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};
2壹士、分類與類拓展的區(qū)別

category : 分類磷雇,類別

  • 專門用來(lái)給類添加新的方法
  • 不能給類添加成員變量
  • 可以添加屬性, 但是只會(huì)生成變量的 getter setter 方法的聲明墓卦,不會(huì)生成方法的實(shí)現(xiàn)和帶下劃線的成員變量

extension : 類拓展

  • 可以說(shuō)是特殊的分類倦春,也稱為匿名分類
  • 可以給類添加成員變量和屬性,但是是私有變量
  • 可以給類添加方法落剪,但也是私有方法
3、關(guān)聯(lián)對(duì)象

如果想要給分類有效的添加屬性尿庐,需要在重寫的 getter setter方法里面去關(guān)聯(lián)對(duì)象

#import <Foundation/Foundation.h>
@interface NSObject (LGA)
@property (nonatomic,strong) NSString * lga_name;
@end

#import "NSObject+LGA.h"
#import <objc/runtime.h>
@implementation NSObject (LGA)
- (void)setLga_name:(NSString *)lga_name{
    objc_setAssociatedObject(self, "lga_name", lga_name, OBJC_ASSOCIATION_RETAIN);
}

- (NSString *)lga_name{
    return  objc_getAssociatedObject(self, "lga_name");
}
@end

分類的加載

準(zhǔn)備: 創(chuàng)建LGPerson的兩個(gè)分類:LGA LGB



在分析realizeClassWithoutSwift時(shí)忠怖,realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories中提及了rwe的加載,中分析了分類的data數(shù)據(jù) 是如何 加載到類中的抄瑟,且分類的加載順序是:LGA -> LGB的順序加載到類中凡泣,即越晚加進(jìn)來(lái),越在前面

其中查看methodizeClass的源碼實(shí)現(xiàn)皮假,可以發(fā)現(xiàn)類的數(shù)據(jù)分類的數(shù)據(jù)是分開(kāi)處理的鞋拟,主要是因?yàn)?code>類在編譯階段,就已經(jīng)確定好了方法的歸屬位置(即實(shí)例方法存儲(chǔ)在類中惹资,類方法存儲(chǔ)在元類中)贺纲,而分類是后面才加進(jìn)來(lái)的


其中分類需要通過(guò)attatchToClass添加到類,然后才能在外界進(jìn)行使用褪测,在此過(guò)程猴誊,我們已經(jīng)知道了分類加載三步驟的后面兩個(gè)步驟,分類的加載主要分為3步:

  • 【第一步】分類數(shù)據(jù)加載時(shí)機(jī):根據(jù)類和分類是否實(shí)現(xiàn)load方法來(lái)區(qū)分不同的時(shí)機(jī)
  • 【第二步】attachCategories準(zhǔn)備分類數(shù)據(jù)
  • 【第三步】attachLists將分類數(shù)據(jù)添加到主類中
【第一步】分類加載的時(shí)機(jī)

以主類LGPerson + 分類LGA侮措、LGB 均實(shí)現(xiàn)+load方法為例

  • load_images
  • loadAllCategories
  • load_categories_nolock
  • attachCategories

拓展:只要有一個(gè)分類是非懶加載分類懈叹,那么所有的分類都會(huì)被標(biāo)記位非懶加載分類

分類與類的搭配下的加載時(shí)機(jī)

【情況1】非懶加載類 + 非懶加載分類

  • 類的數(shù)據(jù)加載是通過(guò)_getObjc2NonlazyClassList加載,即ro分扎、rw的操作澄成,對(duì)rwe賦值初始化,是在extAlloc方法中
  • 分類的數(shù)據(jù)加載是通過(guò)load_images加載到類中的

其調(diào)用路徑為:

  • 非懶加載類路徑:map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass ,此時(shí)的mlists是一維數(shù)組畏吓,然后走到load_images部分

  • 非懶加載分類路徑:load_images --> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists墨状,此時(shí)的mlists是二維數(shù)組

【情況2】非懶加載類 + 懶加載分類
非懶加載類 與 懶加載分類的數(shù)據(jù)加載,有如下結(jié)論:

  • 類 和 分類的加載是在read_images就加載數(shù)據(jù)
  • 其中data數(shù)據(jù)在編譯時(shí)期就已經(jīng)完成了

【情況3】懶加載類 + 懶加載分類
懶加載類 與 懶加載分類的數(shù)據(jù)加載是在消息第一次調(diào)用時(shí)加載的

【情況4】懶加載類 + 非懶加載分類
只要分類實(shí)現(xiàn)了load庵佣,會(huì)迫使主類提前加載,即 主類 強(qiáng)行轉(zhuǎn)換為 非懶加載類樣式歉胶, 加載流程就和情況1是一致的

分類與類的搭配下的加載時(shí)機(jī).png

關(guān)聯(lián)對(duì)象的原理

關(guān)聯(lián)對(duì)象設(shè)置值流程

首先我們先來(lái)了解一下objc_setAssociatedObject方法的四個(gè)參數(shù):

  • 參數(shù)1:要關(guān)聯(lián)的對(duì)象,即給誰(shuí)添加關(guān)聯(lián)屬性
  • 參數(shù)2:標(biāo)識(shí)符巴粪,方便下次查找
  • 參數(shù)3:value
  • 參數(shù)4:屬性的策略通今,即retain粥谬,copy,


objc_setAssociatedObject源碼實(shí)現(xiàn):
SetAssocHook.get()是一種接口模式的設(shè)計(jì)思想辫塌,對(duì)外的接口不變漏策,內(nèi)部的邏輯變化不影響外部的調(diào)用


進(jìn)入SetAssocHook,其底層實(shí)現(xiàn)是_base_objc_setAssociatedObject臼氨,類型是ChainedHookFunction

所以可以理解為SetAssocHook.get()等價(jià)于_base_objc_setAssociatedObject

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);//接口模式掺喻,對(duì)外接口始終不變
}

??等價(jià)于

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    _base_objc_setAssociatedObject(object, key, value, policy);//接口模式,對(duì)外接口始終不變
}

進(jìn)入_base_objc_setAssociatedObject源碼實(shí)現(xiàn):_base_objc_setAssociatedObject -> _object_set_associative_reference
進(jìn)入_object_set_associative_reference源碼實(shí)現(xiàn)

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;

    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    //object封裝成一個(gè)數(shù)組結(jié)構(gòu)類型储矩,類型為DisguisedPtr
    DisguisedPtr<objc_object> disguised{(objc_object *)object};//相當(dāng)于包裝了一下 對(duì)象object,便于使用
    // 包裝一下 policy - value
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();//根據(jù)策略類型進(jìn)行處理
    //局部作用域空間
    {
        //初始化manager變量感耙,相當(dāng)于自動(dòng)調(diào)用AssociationsManager的析構(gòu)函數(shù)進(jìn)行初始化
        AssociationsManager manager;//并不是全場(chǎng)唯一,構(gòu)造函數(shù)中加鎖只是為了避免重復(fù)創(chuàng)建持隧,在這里是可以初始化多個(gè)AssociationsManager變量的
    
        AssociationsHashMap &associations(manager.get());//AssociationsHashMap 全場(chǎng)唯一

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});//返回的結(jié)果是一個(gè)類對(duì)
            if (refs_result.second) {//判斷第二個(gè)存不存在即硼,即bool值是否為true
                /* it's the first association we make 第一次建立關(guān)聯(lián)*/
                object->setHasAssociatedObjects();//nonpointerIsa ,標(biāo)記位true
            }

            /* establish or replace the association 建立或者替換關(guān)聯(lián)*/
            auto &refs = refs_result.first->second; //得到一個(gè)空的桶子屡拨,找到引用對(duì)象類型,即第一個(gè)元素的second值
            auto result = refs.try_emplace(key, std::move(association));//查找當(dāng)前的key是否有association關(guān)聯(lián)對(duì)象
            if (!result.second) {//如果結(jié)果不存在
                association.swap(result.first->second);
            }
        } else {//如果傳的是空值只酥,則移除關(guān)聯(lián),相當(dāng)于移除
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();//釋放
}

通過(guò)源碼可知呀狼,我們總結(jié)一下關(guān)聯(lián)對(duì)象的設(shè)置流程:

  • 1.創(chuàng)建一個(gè) AssociationsManager 管理類
  • 2.獲取唯一的全局靜態(tài)哈希Map:AssociationsHashMap
  • 3.判斷是否插入的關(guān)聯(lián)值value是否存在
    - 3.1 如果存在走第4步
    - 3.2 如果不存在裂允,關(guān)聯(lián)對(duì)象 -插入空 的流程
  • 4.通過(guò)try_emplace方法,并創(chuàng)建一個(gè)空的 ObjectAssociationMap 去取查詢的鍵值對(duì)
  • 5.如果發(fā)現(xiàn)沒(méi)有這個(gè) key 就插入一個(gè) 空的 BucketT進(jìn)去并返回true
  • 6.通過(guò)setHasAssociatedObjects方法標(biāo)記對(duì)象存在關(guān)聯(lián)對(duì)象即置isa指針的has_assoc屬性為true
  • 7.用當(dāng)前 policy 和 value 組成了一個(gè) ObjcAssociation 替換原來(lái) BucketT 中的空
  • 8.標(biāo)記一下 ObjectAssociationMap 的第一次為 false
關(guān)聯(lián)對(duì)象取值流程

我們通過(guò)源碼 objc_getAssociatedObject() --> _object_get_ associative_reference(object,key)

_object_get_associative_reference的源碼實(shí)現(xiàn):

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};//創(chuàng)建空的關(guān)聯(lián)對(duì)象

    {
        AssociationsManager manager;//創(chuàng)建一個(gè)AssociationsManager管理類
        AssociationsHashMap &associations(manager.get());//獲取全局唯一的靜態(tài)哈希map
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);//找到迭代器哥艇,即獲取buckets
        if (i != associations.end()) {//如果這個(gè)迭代查詢器不是最后一個(gè) 獲取
            ObjectAssociationMap &refs = i->second; //找到ObjectAssociationMap的迭代查詢器獲取一個(gè)經(jīng)過(guò)屬性修飾符修飾的value
            ObjectAssociationMap::iterator j = refs.find(key);//根據(jù)key查找ObjectAssociationMap绝编,即獲取bucket
            if (j != refs.end()) {
                association = j->second;//獲取ObjcAssociation
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();//返回value
}

通過(guò)源碼可知,主要分為以下部分:

  • 創(chuàng)建一個(gè) AssociationManager管理類
  • 獲取唯一的全局靜態(tài)哈希Map:AssociationMap
  • 通過(guò)find方法根據(jù) DisguisedPtr找到 AssociationsHashMap 中的 iterator 迭代查詢器
  • 如果這個(gè)迭代查詢器不是最后一個(gè) 獲取 : ObjectAssociationMap (policy和value)
  • 通過(guò)find方法找到ObjectAssociationMap的迭代查詢器獲取一個(gè)經(jīng)過(guò)屬性修飾符修飾的value
  • 返回value

關(guān)聯(lián)對(duì)象涉及的哈希Map結(jié)構(gòu)

  • AssociationHashMap里面存放的是ObjectAssociationMap
  • ObjectAssociationMap存放的是ObjectAssociation
  • ObjectAssociation是一種類似字典一樣結(jié)構(gòu)她奥,存放{policy ,value}結(jié)構(gòu)

load_images

load_image的源碼分析:

  • 1.首先找到所有懶加載類的load方法:prepare_load_methods()
  • 2.然后進(jìn)行調(diào)用:call_load_methods()
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }

    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        //
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

prepare_load_methods()源碼分析:

  • 獲取非懶加載類瓮增,以及繼承鏈上實(shí)現(xiàn)了load方法的類
  • 獲取非懶加載分類上實(shí)現(xiàn)了load方法的類
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    //_getObjc2NonlazyClassList 獲取非懶加載類的列表
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 遞歸獲取繼承鏈上的類
        schedule_class_load(remapClass(classlist[i]));
    }

    // 獲取非懶加載分類上的list
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

call_load_methods()源碼分析:

  • call_class_loads() 執(zhí)行類的load方法
  • call_category_loads() 執(zhí)行分類的load方法
void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

load_images流程分析圖:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市哩俭,隨后出現(xiàn)的幾起案子绷跑,更是在濱河造成了極大的恐慌,老刑警劉巖凡资,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砸捏,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡隙赁,警方通過(guò)查閱死者的電腦和手機(jī)垦藏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)伞访,“玉大人掂骏,你說(shuō)我怎么就攤上這事『裰溃” “怎么了弟灼?”我有些...
    開(kāi)封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵级解,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我田绑,道長(zhǎng)勤哗,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任掩驱,我火速辦了婚禮芒划,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘欧穴。我一直安慰自己民逼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布苔可。 她就那樣靜靜地躺著缴挖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪焚辅。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天苟鸯,我揣著相機(jī)與錄音同蜻,去河邊找鬼。 笑死早处,一個(gè)胖子當(dāng)著我的面吹牛湾蔓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播砌梆,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼默责,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了咸包?” 一聲冷哼從身側(cè)響起桃序,我...
    開(kāi)封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎烂瘫,沒(méi)想到半個(gè)月后媒熊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坟比,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年芦鳍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片葛账。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡柠衅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出籍琳,到底是詐尸還是另有隱情菲宴,我是刑警寧澤贷祈,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站裙顽,受9級(jí)特大地震影響付燥,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜愈犹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一键科、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧漩怎,春花似錦勋颖、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至叁执,卻和暖如春茄厘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谈宛。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工次哈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吆录。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓窑滞,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親恢筝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哀卫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353