痛快的使用KVO ---- FBKVOController源碼分析

前言

KVO是iOS開(kāi)發(fā)當(dāng)中必不可少的一個(gè)工具暖混,可以說(shuō)是使用最廣泛的工具之一。無(wú)論你是要在檢測(cè)某一個(gè)屬性變化,還是構(gòu)建viewmodel雙向綁定UI以及數(shù)據(jù)超陆,KVO都是一個(gè)十分使用的工具。

然而!时呀!

KVO用起來(lái)太TMD麻煩了张漂,要注冊(cè)成為某個(gè)對(duì)象屬性的觀察者,要在適當(dāng)?shù)臅r(shí)候移除觀察者狀態(tài)谨娜,還要寫(xiě)毀掉函數(shù)航攒,更蛋疼的是對(duì)象屬性還要用字符串作為表示。其中任何一個(gè)地方都要注意很多點(diǎn)趴梢,而且因?yàn)镈elegate回調(diào)函數(shù)的原因漠畜,導(dǎo)致代碼分離,可讀性極差坞靶,維護(hù)起來(lái)異常費(fèi)勁憔狞。

所以說(shuō),對(duì)于我來(lái)說(shuō)彰阴,能不用的時(shí)候瘾敢,盡量繞過(guò)去用其他的方法,直到我發(fā)現(xiàn)了Facebook的開(kāi)源框架KVOController尿这。


基本介紹

1簇抵、主要結(jié)構(gòu)

屏幕快照 2017-07-19 上午12.51.20.png

事實(shí)上KVOController的實(shí)現(xiàn)只有2各類,第一個(gè)是NSObject的Category是我們使用的類射众,第二個(gè)則是具體的實(shí)現(xiàn)方法碟摆。

2、NSObject + FBKVOController 分析

在Category的.h文件中有兩個(gè)屬性叨橱,根據(jù)備注可知區(qū)別在意一個(gè)是持有的典蜕,另一個(gè)不是。

/**
 @abstract Lazy-loaded FBKVOController for use with any object
 @return FBKVOController associated with this object, creating one if necessary
 @discussion This makes it convenient to simply create and forget a FBKVOController, and when this object gets dealloc'd, so will the associated     controller and the observation info.
 */
@property (nonatomic, strong) FBKVOController *KVOController;

/**
 @abstract Lazy-loaded FBKVOController for use with any object
 @return FBKVOController associated with this object, creating one if necessary
 @discussion This makes it convenient to simply create and forget a FBKVOController.
 Use this version when a strong reference between controller and observed object would create a retain cycle.
 When not retaining observed objects, special care must be taken to remove observation info prior to deallocation of the observed object.
 */
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;

Category的.m文件和其他文件類似雏逾,寫(xiě)的都是setter以及getter方法嘉裤,并且在getter方法中對(duì)別對(duì)兩個(gè)屬性做了對(duì)于 FBKVOController 的初始化。

- (FBKVOController *)KVOController
{
  id controller = objc_getAssociatedObject(self, NSObjectKVOControllerKey);

  // lazily create the KVOController
  if (nil == controller) {
    controller = [FBKVOController controllerWithObserver:self];
    self.KVOController = controller;
  }

  return controller;
}

- (void)setKVOController:(FBKVOController *)KVOController
 {
  objc_setAssociatedObject(self, NSObjectKVOControllerKey, KVOController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (FBKVOController *)KVOControllerNonRetaining
{
  id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey);

  if (nil == controller) {
    controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO];
    self.KVOControllerNonRetaining = controller;
  }

  return controller;
}

- (void)setKVOControllerNonRetaining:(FBKVOController *)KVOControllerNonRetaining
{
  objc_setAssociatedObject(self, NSObjectKVOControllerNonRetainingKey, KVOControllerNonRetaining,     OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

3栖博、FBKVOController分析

1)幾個(gè)基本API

/**
 @abstract Creates and returns an initialized KVO controller instance.
 @param observer The object notified on key-value change.
 @return The initialized KVO controller instance.
 */
+ (instancetype)controllerWithObserver:(nullable id)observer;


/**
 @abstract Registers observer for key-value change notification.
 @param object The object to observe.
 @param keyPath The key path to observe.
 @param options The NSKeyValueObservingOptions to use for observation.
 @param block The block to execute on notification.
 @discussion On key-value change, the specified block is called. In order to avoid retain loops, the block must avoid referencing the KVO controller or an owner thereof. Observing an already observed object key path or nil results in no operation.
 */
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;


/**
 @abstract Registers observer for key-value change notification.
 @param object The object to observe.
 @param keyPath The key path to observe.
 @param options The NSKeyValueObservingOptions to use for observation.
 @param action The observer selector called on key-value change.
 @discussion On key-value change, the observer's action selector is called. The selector provided should take the form of -propertyDidChange, -    propertyDidChange: or -propertyDidChange:object:, where optional parameters delivered will be KVO change dictionary and object observed. Observing nil or observing an already observed object's key path results in no operation.
 */
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action;


/**
 @abstract Block called on key-value change notification.
 @param observer The observer of the change.
 @param object The object changed.
 @param change The change dictionary which also includes @c FBKVONotificationKeyPathKey
 */
typedef void (^FBKVONotificationBlock)(id _Nullable observer, id object, NSDictionary<NSKeyValueChangeKey, id> *change);
  • 第一個(gè)很簡(jiǎn)單了屑宠,是創(chuàng)建KVOController的實(shí)例
  • 第二個(gè)是注冊(cè)鍵值變化的觀察者,返回一個(gè)有固定參數(shù)的Block仇让。需要注意的是典奉,為了避免循環(huán)引用,盡量避免使用KVOController及其持有者丧叽。
  • 第三個(gè)和第二個(gè)一樣卫玖,也是注冊(cè)鍵值變化的觀察者,但是返回的是一個(gè)選擇子SEL踊淳,API介紹中還對(duì)選擇子SEL進(jìn)行了建議假瞬。
  • 第四個(gè)很簡(jiǎn)單陕靠,是第二個(gè)回調(diào)函數(shù)的Block。值得注意的是脱茉,observer以及object分別是變化的觀察者以及屬性變化的對(duì)象剪芥,所以我們書(shū)寫(xiě)的時(shí)候可以改成我們需要的樣式,以此來(lái)免去另加的轉(zhuǎn)換過(guò)程琴许。

主要的實(shí)現(xiàn)邏輯

KVOController的實(shí)現(xiàn)需要有兩個(gè)私有的成員變量:

  • NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
  • pthread_mutex_t _lock;

以及另一個(gè)暴露在外只讀的屬性:

  • @property (nullable, nonatomic, weak, readonly) id observer;

在實(shí)現(xiàn)過(guò)程中税肪,作為 KVO 的管理者,其必須持有當(dāng)前對(duì)象所有與 KVO 有關(guān)的信息榜田,而在 KVOController 中益兄,用于存儲(chǔ)這個(gè)信息的數(shù)據(jù)結(jié)構(gòu)就是 NSMapTable。為了保證線程安全箭券,需要持有pthread_mutex_t鎖净捅,用于在操作NSMapTable時(shí)候使用。

1辩块、下面讓我們看初始化方法:

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    _observer = observer;
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality :   NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}

很簡(jiǎn)單灸叼,主要工作是持有了傳進(jìn)來(lái)的Observer,初始化了NSMapTable以及初始化了pthread_mutex_t鎖庆捺。
值得一提的是初始化** NSMapTable,我們回看第二部分屁魏,在屬性的區(qū)分就在于是否是持有滔以,根據(jù)屬性的名字也能看出,不持有的話氓拼,引用計(jì)數(shù)就不會(huì)加一你画。所以在初始化的時(shí)候明顯的區(qū)分就是在創(chuàng)建NSPointerFunctionsOptions的時(shí)候,是StrongMemory還是WeakMemory桃漾。
通過(guò)方法
+ (instancetype)controllerWithObserver:(nullable id)observer**初始化的時(shí)候坏匪,默認(rèn)為持有。

2撬统、注冊(cè)觀察者

通常情況下我們會(huì)使用可以回調(diào)Block的API适滓,但是也有少數(shù)情況下會(huì)選擇傳遞選擇子SEL的API,我們這里只拿傳遞Block的方法舉例子恋追。

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
  NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
  if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
  }

  // create info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

  // observe object with info
  [self _observe:object info:info];
}

在這里傳遞進(jìn)來(lái)的一些參數(shù)會(huì)被封裝成為私有的_FBKVOInfo凭迹,那我們來(lái)簡(jiǎn)單看一下_FBKVOInfo的主要實(shí)現(xiàn):

{
@public
  __weak FBKVOController *_controller;
  NSString *_keyPath;
  NSKeyValueObservingOptions _options;
  SEL _action;
  void *_context;
  FBKVONotificationBlock _block;
  _FBKVOInfoState _state;
}

- (instancetype)initWithController:(FBKVOController *)controller
                           keyPath:(NSString *)keyPath
                           options:(NSKeyValueObservingOptions)options
                             block:(nullable FBKVONotificationBlock)block
                            action:(nullable SEL)action
                           context:(nullable void *)context
{
  self = [super init];
  if (nil != self) {
    _controller = controller;
    _block = [block copy];
    _keyPath = [keyPath copy];
    _options = options;
    _action = action;
    _context = context;
  }
  return self;
}

由此可以看出,** _FBKVOInfo的主要作用就是起到了一個(gè)類似Model一樣存儲(chǔ)主要數(shù)據(jù)的作用苦囱,并儲(chǔ)存了一個(gè)_FBKVOInfoState作為表示當(dāng)前的 KVO 狀態(tài)嗅绸。
需要注意的是,成員變量都是用了
@public修飾撕彤。
另外鱼鸠,對(duì)
- (NSString )debugDescription以及- (NSString )debugDescription兩個(gè)方法做了重寫(xiě),方便了使用以及Debug。

之后執(zhí)行了私有方法*- (void)_observe:(id)object info:(_FBKVOInfo )info

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);

  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // check for info existence
  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    // observation info already exists; do not observe it again

    // unlock and return
    pthread_mutex_unlock(&_lock);
    return;
  }

  // lazilly create set of infos
  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object];
  }

  // add info and oberve
  [infos addObject:info];

  // unlock prior to callout
  pthread_mutex_unlock(&_lock);

  [[_FBKVOSharedController sharedController] observe:object info:info];
}

1)首先先進(jìn)行的是對(duì)于自身持有的 _objectInfosMap這個(gè)成員變量的操作蚀狰,一切都需要在先鎖定愉昆,執(zhí)行結(jié)束再解鎖的過(guò)程。

  • 首先獲取了對(duì)于當(dāng)前觀察者的注冊(cè)的關(guān)注列表造锅。
  • 判斷是否當(dāng)前需要關(guān)注的信息是否在此列表中撼唾,如果有則return出去,不再進(jìn)行關(guān)注哥蔚。
  • 如果當(dāng)前的關(guān)注列表不存在則此時(shí)創(chuàng)建一個(gè)
  • 將關(guān)注的信息儲(chǔ)存在關(guān)注列表中倒谷。

2)然后是獲取了** _FBKVOSharedController單例并且執(zhí)行了單例的- (void)observe:(id)object info:(nullable _FBKVOInfo )info*方法。

 - (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
  if (nil == info) {
    return;
  }

  // register info
  pthread_mutex_lock(&_mutex);
  [_infos addObject:info];
  pthread_mutex_unlock(&_mutex);

  // add observer
  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

  if (info->_state == _FBKVOInfoStateInitial) {
    info->_state = _FBKVOInfoStateObserving;
  } else if (info->_state == _FBKVOInfoStateNotObserving) {
    // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
    // and the observer is unregistered within the callback block.
    // at this time the object has been registered as an observer (in Foundation KVO),
    // so we can safely unobserve it.
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
}

加鎖糙箍,對(duì)于當(dāng)前單例的NSHashTable進(jìn)行添加操作的信息渤愁,并執(zhí)行Foundation

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

然后對(duì)信息中的state進(jìn)行更改。

3深夯、觀察并回調(diào)

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                  ofObject:(nullable id)object
                    change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                   context:(nullable void *)context
{
  NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

  _FBKVOInfo *info;

  {
    // lookup context in registered infos, taking out a strong reference only if it exists
    pthread_mutex_lock(&_mutex);
    info = [_infos member:(__bridge id)context];
    pthread_mutex_unlock(&_mutex);
  }

  if (nil != info) {

     // take strong reference to controller
    FBKVOController *controller = info->_controller;
    if (nil != controller) {

      // take strong reference to observer
      id observer = controller.observer;
      if (nil != observer) {

        // dispatch custom block or action, fall back to default action
        if (info->_block) {
          NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
          // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
          if (keyPath) {
            NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
            [mChange addEntriesFromDictionary:change];
            changeWithKeyPath = [mChange copy];
          }
          info->_block(observer, object, changeWithKeyPath);
        } else if (info->_action) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
          [observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
        } else {
          [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
        }
      }
    }
  }
}

這個(gè)就相對(duì)簡(jiǎn)單了抖格,主要是根據(jù)關(guān)注信息內(nèi)是Block還是Action來(lái)執(zhí)行,如果兩者都沒(méi)有就會(huì)調(diào)用觀察者 KVO 回調(diào)方法咕晋。

4雹拄、注銷觀察

事實(shí)上,注銷是在執(zhí)行dealloc的時(shí)候執(zhí)行的掌呜,同時(shí)也去掉了鎖:

- (void)dealloc
{
  [self unobserveAll];
  pthread_mutex_destroy(&_lock);
}

因?yàn)镵VO事件都由私有的** _KVOSharedController** 來(lái)處理滓玖,所以當(dāng)每一個(gè)** KVOController ** 對(duì)象被釋放時(shí),都會(huì)將它自己持有的所有 KVO 的觀察者交由** _KVOSharedControlle** r的方法處理质蕉,我們?cè)賮?lái)看下代碼:

- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos
{
  if (0 == infos.count) {
    return;
  }

  // unregister info
  pthread_mutex_lock(&_mutex);
  for (_FBKVOInfo *info in infos) {
    [_infos removeObject:info];
  }
  pthread_mutex_unlock(&_mutex);

  // remove observer
  for (_FBKVOInfo *info in infos) {
    if (info->_state == _FBKVOInfoStateObserving) {
      [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    }
    info->_state = _FBKVOInfoStateNotObserving;
  }
}

該方法會(huì)遍歷所有傳入的** _FBKVOInfo** 势篡,從其中取出keyPath 并將 ** _KVOSharedController ** 移除觀察者。

當(dāng)然模暗,假如你需要手動(dòng)的移除某一個(gè)的觀察者禁悠,** _KVOSharedController ** 也提供了方法:

- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
{
  if (nil == info) {
    return;
  }

  // unregister info
  pthread_mutex_lock(&_mutex);
  [_infos removeObject:info];
  pthread_mutex_unlock(&_mutex);

  // remove observer
  if (info->_state == _FBKVOInfoStateObserving) {
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
  info->_state = _FBKVOInfoStateNotObserving;
}

總結(jié)

這套框架提供了豐富的結(jié)構(gòu),基本能夠滿足我們對(duì)于KVO的使用需求兑宇。
只需要一次代碼碍侦,就可以完成對(duì)一個(gè)對(duì)象的鍵值觀測(cè),同時(shí)不需要處理移除觀察者顾孽,也可以在同一處代碼進(jìn)行鍵值變化之后的處理祝钢,從惡心的回調(diào)方法中解脫出來(lái),不僅提供了使用方便若厚,也不需要我們手動(dòng)主要觀察者拦英,避免了各種問(wèn)題,絕對(duì)算的上一個(gè)完善好用的框架测秸。


Refrence


另外

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末疤估,一起剝皮案震驚了整個(gè)濱河市灾常,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌铃拇,老刑警劉巖钞瀑,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異慷荔,居然都是意外死亡雕什,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)显晶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)贷岸,“玉大人,你說(shuō)我怎么就攤上這事磷雇〕ゾ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵唯笙,是天一觀的道長(zhǎng)螟蒸。 經(jīng)常有香客問(wèn)我,道長(zhǎng)崩掘,這世上最難降的妖魔是什么七嫌? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮苞慢,結(jié)果婚禮上抄瑟,老公的妹妹穿的比我還像新娘。我一直安慰自己枉疼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布鞋拟。 她就那樣靜靜地躺著骂维,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贺纲。 梳的紋絲不亂的頭發(fā)上航闺,一...
    開(kāi)封第一講書(shū)人閱讀 51,208評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音猴誊,去河邊找鬼潦刃。 笑死,一個(gè)胖子當(dāng)著我的面吹牛懈叹,可吹牛的內(nèi)容都是我干的乖杠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼澄成,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼胧洒!你這毒婦竟也來(lái)了畏吓?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤卫漫,失蹤者是張志新(化名)和其女友劉穎菲饼,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體列赎,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宏悦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了包吝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饼煞。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖漏策,靈堂內(nèi)的尸體忽然破棺而出派哲,到底是詐尸還是另有隱情,我是刑警寧澤掺喻,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布芭届,位于F島的核電站,受9級(jí)特大地震影響感耙,放射性物質(zhì)發(fā)生泄漏褂乍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一即硼、第九天 我趴在偏房一處隱蔽的房頂上張望逃片。 院中可真熱鬧,春花似錦只酥、人聲如沸褥实。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)损离。三九已至,卻和暖如春绝编,著一層夾襖步出監(jiān)牢的瞬間僻澎,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工十饥, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留窟勃,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓逗堵,卻偏偏與公主長(zhǎng)得像秉氧,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蜒秤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

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

  • KVO 作為 iOS 中一種強(qiáng)大并且有效的機(jī)制谬运,為 iOS 開(kāi)發(fā)者們提供了很多的便利隙赁;我們可以使用 KVO 來(lái)檢測(cè)...
    JzRo閱讀 936評(píng)論 0 2
  • KVO 作為 iOS 中一種強(qiáng)大并且有效的機(jī)制,為 iOS 開(kāi)發(fā)者們提供了很多的便利梆暖;我們可以使用 KVO 來(lái)檢測(cè)...
    Draveness閱讀 6,897評(píng)論 11 59
  • 上半年有段時(shí)間做了一個(gè)項(xiàng)目复哆,項(xiàng)目中聊天界面用到了音頻播放响蕴,涉及到進(jìn)度條绩蜻,當(dāng)時(shí)做android時(shí)候處理的不太好礁苗,由于...
    DaZenD閱讀 3,017評(píng)論 0 26
  • FBKVOController是一個(gè)簡(jiǎn)單易用的鍵值觀察框架,KVOController 對(duì)于 Cocoa 中 KV...
    我有小尾巴快看閱讀 1,387評(píng)論 0 0
  • 《沒(méi)蕃故人》張籍前年伐月支芒划,城上沒(méi)全師冬竟。蕃漢斷消息,死生長(zhǎng)別離民逼。無(wú)人收廢帳泵殴,歸馬識(shí)殘旗。欲祭疑君在拼苍,天涯哭此時(shí)笑诅。 ...
    秋涼檸檬閱讀 827評(píng)論 0 1