iOS開發(fā)·KVO用法,原理與底層實現(xiàn): runtime模擬實現(xiàn)KVO監(jiān)聽機制(Blcok及Delgate方式)

摘要:這篇文章首先介紹KVO的基本用法,接著探究 KVO (Key-Value Observing) 實現(xiàn)機制劫流,并利用 runtime 模擬實現(xiàn) KVO的監(jiān)聽機制:一種Block方式回調(diào),一種Delegate回調(diào)丛忆。同時祠汇,本文也會總結(jié)KVO實現(xiàn)過程中與 runtime 相關的API用法。

作為一個開發(fā)者熄诡,有一個學習的氛圍跟一個交流圈子特別重要這是一個我的iOS交流群:776598941可很,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題凰浮、面試經(jīng)驗我抠,討論技術, 大家一起交流學習成長袜茧!

1. KVO理論基礎

1.1 KVO的基本用法

步驟

? 注冊觀察者菜拓,實施監(jiān)聽

[self.person addObserver:selfforKeyPath:@"age"options:NSKeyValueObservingOptionNewcontext:nil];

? 回調(diào)方法,在這里處理屬性發(fā)生的變化

- (void)observeValueForKeyPath:(NSString*)keyPath? ? ? ? ? ? ? ? ? ? ? ofObject:(id)object? ? ? ? ? ? ? ? ? ? ? ? change:(NSDictionary *)change? ? ? ? ? ? ? ? ? ? ? context:(void*)context {//...實現(xiàn)監(jiān)聽處理}

? 移除觀察者

[self removeObserver:self forKeyPath:@“age"];

綜合例子

//添加觀察者_person = [[Person alloc] init];[_person addObserver:selfforKeyPath:@"age"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOldcontext:nil];

//KVO回調(diào)方法- (void)observeValueForKeyPath:(NSString*)keyPath? ? ? ? ? ? ? ? ? ? ? ofObject:(id)object? ? ? ? ? ? ? ? ? ? ? ? change:(NSDictionary *)change? ? ? ? ? ? ? ? ? ? ? context:(void*)context {NSLog(@"%@對象的%@屬性改變了笛厦,change字典為:%@",object,keyPath,change);NSLog(@"屬性新值為:%@",change[NSKeyValueChangeNewKey]);NSLog(@"屬性舊值為:%@",change[NSKeyValueChangeOldKey]);}

//移除觀察者- (void)dealloc{? ? [self.personremoveObserver:selfforKeyPath:@"age"];}

利用了KVO實現(xiàn)鍵值監(jiān)聽的?第三方框架

AFNetworking?纳鼎,?MJRresh

1.2 KVO的實現(xiàn)原理

KVO 是 Objective-C 對?觀察者模式?(Observer Pattern)的實現(xiàn)。當被觀察對象的某個屬性發(fā)生更改時递递,觀察者對象會獲得通知喷橙。有意思的是,你不需要給被觀察的對象添加任何額外代碼登舞,就能使用 KVO 贰逾。這是怎么做到的?

KVO 的實現(xiàn)也依賴于 Objective-C 強大的 Runtime 菠秒。Apple 的文檔有簡單提到過 KVO 的實現(xiàn)疙剑。Apple 的文檔唯一有用的信息是:?被觀察對象的 isa 指針會指向一個中間類氯迂,而不是原來真正的類?。Apple 并不希望過多暴露 KVO 的實現(xiàn)細節(jié)言缤。

不過嚼蚀,要是你用 runtime 提供的方法去深入挖掘,所有被掩蓋的細節(jié)都會原形畢露管挟。Mike Ash 早在 2009 年就做了這么個探究轿曙,了解更多點這里。

KVO 的實現(xiàn):

當你觀察一個對象時僻孝,一個?新的類?會動態(tài)被創(chuàng)建导帝。這個類?繼承?自該對象的原本的類,并?重寫了被觀察屬性的?setter?方法穿铆。自然您单,重寫的?setter?方法會負責在調(diào)用原?setter?方法之前和之后,通知所有觀察對象值的更改荞雏。最后把這個對象的?isa?指針 ( isa 指針告訴 Runtime 系統(tǒng)這個對象的類是什么 ) 指向這個新創(chuàng)建的?子類?虐秦,對象就神奇的變成了新創(chuàng)建的?子類?的實例。

這個?中間類?凤优,繼承自?原本的那個類?悦陋。不僅如此,Apple 還重寫了?-class?方法别洪,企圖欺騙我們這個類沒有變叨恨,就是原本那個類。更具體的信息挖垛,去跑一下 Mike Ash 的那篇文章里的代碼就能明白,這里就不再重復秉颗。

1.3 KVO的不足

KVO 很強大痢毒,沒錯。知道它內(nèi)部實現(xiàn)蚕甥,或許能幫助更好地使用它哪替,或在它出錯時更方便調(diào)試。但官方實現(xiàn)的 KVO 提供的 API 實在不怎么樣菇怀。

比如凭舶,你只能通過重寫?-observeValueForKeyPath:ofObject:change:context:?方法來獲得通知。想要提供自定義的 selector 爱沟,不行帅霜;想要傳一個 block ,門都沒有呼伸。而且你還要處理父類的情況 - 父類同樣監(jiān)聽同一個對象的同一個屬性身冀。但有時候,你不知道父類是不是對這個消息有興趣。雖然 context 這個參數(shù)就是干這個的搂根,也可以解決這個問題 - 在?-addObserver:forKeyPath:options:context:?傳進去一個父類不知道的 context珍促。但總覺得框在這個 API 的設計下,代碼寫的很別扭剩愧。至少至少猪叙,也應該支持 block 吧。

有不少人都覺得官方 KVO 不好使的仁卷。Mike Ash 的?Key-Value Observing Done Right?沐悦,以及獲得不少分享討論的?KVO Considered Harmful?都把 KVO 拿出來吊打了一番。所以在實際開發(fā)中 KVO 使用的情景并不多五督,更多時候還是用 Delegate 或 NotificationCenter藏否。

2. Block實現(xiàn)KVO

2.1 模擬實現(xiàn)

注意:以下都是同一個文件:NSObject+Block_KVO.m中寫的

導入頭文件,并定義兩個靜態(tài)變量

#import"NSObject+Block_KVO.h"#import#import//as prefix string of kvo classstaticNSString*constkCMkvoClassPrefix_for_Block =@"CMObserver_";staticNSString*constkCMkvoAssiociateObserver_for_Block =@"CMAssiociateObserver";

暴露給調(diào)用者為被觀察對象添加KVO方法

- (void)CM_addObserver:(NSObject*)observer forKey:(NSString*)key withBlock:(CM_ObservingHandler)observedHandler{//step 1 get setter method, if not, throw exceptionSEL setterSelector =NSSelectorFromString(setterForGetter(key));? ? Method setterMethod = class_getInstanceMethod([selfclass], setterSelector);if(!setterMethod) {@throw[NSExceptionexceptionWithName:NSInvalidArgumentExceptionreason: [NSStringstringWithFormat:@"unrecognized selector sent to instance %@",self] userInfo:nil];return;? ? }//自己的類作為被觀察者類Class observedClass = object_getClass(self);NSString* className =NSStringFromClass(observedClass);//如果被監(jiān)聽者沒有CMObserver_充包,那么判斷是否需要創(chuàng)建新類if(![className hasPrefix: kCMkvoClassPrefix_for_Block]) {//【代碼①】observedClass = [selfcreateKVOClassWithOriginalClassName: className];//【API注解①】object_setClass(self, observedClass);? ? }//add kvo setter method if its class(or superclass)hasn't implement setterif(![selfhasSelector: setterSelector]) {constchar* types = method_getTypeEncoding(setterMethod);//【代碼②】class_addMethod(observedClass, setterSelector, (IMP)KVO_setter, types);? ? }//add this observation info to saved new observer//【代碼③】CM_ObserverInfo_for_Block* newInfo = [[CM_ObserverInfo_for_Blockalloc] initWithObserver: observer forKey: key observeHandler: observedHandler];//【代碼④】【API注解③】NSMutableArray* observers = objc_getAssociatedObject(self, (__bridgevoid*)kCMkvoAssiociateObserver_for_Block);if(!observers) {? ? ? ? observers = [NSMutableArrayarray];? ? ? ? objc_setAssociatedObject(self, (__bridgevoid*)kCMkvoAssiociateObserver_for_Block, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);? ? }? ? [observers addObject: newInfo];}

其中【代碼①】的意思是副签,被觀察的類如果是被觀察對象本來的類,那么基矮,就要專門依據(jù)本來的類新建一個新的?子類?淆储,區(qū)分是否這個子類的標記是帶有?kCMkvoClassPrefix_for_Block?的前綴。怎樣新建一個?子類?家浇?代碼如下所示:

- (Class)createKVOClassWithOriginalClassName: (NSString*)className{NSString* kvoClassName = [kCMkvoClassPrefix stringByAppendingString: className];? ? Class observedClass =NSClassFromString(kvoClassName);if(observedClass) {returnobservedClass; }//創(chuàng)建新類本砰,并且添加CMObserver_為類名新前綴Class originalClass = object_getClass(self);//【API注解②】Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String,0);//獲取監(jiān)聽對象的class方法實現(xiàn)代碼,然后替換新建類的class實現(xiàn)Method classMethod = class_getInstanceMethod(originalClass,@selector(class));constchar* types = method_getTypeEncoding(classMethod);? ? class_addMethod(kvoClass,@selector(class), (IMP)kvo_Class, types);? ? objc_registerClassPair(kvoClass);returnkvoClass;}

另外【代碼②】的意思是钢悲,將原來的setter方法替換一個新的setter方法(這就是runtime的黑魔法点额,Method Swizzling)。那么新的setter方法又是什么呢莺琳?如下所示:

#pragma mark -- Override setter and getter MethodsstaticvoidKVO_setter(idself, SEL _cmd,idnewValue){NSString* setterName =NSStringFromSelector(_cmd);NSString* getterName = getterForSetter(setterName);if(!getterName) {@throw[NSExceptionexceptionWithName:NSInvalidArgumentExceptionreason: [NSStringstringWithFormat:@"unrecognized selector sent to instance %p",self] userInfo:nil];return;? ? }idoldValue = [selfvalueForKey: getterName];structobjc_super superClass = {? ? ? ? .receiver =self,? ? ? ? .super_class = class_getSuperclass(object_getClass(self))? ? };? ? ? ? [selfwillChangeValueForKey: getterName];void(*objc_msgSendSuperKVO)(void*, SEL,id) = (void*)objc_msgSendSuper;? ? objc_msgSendSuperKVO(&superClass, _cmd, newValue);? ? [selfdidChangeValueForKey: getterName];//獲取所有監(jiān)聽回調(diào)對象進行回調(diào)NSMutableArray* observers = objc_getAssociatedObject(self, (__bridgeconstvoid*)kCMkvoAssiociateObserver_for_Block);for(CM_ObserverInfo_for_Block* infoinobservers) {if([info.key isEqualToString: getterName]) {dispatch_async(dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{? ? ? ? ? ? ? ? info.handler(self, getterName, oldValue, newValue);? ? ? ? ? ? });? ? ? ? }? ? }}

【代碼③】是新建一個觀察者類还棱。這個類的實現(xiàn)寫在同一個class,相當于導入一個類:CM_ObserverInfo_for_Block惭等。這個類的作用是觀察者珍手,并在初始化的時候負責調(diào)用者傳過來的Block回調(diào)。如下辞做,?self.handler = handler;?即負責回調(diào)琳要。

@interfaceCM_ObserverInfo_for_Block:NSObject@property(nonatomic,weak)NSObject* observer;@property(nonatomic,copy)NSString* key;@property(nonatomic,copy)CM_ObservingHandlerhandler;@end@implementationCM_ObserverInfo_for_Block- (instancetype)initWithObserver: (NSObject*)observer forKey: (NSString*)key observeHandler: (CM_ObservingHandler)handler{if(self= [superinit]) {? ? ? ? ? ? ? ? _observer = observer;self.key = key;self.handler = handler;? ? }returnself;}@end

【代碼④】的作用是,以及已知的“屬性名”秤茅,類型為NSString的靜態(tài)變量?kCMkvoAssiociateObserver_for_Block?來獲取這個“屬性”觀察者數(shù)組(這個其實并不是真正意義的屬性稚补,屬于runtime關聯(lián)對象的知識范疇,可理解成?觀察者數(shù)組?這樣一個屬性)嫂伞。其中孔厉,關于?(__bridge void *)?的知識后面會講到拯钻。

調(diào)用者:利用上面的API為被觀察者添加KVO

VC調(diào)用API

#import"NSObject+Block_KVO.h"http://...........- (void)viewDidLoad {? ? [superviewDidLoad];? ? ? ? ObservedObject * object = [ObservedObject new];? ? object.observedNum = @8;#pragma mark - Observed By Block[objectCM_addObserver:selfforKey:@"observedNum"withBlock: ^(idobservedObject,NSString*observedKey,idoldValue,idnewValue) {NSLog(@"Value had changed yet with observing Block");NSLog(@"oldValue---%@",oldValue);NSLog(@"newValue---%@",newValue);? ? }];? ? ? ? object.observedNum = @10;}

2.2 runtime關鍵API解析

【API注解①】:object_setClass

我們可以在運行時創(chuàng)建新的class,這個特性用得不多撰豺,但其實它還是很強大的粪般。你能通過它創(chuàng)建新的子類,并添加新的方法污桦。

但這樣的一個子類有什么用呢亩歹?別忘了Objective-C的一個關鍵點:object內(nèi)部有一個叫做isa的變量指向它的class。這個變量可以被改變凡橱,而不需要重新創(chuàng)建小作。然后就可以添加新的ivar和方法了〖诠常可以通過以下命令來修改一個object的class

object_setClass(myObject, [MySubclassclass]);

這可以用在Key Value Observing顾稀。當你開始observing an object時,Cocoa會創(chuàng)建這個object的class的subclass坝撑,然后將這個object的isa指向新創(chuàng)建的subclass静秆。

【API注解②】:objc_allocateClassPair

objc_allocateClassPair(Class_Nullable superclass,constchar * _Nonnullname,? ? ? ? ? ? ? ? ? ? ? ? size_t extraBytes)

看起來一切都很簡單,運行時創(chuàng)建類只需要三步: 1巡李、為"class pair"分配空間(使用?objc_allocateClassPair?). 2抚笔、為創(chuàng)建的類添加方法和成員(上例使用?class_addMethod?添加了一個方法)。 3侨拦、注冊你創(chuàng)建的這個類殊橙,使其可用(使用?objc_registerClassPair?)。

為什么這里1和3都說到pair狱从,我們知道pair的中文意思是一對膨蛮,這里也就是一對類,那這一對類是誰呢矫夯?他們就是Class鸽疾、MetaClass。

需要配置的參數(shù)為: 1训貌、第一個參數(shù):作為新類的超類,或用Nil來創(chuàng)建一個新的根類。

2冒窍、第二個參數(shù):新類的名稱

3递沪、第三個參數(shù):一般傳0

【API注解③】:(__bridge void *)

在 ARC 有效時,通過?(__bridge void *)?轉(zhuǎn)換 id 和 void * 就能夠相互轉(zhuǎn)換综液。為什么轉(zhuǎn)換款慨?這是因為?objc_getAssociatedObject?的參數(shù)要求的。先看一下它的API:

objc_getAssociatedObject(id _Nonnullobject,constvoid* _Nonnull key)

可以知道谬莹,這個“屬性名”的key是必須是一個?void *?類型的參數(shù)檩奠。所以需要轉(zhuǎn)換桩了。關于這個轉(zhuǎn)換,下面給一個轉(zhuǎn)換的例子:

idobj = [[NSObjectalloc] init];void*p = (__bridgevoid*)obj;ido = (__bridgeid)p;

關于這個轉(zhuǎn)換可以了解更多:?ARC 類型轉(zhuǎn)換:顯示轉(zhuǎn)換 id 和 void *

當然埠戳,如果不通過轉(zhuǎn)換使用這個API井誉,就需要這樣使用:

方式1:

objc_getAssociatedObject(self, @"AddClickedEvent");

方式2:

staticconstvoid*registerNibArrayKey = ?isterNibArrayKey;

NSMutableArray *array= objc_getAssociatedObject(self, registerNibArrayKey);

方式3:

staticconstcharMJErrorKey ='\0';

objc_getAssociatedObject(self,&MJErrorKey);

方式4:

+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property{? ? MJProperty *propertyObj = objc_getAssociatedObject(self,property);//省略}

其中?objc_property_t?是runtime的類型

typedefstructobjc_property *objc_property_t;

2.3 runtime其它API解析

剩下的就是runtime的比較常見API了,這里就不按照上面代碼的順序的講解了整胃。這里只做按runtime的知識范疇將這些API做一個分類:

runtime:關聯(lián)對象相關API

objc_getAssociatedObject(id _Nonnullobject,constvoid* _Nonnull key)objc_setAssociatedObject(id _Nonnullobject,constvoid* _Nonnull key,? ? ? ? ? ? ? ? ? ? ? ? id _Nullablevalue, objc_AssociationPolicy policy)

runtime:方法替換相關API

BOOLclass_addMethod(Class cls, SEL name, IMP imp,constchar*types);object_getClass(id _Nullable obj)Methodclass_getInstanceMethod(Class cls, SEL name);constchar*method_getTypeEncoding(Method m);FOUNDATION_EXPORT SELNSSelectorFromString(NSString *aSelectorName);

runtime:消息機制相關API

objc_msgSendSuper

KVO

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

3. 拓展:Delegate實現(xiàn)KVO

注意:以下都是同一個文件:NSObject+Block_Delegate.m中寫的

觀察類CM_ObserverInfo需要改一個屬性颗圣,將Block改為一個Delegate。

@interfaceCM_ObserverInfo : NSObject@property(nonatomic, weak) NSObject * observer;@property(nonatomic, copy) NSString * key;//修改這里@property(nonatomic, assign) id observerDelegate;@end

同樣屁使,觀察類CM_ObserverInfo初始化的時候也需要相應初始這個新屬性在岂。

@implementationCM_ObserverInfo- (instancetype)initWithObserver: (NSObject*)observer forKey: (NSString*)key{if(self= [superinit]) {? ? ? ? ? ? ? ? _observer = observer;self.key = key;//修改這里self.observerDelegate = (id)observer;? ? }returnself;}@end

暴露給調(diào)用者為被觀察對象添加KVO方法:不需要傳Block了。

#pragma mark -- NSObject Category(KVO Reconstruct)@implementationNSObject(Block_KVO)- (void)CM_addObserver:(NSObject*)observer forKey:(NSString*)key withBlock:(CM_ObservingHandler)observedHandler{//...省略//add this observation info to saved new observer//修改這里CM_ObserverInfo* newInfo = [[CM_ObserverInfoalloc] initWithObserver: observer forKey: key];//...省略}

調(diào)用者:利用上面的API為被觀察者添加KVO

VC調(diào)用API

#import"NSObject+Delegate_KVO.h"http://...........- (void)viewDidLoad {? ? [superviewDidLoad];ObservedObject*object= [ObservedObjectnew];object.observedNum= @8;? ? #pragma mark -ObservedByDelegate[objectCM_addObserver: self forKey: @"observedNum"];object.observedNum= @10;}

VC實現(xiàn)代理方法

#pragma mark - ObserverDelegate-(void)CM_ObserveValueForKeyPath:(NSString*)keyPath ofObject:(id)object oldValue:(id)oldValue newValue:(id)newValue{NSLog(@"Value had changed yet with observing Delegate");NSLog(@"oldValue---%@",oldValue);NSLog(@"newValue---%@",newValue);}

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蛮寂,一起剝皮案震驚了整個濱河市蔽午,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌酬蹋,老刑警劉巖及老,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異除嘹,居然都是意外死亡写半,警方通過查閱死者的電腦和手機燎斩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門辽故,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人铸鹰,你說我怎么就攤上這事年缎』诖罚” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵单芜,是天一觀的道長蜕该。 經(jīng)常有香客問我,道長洲鸠,這世上最難降的妖魔是什么堂淡? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮扒腕,結(jié)果婚禮上绢淀,老公的妹妹穿的比我還像新娘。我一直安慰自己瘾腰,他們只是感情好皆的,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蹋盆,像睡著了一般费薄。 火紅的嫁衣襯著肌膚如雪硝全。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天楞抡,我揣著相機與錄音伟众,去河邊找鬼。 笑死拌倍,一個胖子當著我的面吹牛赂鲤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播柱恤,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼数初,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了梗顺?” 一聲冷哼從身側(cè)響起泡孩,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎寺谤,沒想到半個月后仑鸥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡变屁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年眼俊,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片粟关。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡疮胖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出闷板,到底是詐尸還是另有隱情澎灸,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布遮晚,位于F島的核電站性昭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏县遣。R本人自食惡果不足惜糜颠,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望萧求。 院中可真熱鬧括蝠,春花似錦、人聲如沸饭聚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秒梳。三九已至法绵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間酪碘,已是汗流浹背朋譬。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留兴垦,地道東北人徙赢。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像探越,于是被迫代替她去往敵國和親狡赐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

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