iOS KVO實(shí)現(xiàn)原理

相關(guān)API以及用法

翻開(kāi)蘋(píng)果的觀察者api,實(shí)現(xiàn)很簡(jiǎn)潔接口也很少穴豫,定義在NSKeyValueObserving.h里面

@interface NSObject(NSKeyValueObserverRegistration)

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

@interface NSObject(NSKeyValueObserving)

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

@end

@interface NSObject(NSKeyValueObserverNotification)

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

@end

如上,是通過(guò)給NSObject添加分類(lèi)實(shí)現(xiàn)的:

  • NSKeyValueObserverRegistration注冊(cè)觀察者
  • observeValueForKeyPath觀察者回調(diào)
  • NSKeyValueObserverNotification觀察者通知

使用起來(lái)也很簡(jiǎn)單,我們定義一個(gè)Person類(lèi)捉蚤,添加三個(gè)屬性a、b炼七、c

@interface Person : NSObject

@property (nonatomic, assign) NSInteger a;
@property (nonatomic, assign) NSInteger b;
@property (nonatomic, assign) NSInteger c;

@end

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.person addObserver:self
                  forKeyPath:@"a"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];
    [self.person addObserver:self
                  forKeyPath:@"b"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];
    [self.person addObserver:self
                  forKeyPath:@"c"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];
    self.person.a = 10;
    self.person.b = 5;
    self.person.c = 2;
    [self.person removeObserver:self forKeyPath:@"a"];
    [self.person removeObserver:self forKeyPath:@"b"];
    [self.person removeObserver:self forKeyPath:@"c"];
    NSLog(@"person對(duì)象觀察者全部移除");
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context {
    NSLog(@"%@屬性變化:%@", keyPath, change);
}

- (Person *)person {
    if (!_person) {
        _person = [[Person alloc] init];
    }
    return _person;
}

初始值都是0缆巧,控制臺(tái)輸出如下

2021-08-10 21:55:30.100992+0800 test[19703:48456267] a屬性變化:{
    kind = 1;
    new = 10;
    old = 0;
}
2021-08-10 21:55:30.101123+0800 test[19703:48456267] b屬性變化:{
    kind = 1;
    new = 5;
    old = 0;
}
2021-08-10 21:55:30.101235+0800 test[19703:48456267] c屬性變化:{
    kind = 1;
    new = 2;
    old = 0;
}
2021-08-10 21:55:30.101336+0800 test[19703:48456267] person對(duì)象觀察者全部移除

我們?cè)谌缟衔恢么蛏蠑帱c(diǎn),然后在控制臺(tái)打印person的isa指針豌拙,輸出如下

(lldb) po self.person->isa
NSKVONotifying_Person

(lldb) po self.person->isa
Person

可以看到陕悬,對(duì)象的觀察者沒(méi)有完全移除的時(shí)候isa指向NSKVONotifying_Person,完全移除之后isa指向Person

實(shí)現(xiàn)原理

蘋(píng)果的官方文檔有KVO實(shí)現(xiàn)原理的描述按傅,很遺憾KVO的源碼沒(méi)有開(kāi)源捉超,不過(guò)通過(guò)上面在控制臺(tái)的打印結(jié)果,也能側(cè)面印證底層實(shí)現(xiàn)

當(dāng)對(duì)象的屬性被添加觀察者時(shí)唯绍,一個(gè)繼承自該對(duì)象所屬類(lèi)的子類(lèi)被動(dòng)態(tài)創(chuàng)建拼岳,接著修改該對(duì)象的isa指針,使其指向該子類(lèi)况芒,并重寫(xiě)了被觀察屬性的setter方法惜纸,依次調(diào)用willChangeValueForKey、父類(lèi)的setter方法绝骚、didChangeValueForKey耐版,最后會(huì)調(diào)用到該對(duì)象的observeValueForKeyPath方法,不僅如此蘋(píng)果還修改了class方法的返回值使其返回對(duì)象原本的類(lèi)皮壁,目的是隱藏觀察者的底層實(shí)現(xiàn)椭更,當(dāng)對(duì)象屬性的觀察者被全部移除之后,對(duì)象的isa指針會(huì)被修正蛾魄,重新指向原本的類(lèi)

觀察者相關(guān)的crash

  • 添加次數(shù)多于移除次數(shù)虑瀑,當(dāng)監(jiān)聽(tīng)者釋放后,觸發(fā)observeValueForKeyPath時(shí)crash
  • 添加次數(shù)少于移除次數(shù)指直接crash
  • 觀察者沒(méi)有實(shí)現(xiàn)observeValueForKeyPath時(shí)直接crash

如上幾個(gè)crash蘋(píng)果完全有能力避免他們發(fā)生滴须,但是為什么蘋(píng)果沒(méi)有做這件事呢舌狗,因?yàn)樗恢烙脩舻恼嬲鈭D,蘋(píng)果期望在調(diào)試階段就暴露可能有問(wèn)題的邏輯扔水,讓其直接crash痛侍,然而事與愿違,通常我們是成對(duì)調(diào)用的,但是由于某種原因主届,導(dǎo)致添加和移除的次數(shù)無(wú)法匹配赵哲,最終導(dǎo)致線上大量的crash,所以crash防護(hù)需求就誕生了橡庞,沒(méi)有什么問(wèn)題是添加一個(gè)中間層解決不了的,如果有,那就再添加一層
在添加或移除觀察者之前插入一層數(shù)據(jù)結(jié)構(gòu)用于存儲(chǔ)次數(shù)再菊,比如哈希表

添加觀察者時(shí):控制只添加一次
移除觀察者時(shí):控制只移除一次
觀察鍵值改變時(shí):控制消息分發(fā)到觀察者上

為了避免被觀察者提前被釋放后泛豪,觸發(fā)observeValueForKeyPath時(shí)的crash臀叙,需要hook一下NSObject的dealloc方法劝萤,在對(duì)象dealloc函數(shù)調(diào)用之前,移除相關(guān)觀察者。

還是有點(diǎn)復(fù)雜阔涉!有沒(méi)有一種方案既可以實(shí)現(xiàn)安全性又不用hook系統(tǒng)方法呢?

實(shí)現(xiàn)安全的觀察者

一郭毕、API

干脆用runtime庫(kù)自己實(shí)現(xiàn)一個(gè)安全的觀察者扳肛,根據(jù)其實(shí)現(xiàn)原理金拒,仿照系統(tǒng)api,通過(guò)分類(lèi)的方式添加一個(gè)中間層,作者寫(xiě)了一個(gè)工具症副,下面講述下實(shí)現(xiàn)原理沮明,如下接口類(lèi)似系統(tǒng)api酱畅,只是把回調(diào)函數(shù)寫(xiě)成了block

/* - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
*/
typedef void (^SK_ObservedValueChanged) (id object, NSString *keyPath ,id oldValue, id newValue);

@interface NSObject (SafeKVO)

/// 添加安全觀察者
/// @param observer 觀察者
/// @param keyPath 屬性鏈
/// @param change 回調(diào)
- (void)sk_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath observeValueChanged:(SK_ObservedValueChanged)change;

/// 移除觀察者
/// @param observer 觀察者
/// @param keyPath 屬性鏈
- (void)sk_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

同時(shí)去掉了context和options參數(shù)
原因是context參數(shù)用于給同一個(gè)屬性添加同一個(gè)觀察者同時(shí)代入上下文吁峻,回調(diào)時(shí)用于反解參數(shù),基本沒(méi)啥場(chǎng)景,options參數(shù)用于描述屬性改變的類(lèi)型,通常只用new和change,工具已經(jīng)實(shí)現(xiàn)這兩種類(lèi)型匆笤,綜上省略了context和options參數(shù)

二、安全數(shù)據(jù)模型

用于存儲(chǔ):觀察者、被觀察者、屬性鏈箩艺、觀察者回調(diào)到關(guān)聯(lián)對(duì)象

@interface SafeKVOModel : NSObject

@property (nonatomic, weak) NSObject *observer;// 觀察者
@property (nonatomic, weak) NSObject *observed;// 被觀察者
@property (nonatomic, copy) NSString *keyPath;// 屬性鏈
@property (nonatomic, copy) SK_ObservedValueChanged change; // 觀察者回調(diào)
@property (nonatomic, strong) NSObject *oldValue;// 被觀察屬性原值

@end

@implementation SafeKVOModel

- (instancetype)initWithObserver:(NSObject *)observer observed:(NSObject *)observed forKeyPath:(NSString *)keyPath change:(SK_ObservedValueChanged)change {
    if (self = [super init]) {
        self.observer = observer;
        self.observed = observed;
        self.keyPath = keyPath;
        self.change = change;
    }
    return self;
}

@end
三居凶、工具函數(shù)

通過(guò)屬性名生成setterSEL

static forceInline SEL sk_setterSelectorFromPropertyName(NSString *propertyName) {
    if (propertyName.length <= 0)
        return nil;
    NSString *setterString = [NSString stringWithFormat:@"set%@%@:", [[propertyName substringToIndex:1] uppercaseString], [propertyName substringFromIndex:1]];
    return NSSelectorFromString(setterString);
}

通過(guò)setter方法名生成屬性名

static forceInline NSString *sk_propertyNameFromSetterString(NSString *setterString) {
    if (setterString.length <= 0 || ![setterString hasPrefix: @"set"] || ![setterString hasSuffix: @":"])
        return nil;
    NSRange range = NSMakeRange(3, setterString.length - 4);
    NSString *propertyName = [setterString substringWithRange:range];
    propertyName = [propertyName stringByReplacingCharactersInRange: NSMakeRange(0, 1) withString:[[propertyName substringToIndex: 1] lowercaseString]];
    return propertyName;
}

核心方法替饿,子類(lèi)重寫(xiě)setter方法贸典,內(nèi)部調(diào)用父類(lèi)的setter方法修改值盛垦,注意系統(tǒng)的是現(xiàn)實(shí)在調(diào)用父類(lèi)setter方法前后分別調(diào)用willChangeValueForKeydidChangeValueForKey方法,然后通過(guò)observeValueForKeyPath方法回調(diào)到父類(lèi)瓤漏,而我們這里直接通過(guò)自定義的block回調(diào),因此不用調(diào)用上面兩個(gè)方法

static forceInline void sk_setter(id self, SEL _cmd, id newValue) {
    @synchronized (self) {
        NSString *propertyName = sk_propertyNameFromSetterString(NSStringFromSelector(_cmd));
        NSParameterAssert(propertyName);
        if (!propertyName)
            return;
        
        NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)kSafeKVOAssiociateObservers);
        for (SafeKVOModel *model in observers) {
            if ([model.keyPath containsString:propertyName])
                model.oldValue = [model.observed valueForKeyPath:model.keyPath];
        }
        // 調(diào)用父類(lèi)的set方法
        struct objc_super superClass = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self))
        };
        void (*superSetter)(void *, SEL, id) = (void *)objc_msgSendSuper;
        superSetter(&superClass, _cmd, newValue);
        
        // 觀察者回調(diào)
        for (SafeKVOModel *model in observers) {
            // 觀察者未釋放才需回調(diào)
            if ([model.keyPath containsString:propertyName] && model.observer) {
                model.change(model.observed, model.keyPath, model.oldValue, [model.observed valueForKeyPath:model.keyPath]);
                model.oldValue = nil;
            }
        }
    }
}

返回父類(lèi)的Class用于重寫(xiě)子類(lèi)的Class方法

static forceInline Class sk_class(id self) {
    return class_getSuperclass(object_getClass(self));
}

核心方法颊埃,用于動(dòng)態(tài)創(chuàng)建子類(lèi)并注冊(cè)到運(yùn)行時(shí)環(huán)境

static forceInline Class createSafeKVOClass(id object) {
    // 獲取以SafeKVONotifying_為前綴拼接類(lèi)名的子類(lèi)
    Class observedClass = object_getClass(object);
    NSString *className = NSStringFromClass(observedClass);
    NSString *subClassName = [kSafeKVOClassPrefix stringByAppendingString:className];
    Class subClass = NSClassFromString(subClassName);
    // 運(yùn)行時(shí)已經(jīng)加載該類(lèi)則直接返回
    if (subClass)
        return subClass;
    
    Class originalClass = object_getClass(object);
    // 分配類(lèi)和原類(lèi)的內(nèi)存
    subClass = objc_allocateClassPair(originalClass, subClassName.UTF8String, 0);
    // 修改class實(shí)現(xiàn)蔬充,返回父類(lèi)Class
    Method classMethod = class_getInstanceMethod(originalClass, @selector(class));
    const char *types = method_getTypeEncoding(classMethod);
    class_addMethod(subClass, @selector(class), (IMP)sk_class, types);
    // 注冊(cè)類(lèi)到運(yùn)行時(shí)環(huán)境
    objc_registerClassPair(subClass);
    return subClass;
}

判斷對(duì)象是否能響應(yīng)傳入的SEL

static forceInline BOOL objectHasSelector(id object, SEL selector) {
    BOOL result = NO;
    unsigned int count = 0;
    Class observedClass = object_getClass(object);
    Method *methods = class_copyMethodList(observedClass, &count);
    for (NSInteger i = 0; i < count; i++) {
        SEL sel = method_getName(methods[i]);
        if (sel == selector) {
            result = YES;
            break;
        }
    }
    free(methods);
    return result;
}
四、API實(shí)現(xiàn)

添加安全觀察者班利,此處有個(gè)易忽略點(diǎn)就是keyPath的處理饥漫,需要通過(guò)屬性鏈中的類(lèi)一一生成其子類(lèi),因?yàn)閗eyPath中的任意節(jié)點(diǎn)變化都有可能導(dǎo)致最終的屬性變化罗标,都是我們監(jiān)聽(tīng)的范圍

- (void)sk_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath observeValueChanged:(SK_ObservedValueChanged)change {
    @synchronized (self) {
        NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge void *)kSafeKVOAssiociateObservers);
        for (SafeKVOModel *observerModel in observers) {
            // 已添加過(guò)同一個(gè)觀察者庸队,無(wú)需重復(fù)添加
            if (observerModel.observer == observer && observerModel.observed == self && [observerModel.keyPath isEqualToString:keyPath]) {
                return;
            }
        }
        // 通過(guò)keyPath依次執(zhí)行->創(chuàng)建子類(lèi)重寫(xiě)set方法操作
        NSArray *keys = [keyPath componentsSeparatedByString:@"."];
        NSInteger index = 0;
        id object = self;
        while (index < keys.count) {
            SEL setterSelector = sk_setterSelectorFromPropertyName(keys[index]);
            Method setterMethod = class_getInstanceMethod([object class], setterSelector);
            NSParameterAssert(setterMethod);
            if (!setterMethod) {
                return;
            }
            id nextObject = [object valueForKey:keys[index]];
            Class observedClass = object_getClass(object);
            NSString *className = NSStringFromClass(observedClass);
            if (![className hasPrefix:kSafeKVOClassPrefix]) {
                // 創(chuàng)建子類(lèi)并修改本類(lèi)isa指針使其指向子類(lèi)
                observedClass = createSafeKVOClass(object);
                object_setClass(object, observedClass);
            }
            if (!objectHasSelector(object, setterSelector)) {
                // 重寫(xiě)set方法在方法里調(diào)用父類(lèi)的set方法并通過(guò)block回調(diào)到上層,以完成監(jiān)聽(tīng)過(guò)程
                const char *types = method_getTypeEncoding(setterMethod);
                class_addMethod(observedClass, setterSelector, (IMP)sk_setter, types);
            }
            // 添加監(jiān)聽(tīng)者到類(lèi)的關(guān)聯(lián)對(duì)象數(shù)組
            observers = objc_getAssociatedObject(object, (__bridge void *)kSafeKVOAssiociateObservers);
            if (!observers) {
                observers = [NSMutableArray array];
                objc_setAssociatedObject(object, (__bridge void *)kSafeKVOAssiociateObservers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            }
            SafeKVOModel *kvoModel = [[SafeKVOModel alloc] initWithObserver:observer observed:self forKeyPath:keyPath change:change];
            [observers addObject:kvoModel];
            
            index++;
            if (index < keys.count) {
                object = nextObject;
            }
        }
    }
}

遍歷清除觀察者闯割,若已經(jīng)清空則修正對(duì)象的isa指針

- (void)sk_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    @synchronized (self) {
        NSArray *keys = [keyPath componentsSeparatedByString:@"."];
        NSInteger index = 0;
        id object = self;
        while (index < keys.count) {
            SafeKVOModel *modelRemoved = nil;
            NSMutableArray *observers = objc_getAssociatedObject(object, (__bridge void *)kSafeKVOAssiociateObservers);
            for (SafeKVOModel *model in observers) {
                if (model.observer == observer && model.observed == self && [model.keyPath isEqualToString:keyPath]) {
                    modelRemoved = model;
                    break;
                }
            }
            if (modelRemoved) {
                [observers removeObject:modelRemoved];
                if (!observers.count) {
                    object_setClass(object, [object class]);
                }
            } else {
                object_setClass(object, [object class]);
            }
            object = [object valueForKey:keys[index]];
            index++;
        }
    }
}

總結(jié)

本工具支持了多線程彻消,同時(shí)通過(guò)runtime和關(guān)聯(lián)對(duì)象實(shí)現(xiàn)了安全觀察者,解決了觀察者添加宙拉、移除宾尚、回調(diào)的各種crash,注意谢澈,本代碼還沒(méi)有經(jīng)過(guò)大量測(cè)試煌贴,如有需要,請(qǐng)務(wù)必反復(fù)測(cè)試之后再應(yīng)用于項(xiàng)目中
下載鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末锥忿,一起剝皮案震驚了整個(gè)濱河市牛郑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌敬鬓,老刑警劉巖淹朋,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笙各,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡瑞你,警方通過(guò)查閱死者的電腦和手機(jī)酪惭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)者甲,“玉大人春感,你說(shuō)我怎么就攤上這事÷哺祝” “怎么了鲫懒?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)刽辙。 經(jīng)常有香客問(wèn)我窥岩,道長(zhǎng),這世上最難降的妖魔是什么宰缤? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任颂翼,我火速辦了婚禮,結(jié)果婚禮上慨灭,老公的妹妹穿的比我還像新娘朦乏。我一直安慰自己,他們只是感情好氧骤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布呻疹。 她就那樣靜靜地躺著,像睡著了一般筹陵。 火紅的嫁衣襯著肌膚如雪刽锤。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,554評(píng)論 1 305
  • 那天朦佩,我揣著相機(jī)與錄音并思,去河邊找鬼。 笑死吕粗,一個(gè)胖子當(dāng)著我的面吹牛纺荧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播颅筋,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼宙暇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了议泵?” 一聲冷哼從身側(cè)響起占贫,我...
    開(kāi)封第一講書(shū)人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎先口,沒(méi)想到半個(gè)月后型奥,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體瞳收,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年厢汹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了螟深。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡烫葬,死狀恐怖界弧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情搭综,我是刑警寧澤垢箕,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站兑巾,受9級(jí)特大地震影響条获,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蒋歌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一帅掘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧堂油,春花似錦锄开、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)头遭。三九已至寓免,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間计维,已是汗流浹背袜香。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鲫惶,地道東北人蜈首。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像欠母,于是被迫代替她去往敵國(guó)和親欢策。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

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