iOS KVC 底層原理

KVC的全稱是Key-Value Coding疏之,翻譯成中文是 鍵值編碼胆筒,鍵值編碼是由NSKeyValueCoding非正式協(xié)議啟用的一種機制朽色,對象采用該協(xié)議來間接訪問其屬性拘悦。既可以通過一個·字符串key來訪問某個屬性·齿兔。這種間接訪問機制補充了實例變量及其相關的訪問器方法所提供的直接訪問。

KVC 相關API
常用方法

主要有以下四個常用的方法

通過key設值/取值

//直接通過Key來取值
- (nullable id)valueForKey:(NSString *)key;

//通過Key來設值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
  • 通過keyPath (即路由)設值/取值
//通過KeyPath來取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; 

//通過KeyPath來設值                 
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  

其他方法

//默認返回YES础米,表示如果沒有找到Set<Key>方法的話分苇,會按照_key,_iskey屁桑,key医寿,iskey的順序搜索成員,設置成NO就不這樣搜索
+ (BOOL)accessInstanceVariablesDirectly;

//KVC提供屬性值正確性驗證的API蘑斧,它可以用來檢查set的值是否正確靖秩、為不正確的值做一個替換值或者拒絕設置新值并返回錯誤原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

//這是集合操作的API竖瘾,里面還有一系列這樣的API沟突,如果屬性是一個NSMutableArray,那么可以用這個方法來返回捕传。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

//如果Key不存在惠拭,且KVC無法搜索到任何和Key有關的字段或者屬性,則會調(diào)用這個方法庸论,默認是拋出異常职辅。
- (nullable id)valueForUndefinedKey:(NSString *)key;

//和上一個方法一樣,但這個方法是設值聂示。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

//如果你在SetValue方法時面給Value傳nil域携,則會調(diào)用這個方法
- (void)setNilValueForKey:(NSString *)key;

//輸入一組key,返回該組key對應的Value,再轉成字典返回催什,用于將Model轉到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

KVC 設值 底層原理

在日常開發(fā)中,針對對象屬性的賦值蒲凶,一般有以下兩種方式

  • 直接通過setter方法賦值
  • 通過KVC鍵值編碼的相關API賦值
LGPerson *person = [[LGPerson alloc] init];
// 1气筋、一般setter 方法
person.name      = @"CJL_哈哈";
// 2、KVC方式
[person setValue:@"CJL_嘻嘻" forKey:@"name"]; 

下面針對使用最多的KVC設值方法:setValue:forKey旋圆,來進行其底層原理的探索宠默。

首先進入setValue:forKey的聲明,發(fā)現(xiàn)是在Foundation框架中灵巧,而Foundation框架是不開源的搀矫,有以下幾種方式可以去探索底層

  • 通過Hopper反匯編,查看偽代碼
  • 通過蘋果官方文檔
  • Github搜索是否有相關的demo

在這里刻肄,我們通過Key-Value Coding Programming Guide蘋果官方文檔來研究瓤球,針對設值流程,有如下說明

當調(diào)用setValue:forKey:設置屬性value時敏弃,其底層的執(zhí)行流程為

  • 【第一步】首先查找是否有這三種setter方法卦羡,按照查找順序為set<Key>:-> _set<Key> -> setIs<Key>

    • 如果有其中任意一個setter方法,則直接設置屬性的value(主注意:key是指成員變量名麦到,首字符大小寫需要符合KVC的命名規(guī)范)

    • 如果都沒有绿饵,則進入【第二步】

  • 【第二步】:如果沒有第一步中的三個簡單的setter方法,則查找accessInstanceVariablesDirectly是否返回YES瓶颠,

    • 如果返回YES拟赊,則查找間接訪問的實例變量進行賦值,查找順序為:_<key> -> _is<Key> -> <key> -> is<Key>

      • 如果找到其中任意一個實例變量粹淋,則賦值

      • 如果都沒有吸祟,則進入【第三步】

    • 如果返回NO,則進入【第三步】

  • 【第三步】如果setter方法 或者 實例變量都沒有找到廓啊,系統(tǒng)會執(zhí)行該對象的setValue:forUndefinedKey:方法欢搜,默認拋出NSUndefinedKeyException類型的異常

綜上所述,KVC通過 setValue:forKey: 方法設值的流程以設置LGPerson的對象person的屬性name為例谴轮,如下圖所示

image

KVC 取值 底層原理

同樣的炒瘟,我們可以通過官方文檔分析KVC取值的底層原理

當調(diào)用valueForKey:時,其底層的執(zhí)行流程如下

  • 【第一步】首先查找getter方法第步,按照get<Key> -> <key> -> is<Key> -> _<key>的方法順序查找疮装,

    • 如果找到,則進入【第五步】

    • 如果沒有找到粘都,則進入【第二步】

  • 【第二步】如果第一步中的getter方法沒有找到廓推,KVC會查找countOf <Key>和objectIn <Key> AtIndex :和<key> AtIndexes :

    • 如果找到countOf <Key>和其他兩個中的一個,則會創(chuàng)建一個響應所有NSArray方法的集合代理對象翩隧,并返回該對象樊展,即NSKeyValueArray,是NSArray子類。代理對象隨后將接收到的所有NSArray消息轉換為countOf<Key>专缠,objectIn<Key> AtIndex:和<key>AtIndexes:消息的某種組合雷酪,用來創(chuàng)建鍵值編碼對象。如果原始對象還實現(xiàn)了一個名為get<Key>:range:之類的可選方法涝婉,則代理對象也將在適當時使用該方法(注意:方法名的命名規(guī)則要符合KVC的標準命名方法哥力,包括方法簽名。)

    • 如果沒有找到這三個訪問數(shù)組的墩弯,請繼續(xù)進入【第三步】

  • 【第三步】如果沒有找到上面的幾種方法吩跋,則會同時查找countOf <Key>,enumeratorOf<Key>和memberOf<Key>這三個方法

    • 如果這三個方法都找到渔工,則會創(chuàng)建一個響應所有NSSet方法的集合代理對象锌钮,并返回該對象,此代理對象隨后將其收到的所有NSSet消息轉換為countOf<Key>涨缚,enumeratorOf<Key>和memberOf<Key>:消息的某種組合轧粟,用于創(chuàng)建它的對象

    • 如果還是沒有找到,則進入【第四步】

  • 【第四步】如果還沒有找到脓魏,檢查類方法InstanceVariablesDirectly是否YES兰吟,依次搜索_<key>,_is<Key>茂翔,<key>或is<Key>的實例變量

    • 如果搜到混蔼,直接獲取實例變量的值,進入【第五步】
  • 【第五步】根據(jù)搜索到的屬性值的類型珊燎,返回不同的結果

    • 如果是對象指針惭嚣,則直接返回結果

    • 如果是NSNumber支持的標量類型,則將其存儲在NSNumber實例中并返回它

    • 如果是是NSNumber不支持的標量類型悔政,請轉換為NSValue對象并返回該對象

  • 【第六步】如果上面5步的方法均失敗晚吞,系統(tǒng)會執(zhí)行該對象的valueForUndefinedKey:方法,默認拋出NSUndefinedKeyException類型的異常

綜上所述谋国,KVC通過 valueForKey: 方法取值的流程以設置LGPerson的對象person的屬性name為例槽地,如下圖所示

&

自定義KVC

原理:通過給NSObject添加分類CJLKVC,實現(xiàn)自定義的cjl_setValue:forKey:cjl_valueForKey:方法芦瘾,根據(jù)蘋果官方文檔提供的查找規(guī)則進行實現(xiàn)

@interface NSObject (CJLKVC)

//設值
- (void)cjl_setValue:(nullable id)value forKey:(NSString *)key;
//取值
- (nullable id)cjl_valueForKey:(NSString *)key;

@end

自定義KVC設值

自定義KVC設置流程捌蚊,主要分為以下幾個步驟:

  • 1、判斷key非空
  • 2近弟、查找setter方法缅糟,順序是:setXXX、_setXXX祷愉、 setIsXXX
  • 3窗宦、判斷是否響應accessInstanceVariablesDirectly方法赦颇,即間接訪問實例變量,
    • 返回YES赴涵,繼續(xù)下一步設值沐扳,
    • 如果是NO,則崩潰
  • 4句占、間接訪問變量賦值(只會走一次),順序是:_key躯嫉、_isKey纱烘、key、isKey
    • 4.1 定義一個收集實例變量的可變數(shù)組
    • 4.2 通過class_getInstanceVariable方法祈餐,獲取相應的 ivar
    • 4.3 通過object_setIvar方法擂啥,對相應的 ivar 設置值
  • 5、如果找不到相關實例變量帆阳,則拋出異常
//設值
- (void)cjl_setValue:(nullable id)value forKey:(NSString *)key{

//    1哺壶、判斷key 是否存在
    if (key == nil || key.length == 0) return;

//    2、找setter方法蜒谤,順序是:setXXX山宾、_setXXX、 setIsXXX
    // key 要大寫
    NSString *Key = key.capitalizedString;
    // key 要大寫
    NSString *setKey = [NSString stringWithFormat:@"set%@:", Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:", Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", Key];

    if ([self cjl_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*************%@*************", setKey);
        return;
    }else if([self cjl_performSelectorWithMethodName:_setKey value:value]){
        NSLog(@"*************%@*************", _setKey);
        return;
    }else if([self cjl_performSelectorWithMethodName:setIsKey value:value]){
        NSLog(@"*************%@*************", setIsKey);
        return;
    }

//    3鳍徽、判斷是否響應`accessInstanceVariablesDirectly`方法资锰,即間接訪問實例變量,返回YES阶祭,繼續(xù)下一步設值绷杜,如果是NO,則崩潰
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"CJLUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }

//    4濒募、間接訪問變量賦值鞭盟,順序為:_key、_isKey瑰剃、key齿诉、isKey
    // 4.1 定義一個收集實例變量的可變數(shù)組
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@", key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@", key];
    NSString *isKey = [NSString stringWithFormat:@"is%@", key];
    if ([mArray containsObject:_key]) {
        // 4.2 獲取相應的 ivar
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 對相應的 ivar 設置值
        object_setIvar(self, ivar, value);
        return;
    }else if ([mArray containsObject:_isKey]) {

        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        object_setIvar(self, ivar, value);
        return;
    }else if ([mArray containsObject:key]) {

        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        object_setIvar(self, ivar, value);
        return;
    }else if ([mArray containsObject:isKey]) {

        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        object_setIvar(self, ivar, value);
        return;
    }

//    5、如果找不到則拋出異常
    @throw [NSException exceptionWithName:@"CJLUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];

}

自定義KVC取值

取值的自定義代碼如下培他,分為以下幾步

  • 1鹃两、判斷key非空

  • 2、查找相應方法舀凛,順序是:get<Key>俊扳、 <key>、 countOf<Key>猛遍、 objectIn<Key>AtIndex

  • 3馋记、判斷是否能夠直接賦值實例變量号坡,即判斷是否響應accessInstanceVariablesDirectly方法,間接訪問實例變量梯醒,

    • 返回YES宽堆,繼續(xù)下一步取值

    • 如果是NO,則崩潰

  • 4茸习、間接訪問實例變量畜隶,順序是:_<key> _is<Key> <key> is<Key>

    • 4.1 定義一個收集實例變量的可變數(shù)組

    • 4.2 通過class_getInstanceVariable方法,獲取相應的 ivar

    • 4.3 通過object_getIvar方法号胚,返回相應的 ivar 的值

//取值
- (nullable id)cjl_valueForKey:(NSString *)key{

//    1籽慢、判斷非空
    if (key == nil || key.length == 0) {
        return nil;
    }

//    2、找到相關方法:get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // key 要大寫
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(key)]){
        return [self performSelector:NSSelectorFromString(key)];
    }
    //集合類型
    else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
        if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
            int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
            for (int i = 0; i<num-1; i++) {
                num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            }
            for (int j = 0; j<num; j++) {
                id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                [mArray addObject:objc];
            }
            return mArray;
        }
    }

#pragma clang diagnostic pop

//    3猫胁、判斷是否響應`accessInstanceVariablesDirectly`方法箱亿,即間接訪問實例變量,返回YES弃秆,繼續(xù)下一步設值届惋,如果是NO,則崩潰
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"CJLUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }

//    4.找相關實例變量進行賦值菠赚,順序為:_<key>脑豹、 _is<Key>、 <key>衡查、 is<Key>
    // 4.1 定義一個收集實例變量的可變數(shù)組
    NSMutableArray *mArray = [self getIvarListName];
    // 例如:_name -> _isName -> name -> isName
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }

    return @"";

    return @"";
}

使用路由訪問晨缴,即keyPath

在日常開發(fā)中,一個類的成員變量有可能是自定義類或者其他的復雜數(shù)據(jù)類型峡捡,一般的操作是击碗,我們可以先通過KVC獲取該屬性,然后再通過KVC獲取自定義類的屬性们拙,就是比較麻煩稍途,還有另一種比較簡便的方法,就是使用KeyPath路由砚婆,涉及以下兩個方法:setValue:forKeyPath:valueForKeyPath:

//通過KeyPath來取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;                  

//通過KeyPath來設值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

參考如下的案例

//CJLPerson類
@interface CJLPerson : NSObject
@property (nonatomic, copy)   NSString          *age;
@property (nonatomic, strong) CJLStudent         *student;
@end

//CJLStudent類
@interface CJLStudent : NSObject
@property (nonatomic, copy)   NSString          *name;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJLPerson *person = [[CJLPerson alloc] init];
        CJLStudent *student = [CJLStudent alloc];
        student.name    = @"CJL";
        person.student     = student;
        //根據(jù)kvc - keyPath路由修改student的subject屬性的值
        [person setValue:@"嘻嘻" forKeyPath:@"student.name"];
        NSLog(@"%@",[person valueForKeyPath:@"student.name"]);
    }
    return 0;
}

//*************打印結果*************
2020-10-27 09:55:08.512833+0800 001-KVC簡介[58089:6301894] 改變前:CJL
2020-10-27 09:55:08.512929+0800 001-KVC簡介[58089:6301894] 改變后:嘻嘻

KVC 使用場景

1械拍、動態(tài)設值和取值

  • 常用的可以通過setValue:forKey:valueForKey:

  • 也可以通過路由的方式setValue:forKeyPath:valueForKeyPath:

2、通過KVC訪問和修改私有變量

在日常開發(fā)中装盯,對于類的私有屬性坷虑,在外部定義的對象,是無法直接訪問私有屬性的埂奈,但是對于KVC而言迄损,一個對象沒有自己的隱私,所以可以通過KVC修改和訪問任何私有屬性

3账磺、多值操作(model和字典互轉)

model和字典的轉換可以通過下面兩個KVC的API實現(xiàn)

//字典轉模型
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

//模型轉字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

4芹敌、修改一些系統(tǒng)空間的內(nèi)部屬性

在日常開發(fā)中痊远,我們知道,很多UI控件都是在其內(nèi)部由多個UI空間組合而成氏捞,這些內(nèi)部控件蘋果并沒有提供訪問的API碧聪,但是使用KVC可以解決這個問題,常用的就是自定義tabbar液茎、個性化UITextField中的placeHolderText

5逞姿、用KVC實現(xiàn)高階消息傳遞

在對容器類使用KVC時,valueForKey:將會被傳遞給容器中的每一個對象捆等,而不是對容器本身進行操作哼凯,結果會被添加到返回的容器中,這樣楚里,可以很方便的操作集合 來返回 另一個集合

如下所示

//KVC實現(xiàn)高階消息傳遞
- (void)transmitMsg{
    NSArray *arrStr = @[@"english", @"franch", @"chinese"];
    NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"];

    for (NSString *str in arrCapStr) {
        NSLog(@"%@", str);
    }

    NSArray *arrCapStrLength = [arrCapStr valueForKeyPath:@"capitalizedString.length"];
    for (NSNumber *length in arrCapStrLength) {
        NSLog(@"%ld", (long)length.integerValue);
    }
}

//********打印結果********
2020-10-27 11:33:43.377672+0800 CJLCustom[60035:6380757] English
2020-10-27 11:33:43.377773+0800 CJLCustom[60035:6380757] Franch
2020-10-27 11:33:43.377860+0800 CJLCustom[60035:6380757] Chinese
2020-10-27 11:33:43.378233+0800 CJLCustom[60035:6380757] 7
2020-10-27 11:33:43.378327+0800 CJLCustom[60035:6380757] 6
2020-10-27 11:33:43.378417+0800 CJLCustom[60035:6380757] 7

附錄

自定義KVC的完整代碼見Github-CustomKVC_KVO,喜歡的可以點個?

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末猎贴,一起剝皮案震驚了整個濱河市班缎,隨后出現(xiàn)的幾起案子肺稀,更是在濱河造成了極大的恐慌趟径,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件早敬,死亡現(xiàn)場離奇詭異趁耗,居然都是意外死亡沉唠,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門苛败,熙熙樓的掌柜王于貴愁眉苦臉地迎上來满葛,“玉大人,你說我怎么就攤上這事罢屈∴秩停” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵缠捌,是天一觀的道長锄贷。 經(jīng)常有香客問我,道長曼月,這世上最難降的妖魔是什么谊却? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮哑芹,結果婚禮上炎辨,老公的妹妹穿的比我還像新娘。我一直安慰自己聪姿,他們只是感情好蹦魔,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布激率。 她就那樣靜靜地躺著,像睡著了一般勿决。 火紅的嫁衣襯著肌膚如雪乒躺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天低缩,我揣著相機與錄音嘉冒,去河邊找鬼。 笑死咆繁,一個胖子當著我的面吹牛讳推,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播玩般,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼银觅,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了坏为?” 一聲冷哼從身側響起究驴,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎匀伏,沒想到半個月后洒忧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡够颠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年熙侍,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片履磨。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蛉抓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出剃诅,到底是詐尸還是另有隱情芝雪,我是刑警寧澤,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布综苔,位于F島的核電站惩系,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏如筛。R本人自食惡果不足惜堡牡,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望杨刨。 院中可真熱鬧晤柄,春花似錦、人聲如沸妖胀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至爬坑,卻和暖如春纠屋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盾计。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工售担, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人署辉。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓族铆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哭尝。 傳聞我的和親對象是個殘疾皇子哥攘,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

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