FBKVOController.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. /**
  2. Copyright (c) 2014-present, Facebook, Inc.
  3. All rights reserved.
  4. This source code is licensed under the BSD-style license found in the
  5. LICENSE file in the root directory of this source tree. An additional grant
  6. of patent rights can be found in the PATENTS file in the same directory.
  7. */
  8. #import "FBKVOController.h"
  9. #import <objc/message.h>
  10. #import <pthread/pthread.h>
  11. #if !__has_feature(objc_arc)
  12. #error This file must be compiled with ARC. Convert your project to ARC or specify the -fobjc-arc flag.
  13. #endif
  14. NS_ASSUME_NONNULL_BEGIN
  15. #pragma mark Utilities -
  16. static NSString *describe_option(NSKeyValueObservingOptions option)
  17. {
  18. switch (option) {
  19. case NSKeyValueObservingOptionNew:
  20. return @"NSKeyValueObservingOptionNew";
  21. break;
  22. case NSKeyValueObservingOptionOld:
  23. return @"NSKeyValueObservingOptionOld";
  24. break;
  25. case NSKeyValueObservingOptionInitial:
  26. return @"NSKeyValueObservingOptionInitial";
  27. break;
  28. case NSKeyValueObservingOptionPrior:
  29. return @"NSKeyValueObservingOptionPrior";
  30. break;
  31. default:
  32. NSCAssert(NO, @"unexpected option %tu", option);
  33. break;
  34. }
  35. return nil;
  36. }
  37. static void append_option_description(NSMutableString *s, NSUInteger option)
  38. {
  39. if (0 == s.length) {
  40. [s appendString:describe_option(option)];
  41. } else {
  42. [s appendString:@"|"];
  43. [s appendString:describe_option(option)];
  44. }
  45. }
  46. static NSUInteger enumerate_flags(NSUInteger *ptrFlags)
  47. {
  48. NSCAssert(ptrFlags, @"expected ptrFlags");
  49. if (!ptrFlags) {
  50. return 0;
  51. }
  52. NSUInteger flags = *ptrFlags;
  53. if (!flags) {
  54. return 0;
  55. }
  56. NSUInteger flag = 1 << __builtin_ctzl(flags);
  57. flags &= ~flag;
  58. *ptrFlags = flags;
  59. return flag;
  60. }
  61. static NSString *describe_options(NSKeyValueObservingOptions options)
  62. {
  63. NSMutableString *s = [NSMutableString string];
  64. NSUInteger option;
  65. while (0 != (option = enumerate_flags(&options))) {
  66. append_option_description(s, option);
  67. }
  68. return s;
  69. }
  70. #pragma mark _FBKVOInfo -
  71. typedef NS_ENUM(uint8_t, _FBKVOInfoState) {
  72. _FBKVOInfoStateInitial = 0,
  73. // whether the observer registration in Foundation has completed
  74. _FBKVOInfoStateObserving,
  75. // whether `unobserve` was called before observer registration in Foundation has completed
  76. // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions
  77. _FBKVOInfoStateNotObserving,
  78. };
  79. NSString *const FBKVONotificationKeyPathKey = @"FBKVONotificationKeyPathKey";
  80. /**
  81. @abstract The key-value observation info.
  82. @discussion Object equality is only used within the scope of a controller instance. Safely omit controller from equality definition.
  83. */
  84. @interface _FBKVOInfo : NSObject
  85. @end
  86. @implementation _FBKVOInfo
  87. {
  88. @public
  89. __weak FBKVOController *_controller;
  90. NSString *_keyPath;
  91. NSKeyValueObservingOptions _options;
  92. SEL _action;
  93. void *_context;
  94. FBKVONotificationBlock _block;
  95. _FBKVOInfoState _state;
  96. }
  97. - (instancetype)initWithController:(FBKVOController *)controller
  98. keyPath:(NSString *)keyPath
  99. options:(NSKeyValueObservingOptions)options
  100. block:(nullable FBKVONotificationBlock)block
  101. action:(nullable SEL)action
  102. context:(nullable void *)context
  103. {
  104. self = [super init];
  105. if (nil != self) {
  106. _controller = controller;
  107. _block = [block copy];
  108. _keyPath = [keyPath copy];
  109. _options = options;
  110. _action = action;
  111. _context = context;
  112. }
  113. return self;
  114. }
  115. - (instancetype)initWithController:(FBKVOController *)controller keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
  116. {
  117. return [self initWithController:controller keyPath:keyPath options:options block:block action:NULL context:NULL];
  118. }
  119. - (instancetype)initWithController:(FBKVOController *)controller keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action
  120. {
  121. return [self initWithController:controller keyPath:keyPath options:options block:NULL action:action context:NULL];
  122. }
  123. - (instancetype)initWithController:(FBKVOController *)controller keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
  124. {
  125. return [self initWithController:controller keyPath:keyPath options:options block:NULL action:NULL context:context];
  126. }
  127. - (instancetype)initWithController:(FBKVOController *)controller keyPath:(NSString *)keyPath
  128. {
  129. return [self initWithController:controller keyPath:keyPath options:0 block:NULL action:NULL context:NULL];
  130. }
  131. - (NSUInteger)hash
  132. {
  133. return [_keyPath hash];
  134. }
  135. - (BOOL)isEqual:(id)object
  136. {
  137. if (nil == object) {
  138. return NO;
  139. }
  140. if (self == object) {
  141. return YES;
  142. }
  143. if (![object isKindOfClass:[self class]]) {
  144. return NO;
  145. }
  146. return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
  147. }
  148. - (NSString *)debugDescription
  149. {
  150. NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p keyPath:%@", NSStringFromClass([self class]), self, _keyPath];
  151. if (0 != _options) {
  152. [s appendFormat:@" options:%@", describe_options(_options)];
  153. }
  154. if (NULL != _action) {
  155. [s appendFormat:@" action:%@", NSStringFromSelector(_action)];
  156. }
  157. if (NULL != _context) {
  158. [s appendFormat:@" context:%p", _context];
  159. }
  160. if (NULL != _block) {
  161. [s appendFormat:@" block:%p", _block];
  162. }
  163. [s appendString:@">"];
  164. return s;
  165. }
  166. @end
  167. #pragma mark _FBKVOSharedController -
  168. /**
  169. @abstract The shared KVO controller instance.
  170. @discussion Acts as a receptionist, receiving and forwarding KVO notifications.
  171. */
  172. @interface _FBKVOSharedController : NSObject
  173. /** A shared instance that never deallocates. */
  174. + (instancetype)sharedController;
  175. /** observe an object, info pair */
  176. - (void)observe:(id)object info:(nullable _FBKVOInfo *)info;
  177. /** unobserve an object, info pair */
  178. - (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info;
  179. /** unobserve an object with a set of infos */
  180. - (void)unobserve:(id)object infos:(nullable NSSet *)infos;
  181. @end
  182. @implementation _FBKVOSharedController
  183. {
  184. NSHashTable<_FBKVOInfo *> *_infos;
  185. pthread_mutex_t _mutex;
  186. }
  187. + (instancetype)sharedController
  188. {
  189. static _FBKVOSharedController *_controller = nil;
  190. static dispatch_once_t onceToken;
  191. dispatch_once(&onceToken, ^{
  192. _controller = [[_FBKVOSharedController alloc] init];
  193. });
  194. return _controller;
  195. }
  196. - (instancetype)init
  197. {
  198. self = [super init];
  199. if (nil != self) {
  200. NSHashTable *infos = [NSHashTable alloc];
  201. #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
  202. _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
  203. #elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
  204. if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
  205. _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
  206. } else {
  207. // silence deprecated warnings
  208. #pragma clang diagnostic push
  209. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  210. _infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
  211. #pragma clang diagnostic pop
  212. }
  213. #endif
  214. pthread_mutex_init(&_mutex, NULL);
  215. }
  216. return self;
  217. }
  218. - (void)dealloc
  219. {
  220. pthread_mutex_destroy(&_mutex);
  221. }
  222. - (NSString *)debugDescription
  223. {
  224. NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p", NSStringFromClass([self class]), self];
  225. // lock
  226. pthread_mutex_lock(&_mutex);
  227. NSMutableArray *infoDescriptions = [NSMutableArray arrayWithCapacity:_infos.count];
  228. for (_FBKVOInfo *info in _infos) {
  229. [infoDescriptions addObject:info.debugDescription];
  230. }
  231. [s appendFormat:@" contexts:%@", infoDescriptions];
  232. // unlock
  233. pthread_mutex_unlock(&_mutex);
  234. [s appendString:@">"];
  235. return s;
  236. }
  237. - (void)observe:(id)object info:(nullable _FBKVOInfo *)info
  238. {
  239. if (nil == info) {
  240. return;
  241. }
  242. // register info
  243. pthread_mutex_lock(&_mutex);
  244. [_infos addObject:info];
  245. pthread_mutex_unlock(&_mutex);
  246. // add observer
  247. [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
  248. if (info->_state == _FBKVOInfoStateInitial) {
  249. info->_state = _FBKVOInfoStateObserving;
  250. } else if (info->_state == _FBKVOInfoStateNotObserving) {
  251. // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
  252. // and the observer is unregistered within the callback block.
  253. // at this time the object has been registered as an observer (in Foundation KVO),
  254. // so we can safely unobserve it.
  255. [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  256. }
  257. }
  258. - (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
  259. {
  260. if (nil == info) {
  261. return;
  262. }
  263. // unregister info
  264. pthread_mutex_lock(&_mutex);
  265. [_infos removeObject:info];
  266. pthread_mutex_unlock(&_mutex);
  267. // remove observer
  268. if (info->_state == _FBKVOInfoStateObserving) {
  269. [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  270. }
  271. info->_state = _FBKVOInfoStateNotObserving;
  272. }
  273. - (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos
  274. {
  275. if (0 == infos.count) {
  276. return;
  277. }
  278. // unregister info
  279. pthread_mutex_lock(&_mutex);
  280. for (_FBKVOInfo *info in infos) {
  281. [_infos removeObject:info];
  282. }
  283. pthread_mutex_unlock(&_mutex);
  284. // remove observer
  285. for (_FBKVOInfo *info in infos) {
  286. if (info->_state == _FBKVOInfoStateObserving) {
  287. [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  288. }
  289. info->_state = _FBKVOInfoStateNotObserving;
  290. }
  291. }
  292. - (void)observeValueForKeyPath:(nullable NSString *)keyPath
  293. ofObject:(nullable id)object
  294. change:(nullable NSDictionary<NSString *, id> *)change
  295. context:(nullable void *)context
  296. {
  297. NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
  298. _FBKVOInfo *info;
  299. {
  300. // lookup context in registered infos, taking out a strong reference only if it exists
  301. pthread_mutex_lock(&_mutex);
  302. info = [_infos member:(__bridge id)context];
  303. pthread_mutex_unlock(&_mutex);
  304. }
  305. if (nil != info) {
  306. // take strong reference to controller
  307. FBKVOController *controller = info->_controller;
  308. if (nil != controller) {
  309. // take strong reference to observer
  310. id observer = controller.observer;
  311. if (nil != observer) {
  312. // dispatch custom block or action, fall back to default action
  313. if (info->_block) {
  314. NSDictionary<NSString *, id> *changeWithKeyPath = change;
  315. // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
  316. if (keyPath) {
  317. NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
  318. [mChange addEntriesFromDictionary:change];
  319. changeWithKeyPath = [mChange copy];
  320. }
  321. info->_block(observer, object, changeWithKeyPath);
  322. } else if (info->_action) {
  323. #pragma clang diagnostic push
  324. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  325. [observer performSelector:info->_action withObject:change withObject:object];
  326. #pragma clang diagnostic pop
  327. } else {
  328. [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
  329. }
  330. }
  331. }
  332. }
  333. }
  334. @end
  335. #pragma mark FBKVOController -
  336. @implementation FBKVOController
  337. {
  338. NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
  339. pthread_mutex_t _lock;
  340. }
  341. #pragma mark Lifecycle -
  342. + (instancetype)controllerWithObserver:(nullable id)observer
  343. {
  344. return [[self alloc] initWithObserver:observer];
  345. }
  346. - (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
  347. {
  348. self = [super init];
  349. if (nil != self) {
  350. _observer = observer;
  351. NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
  352. _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
  353. pthread_mutex_init(&_lock, NULL);
  354. }
  355. return self;
  356. }
  357. - (instancetype)initWithObserver:(nullable id)observer
  358. {
  359. return [self initWithObserver:observer retainObserved:YES];
  360. }
  361. - (void)dealloc
  362. {
  363. [self unobserveAll];
  364. pthread_mutex_destroy(&_lock);
  365. }
  366. #pragma mark Properties -
  367. - (NSString *)debugDescription
  368. {
  369. NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p", NSStringFromClass([self class]), self];
  370. [s appendFormat:@" observer:<%@:%p>", NSStringFromClass([_observer class]), _observer];
  371. // lock
  372. pthread_mutex_lock(&_lock);
  373. if (0 != _objectInfosMap.count) {
  374. [s appendString:@"\n "];
  375. }
  376. for (id object in _objectInfosMap) {
  377. NSMutableSet *infos = [_objectInfosMap objectForKey:object];
  378. NSMutableArray *infoDescriptions = [NSMutableArray arrayWithCapacity:infos.count];
  379. [infos enumerateObjectsUsingBlock:^(_FBKVOInfo *info, BOOL *stop) {
  380. [infoDescriptions addObject:info.debugDescription];
  381. }];
  382. [s appendFormat:@"%@ -> %@", object, infoDescriptions];
  383. }
  384. // unlock
  385. pthread_mutex_unlock(&_lock);
  386. [s appendString:@">"];
  387. return s;
  388. }
  389. #pragma mark Utilities -
  390. - (void)_observe:(id)object info:(_FBKVOInfo *)info
  391. {
  392. // lock
  393. pthread_mutex_lock(&_lock);
  394. NSMutableSet *infos = [_objectInfosMap objectForKey:object];
  395. // check for info existence
  396. _FBKVOInfo *existingInfo = [infos member:info];
  397. if (nil != existingInfo) {
  398. // observation info already exists; do not observe it again
  399. // unlock and return
  400. pthread_mutex_unlock(&_lock);
  401. return;
  402. }
  403. // lazilly create set of infos
  404. if (nil == infos) {
  405. infos = [NSMutableSet set];
  406. [_objectInfosMap setObject:infos forKey:object];
  407. }
  408. // add info and oberve
  409. [infos addObject:info];
  410. // unlock prior to callout
  411. pthread_mutex_unlock(&_lock);
  412. [[_FBKVOSharedController sharedController] observe:object info:info];
  413. }
  414. - (void)_unobserve:(id)object info:(_FBKVOInfo *)info
  415. {
  416. // lock
  417. pthread_mutex_lock(&_lock);
  418. // get observation infos
  419. NSMutableSet *infos = [_objectInfosMap objectForKey:object];
  420. // lookup registered info instance
  421. _FBKVOInfo *registeredInfo = [infos member:info];
  422. if (nil != registeredInfo) {
  423. [infos removeObject:registeredInfo];
  424. // remove no longer used infos
  425. if (0 == infos.count) {
  426. [_objectInfosMap removeObjectForKey:object];
  427. }
  428. }
  429. // unlock
  430. pthread_mutex_unlock(&_lock);
  431. // unobserve
  432. [[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo];
  433. }
  434. - (void)_unobserve:(id)object
  435. {
  436. // lock
  437. pthread_mutex_lock(&_lock);
  438. NSMutableSet *infos = [_objectInfosMap objectForKey:object];
  439. // remove infos
  440. [_objectInfosMap removeObjectForKey:object];
  441. // unlock
  442. pthread_mutex_unlock(&_lock);
  443. // unobserve
  444. [[_FBKVOSharedController sharedController] unobserve:object infos:infos];
  445. }
  446. - (void)_unobserveAll
  447. {
  448. // lock
  449. pthread_mutex_lock(&_lock);
  450. NSMapTable *objectInfoMaps = [_objectInfosMap copy];
  451. // clear table and map
  452. [_objectInfosMap removeAllObjects];
  453. // unlock
  454. pthread_mutex_unlock(&_lock);
  455. _FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];
  456. for (id object in objectInfoMaps) {
  457. // unobserve each registered object and infos
  458. NSSet *infos = [objectInfoMaps objectForKey:object];
  459. [shareController unobserve:object infos:infos];
  460. }
  461. }
  462. #pragma mark API -
  463. - (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
  464. {
  465. NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
  466. if (nil == object || 0 == keyPath.length || NULL == block) {
  467. return;
  468. }
  469. // create info
  470. _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
  471. // observe object with info
  472. [self _observe:object info:info];
  473. }
  474. - (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
  475. {
  476. NSAssert(0 != keyPaths.count && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPaths, block);
  477. if (nil == object || 0 == keyPaths.count || NULL == block) {
  478. return;
  479. }
  480. for (NSString *keyPath in keyPaths) {
  481. [self observe:object keyPath:keyPath options:options block:block];
  482. }
  483. }
  484. - (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action
  485. {
  486. NSAssert(0 != keyPath.length && NULL != action, @"missing required parameters observe:%@ keyPath:%@ action:%@", object, keyPath, NSStringFromSelector(action));
  487. NSAssert([_observer respondsToSelector:action], @"%@ does not respond to %@", _observer, NSStringFromSelector(action));
  488. if (nil == object || 0 == keyPath.length || NULL == action) {
  489. return;
  490. }
  491. // create info
  492. _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options action:action];
  493. // observe object with info
  494. [self _observe:object info:info];
  495. }
  496. - (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options action:(SEL)action
  497. {
  498. NSAssert(0 != keyPaths.count && NULL != action, @"missing required parameters observe:%@ keyPath:%@ action:%@", object, keyPaths, NSStringFromSelector(action));
  499. NSAssert([_observer respondsToSelector:action], @"%@ does not respond to %@", _observer, NSStringFromSelector(action));
  500. if (nil == object || 0 == keyPaths.count || NULL == action) {
  501. return;
  502. }
  503. for (NSString *keyPath in keyPaths) {
  504. [self observe:object keyPath:keyPath options:options action:action];
  505. }
  506. }
  507. - (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
  508. {
  509. NSAssert(0 != keyPath.length, @"missing required parameters observe:%@ keyPath:%@", object, keyPath);
  510. if (nil == object || 0 == keyPath.length) {
  511. return;
  512. }
  513. // create info
  514. _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options context:context];
  515. // observe object with info
  516. [self _observe:object info:info];
  517. }
  518. - (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options context:(nullable void *)context
  519. {
  520. NSAssert(0 != keyPaths.count, @"missing required parameters observe:%@ keyPath:%@", object, keyPaths);
  521. if (nil == object || 0 == keyPaths.count) {
  522. return;
  523. }
  524. for (NSString *keyPath in keyPaths) {
  525. [self observe:object keyPath:keyPath options:options context:context];
  526. }
  527. }
  528. - (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath
  529. {
  530. // create representative info
  531. _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath];
  532. // unobserve object property
  533. [self _unobserve:object info:info];
  534. }
  535. - (void)unobserve:(nullable id)object
  536. {
  537. if (nil == object) {
  538. return;
  539. }
  540. [self _unobserve:object];
  541. }
  542. - (void)unobserveAll
  543. {
  544. [self _unobserveAll];
  545. }
  546. @end
  547. NS_ASSUME_NONNULL_END