Facebook之KVOController详解

简介

键值观察是模型 - 视图 - 控制器应用程序(MVC)中各层之间通信的一种特别有用的技术。

KVOController以Cocoa经过时间考验的键值观察实现为基础。 它提供了一个简单、现代的API,同时也是线程安全的。

如果你在项目中有使用 KVO ,那么 KVOController 绝对是个好选择。它是 facebook 开源的一个 KVO 增强框架。

有以下几个特性:

  • 使用 Blocks 、自定义 Actions 或者 NSKeyValueObserving 回调进行通知

  • 观测者移除时无异常

  • 控制器 dealloc 时隐式的观测者移除

  • 提升使用 NSKeyValueObservingInitial 的性能

  • 线程安全并提供在观测者恢复时额外的保护

开发环境

KVOController利用了当前Objective-C中runtime的优势,以及ARC和指针集合类。

开发环境要求:

  • iOS 6 or later.
  • OS X 10.7 or later.

集成开发环境(IDE,Integrated Development Environment ):

  • Xcode 8.0+

安装

要使用CocoaPods进行安装,请将以下内容添加到项目Podfile中:
pod 'KVOController'

要使用Carthage进行安装,请将以下内容添加到项目Cartfile中:
github "facebook/KVOController"

或者,将FBKVOController.hFBKVOController.m拖放到Xcode项目中, 并同意在需要时复制文件。 对于iOS应用程序,您可以选择链接KVOController项目的静态库目标。

使用CocoaPods或Carthage安装后,在Objective-C中添加以下内容进行导入:
#import

使用

// Objective-C
#import 

// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;

// observe clock date property
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {

  // update clock view with new value
  clockView.date = change[NSKeyValueChangeNewKey];
}];

KVOController在Swift中运行良好,但有两点要求:

  • 观察者应该是 NSObject 的子类
  • 观察属性必须标记为 dynamic
// Swift
class TasksListViewModel: NSObject {

  dynamic var tasksList: [TaskList] = []
}

/// In ViewController.swift

import KVOController

kvoController.observe(viewModel,
                      keyPath: "listsDidChange",
                      options: [.new, .initial]) { (viewController, viewModel, change) in
    
  self.taskListsTableView.reloadData()
}

测试

单元测试包括使用CocoaPods来管理依赖项。 如果您尚未安装CocoaPods,请安装它们。 然后,在命令行中,导航到根KVOController目录并键入:
pod install
这将在OCHamcrestOCMockito上安装和添加测试依赖项。 重新打开Xcode的KVOController工作区和Test,⌘U。

开源许可证

KVOController根据 BSD许可证 发布。 有关详细信息,请参阅LICENSE文件。

BSD License

For KVOController software

Copyright (c) 2014, Facebook, Inc. All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

 * Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

 * Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

 * Neither the name Facebook nor the names of its contributors may be used to
   endorse or promote products derived from this software without specific
   prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

源码分析

KVOController是对Cocoa中KVO进行了封装,整个框架只有两个实现文件:

#import 
#import 
  • 类图
Facebook之KVOController详解_第1张图片
类图

上面类图的PlantUML源码如下:

@startuml

skinparam class {
    BackgroundColor Beige
    ArrowColor Indigo
    BorderColor Indigo
    BackgroundColor<> Wheat
    BorderColor<> Tomato
}
skinparam stereotypeCBackgroundColor YellowGreen
skinparam stereotypeCBackgroundColor<< Foo >> DimGray


NSObject <|-- _FBKVOInfo
NSObject <|-- _FBKVOSharedController
NSObject <|-- FBKVOController
FBKVOController <--o _FBKVOSharedController
FBKVOController <--o _FBKVOInfo

note top of NSObject
    在Objective-C中,几乎所有的类都是继承与NSObject,剩下不继承NSObject的都继承NSProxy.
    NSObject(NSKeyValueObserving) 接收KVO所监听的属性值发生变化的通知
end note

class NSObject {
    # hash : NSUInteger
    # superclass : Class
    # description : NSString
    # debugDescription : NSString

    -init()
    -hash()
    -isEqual:(id)object()
    -debugDescription()
    -dealloc()
    -observeValueForKeyPath: ofObject: change: context:()
}

class _FBKVOInfo {
    +_controller : FBKVOController
    +_keyPath : NSString
    +_options : NSKeyValueObservingOptions
    +_action : SEL
    +_context : void *
    +_block : FBKVONotificationBlock
    +_state : _FBKVOInfoState

    -initWithController: keyPath: options: block: action: context:()
    -hash()
    -isEqual:(id)object()
    -debugDescription()
}

class _FBKVOSharedController {
    #_infos : NSHashTable<_FBKVOInfo *>
    #_mutex : pthread_mutex_t

    +sharedController()
    -observe: info:()
    -unobserve: info:()
    -unobserve: infos:()
    -init()
    -dealloc()
    -debugDescription()
    -observeValueForKeyPath: ofObject: change: context:()
}

class FBKVOController {
    #observer : id

    -_objectInfosMap : NSMapTable *>
    -_lock : pthread_mutex_t

    +controllerWithObserver:()
    -initWithObserver: retainObserved:()
    -initWithObserver:()
    -init()
    -new()
    -observe: keyPath: options: block:()
    -observe: keyPath: options: action:()
    -observe: keyPath: options: context:()
    -observe: keyPaths: options: block:()
    -observe: keyPaths: options: action:()
    -observe: keyPaths: options: context:()
    -unobserve: keyPath:()
    -unobserve:()
    -unobserveAll()
}

@enduml
  • 核心调用栈


    Facebook之KVOController详解_第2张图片
    Add Observer

KVOController实现流程:

  1. Observer会创建一个FBKVOController的属性;

  2. FBKVOController中包含一个NSMapTable的成员属性,用来存储observer的KVO信息;

  3. FBKVOController创建一个_FBKVOInfo类型的实例,实例中存储了和KVO操作相关的信息(keypath等),然后将需要观察的对象Target作为Key,_FBKVOInfo的实例加入数组(对同一个Target的不同keypath的多次KVO操作)并把数组作为Value,存入步骤2中的mapTable中;

  4. FBKVOController会调用_FBKVOSharedController的单例中的方法,同时将步骤3创建的info和观察的target传入给这个方法,这个单例进行了最终的KVO操作;

  5. _FBKVOSharedController的单例调用系统KVO方法,将自己作为观察者来观察Target对象。


    Facebook之KVOController详解_第3张图片
    Remove Observer

在Observer内存被释放,执行dealloc时,其创建的FBKVOController属性的析构方法dealloc会通过KVOInfoMap找到所有KVO的对象,并执行移除观察的操作,十分巧妙的设计!

但是在使用的过程中还是有一些注意事项的:
首先,FBKVOController使用block来传递系统KVO的回调,因此要注意retain cycle。
其次,在使用的过程中,target不能强引用observer,否则也会形成retain cycle。

  • 核心代码分析
  1. FBKVOController构造函数:
- (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;
}

NSMapTable 是早在 Mac OS X 10.5(Leopard)就引入的集合类型。

NSMapTable除了使用NSPointerFunctionsCopyIn,任何的默认行为都会retain(或弱引用)键对象而不会拷贝它,与CFDictionary的行为相同而与NSDictionary不同。当你需要一个字典,它的键没有实现NSCopying协议,比如UIView,的时候非常有用。

如果你好奇为什么苹果”忘记”为NSMapTable增加下标,你现在知道了。下标访问需要一个id作为key,对NSMapTable来说这不是强制的。如果不通过一个非法的API协议或者移除NSCopying协议来削弱全局下标,是没有办法给它增加下标的。

NSMapTable查询性能只比NSDictionary略微慢一点。如果你需要一个不retain键的字典,放弃CFDictionary使用它吧。

pthread_mutex为C语言定义下多线程加锁方式。


1:pthread_mutex_init(pthread_mutex_t mutex,const pthread_mutexattr_t attr);初始化锁变量mutex。attr为锁属性,NULL值为默认属性。

2:pthread_mutex_lock(pthread_mutex_t mutex);加锁

3:pthread_mutex_tylock(*pthread_mutex_t *mutex);加锁,但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。

4:pthread_mutex_unlock(pthread_mutex_t *mutex);释放锁

5:pthread_mutex_destroy(pthread_mutex_t* mutex);使用完后释放
  1. 单例_FBKVOSharedController监听object的info.keyPath属性
- (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];
  }
}
  1. 单例_FBKVOSharedController接收KVO所监听的属性值发生变化的通知
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary *)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 *changeWithKeyPath = change;
          // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
          if (keyPath) {
            NSMutableDictionary *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];
        }
      }
    }
  }
}

addEntriesFromDictionary拼接字典,需要注意的是:
在相同key的情况下,相对应的value会被赋予新值,使用时注意顺序

NSDictionary *dic1 = [NSDictionary dictionaryWithObjectsAndKeys:@"BMW",@"CarLogo",@"Red",@"CarColor",@"MountainX",@"name",nil];
NSMutableDictionary *dic2 = [NSMutableDictionary dictionaryWithObjectsAndKeys:@"JSK",@"name",@"25", @"age", nil];
[dic2 addEntriesFromDictionary:dic1];
    
NSLog(@"%@",dic2);

// 控制台打印如下:
{
    CarColor = Red;
    CarLogo = BMW;
    age = 25;
    name = MountainX;
}

  • 分类 NSObject+FBKVOController

利用分类和Runtime中的关联机制为NSObject添加了两个懒加载的属性:

/**
  Copyright (c) 2014-present, Facebook, Inc.
  All rights reserved.

  This source code is licensed under the BSD-style license found in the
  LICENSE file in the root directory of this source tree. An additional grant
  of patent rights can be found in the PATENTS file in the same directory.
 */

#import 

#import "FBKVOController.h"

NS_ASSUME_NONNULL_BEGIN

/**
 Category that adds built-in `KVOController` and `KVOControllerNonRetaining` on any instance of `NSObject`.

 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.
 */
@interface NSObject (FBKVOController)

/**
 @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;

@end

NS_ASSUME_NONNULL_END

顾名思义, KVOControllerNonRetaining在使用时并不会持有被观察的对象

实现方式:

/**
 Copyright (c) 2014-present, Facebook, Inc.
 All rights reserved.
 
 This source code is licensed under the BSD-style license found in the
 LICENSE file in the root directory of this source tree. An additional grant
 of patent rights can be found in the PATENTS file in the same directory.
 */

#import "NSObject+FBKVOController.h"

#import 

#if !__has_feature(objc_arc)
#error This file must be compiled with ARC. Convert your project to ARC or specify the -fobjc-arc flag.
#endif

#pragma mark NSObject Category -

NS_ASSUME_NONNULL_BEGIN

static void *NSObjectKVOControllerKey = &NSObjectKVOControllerKey;
static void *NSObjectKVOControllerNonRetainingKey = &NSObjectKVOControllerNonRetainingKey;

@implementation NSObject (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);
}

@end


NS_ASSUME_NONNULL_END

其中

static void *NSObjectKVOControllerKey = &NSObjectKVOControllerKey;
static void *NSObjectKVOControllerNonRetainingKey = &NSObjectKVOControllerNonRetainingKey;

这种声明方式在编译的时候创建一个唯一的指针a method to create a unique pointer at compile time.

well, so idea for these constants is to have some unique value, that will not repeat anywhere in the program, but we don't really care about its content.

now, instead of coming up with some random string/number etc, we just create a pointer, and put its address as content, this way it's unique and the code is simple is nice :)

这种声明方式也常用于kvo,用来当做context的key来添加.
因为kvo的时候context如果不小心重复了,会发生奇怪的事情.用这种方式可以避免.

static void *CapturingStillImageContext = &CapturingStillImageContext;
[self addObserver:self forKeyPath:@"stillImageOutput.capturingStillImage" options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) context:CapturingStillImageContext];

PS

  • 关联

关联是指把两个对象相互关联起来,使得其中的一个对象作为另外一个对象的一部分。
关联特性只有在Mac OS X V10.6以及以后的版本上才是可用的。

使用关联,我们可以不用修改类的定义而为其对象增加存储空间。这在我们无法访问到类的源码的时候或者是考虑到二进制兼容性的时候是非常有用。
关联是基于关键字的,因此,我们可以为任何对象增加任意多的关联,每个都使用不同的关键字即可。关联是可以保证被关联的对象在关联对象的整个生命周期都是可用的(在垃圾自动回收环境下也不会导致资源不可回收)。

objc_setAssociatedObject

/** 
 * Sets an associated value for a given object using a given key and association policy.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * @param value The value to associate with the key key for object. Pass nil to clear an existing association.
 * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
 * 
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

创建关联要使用到Objective-C的运行时函数:objc_setAssociatedObject来把一个对象与另外一个对象进行关联。该函数需要四个参数:源对象,关键字,关联的对象和一个关联策略。当然,此处的关键字和关联策略是需要进一步讨论的。
■ 关键字是一个void类型的指针。每一个关联的关键字必须是唯一的。通常都是会采用静态变量来作为关键字。
■ 关联策略表明了相关的对象是通过赋值,保留引用还是复制的方式进行关联的;还有这种关联是原子的还是非原子的。这里的关联策略和声明属性时的很类似。这种关联策略是通过使用预先定义好的常量来表示的。

objc_getAssociatedObject

/** 
 * Returns the value associated with a given object for a given key.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * 
 * @return The value associated with the key \e key for \e object.
 * 
 * @see objc_setAssociatedObject
 */
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

获取相关联的对象

objc_removeAssociatedObjects

/** 
 * Removes all associations for a given object.
 * 
 * @param object An object that maintains associated objects.
 * 
 * @note The main purpose of this function is to make it easy to return an object 
 *  to a "pristine state”. You should not use this function for general removal of
 *  associations from objects, since it also removes associations that other clients
 *  may have added to the object. Typically you should use \c objc_setAssociatedObject 
 *  with a nil value to clear an association.
 * 
 * @see objc_setAssociatedObject
 * @see objc_getAssociatedObject
 */
OBJC_EXPORT void
objc_removeAssociatedObjects(id _Nonnull object)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

使用函数objc_removeAssociatedObjects可以断开所有关联。通常情况下不建议使用这个函数,因为他会断开所有关联。只有在需要把对象恢复到“原始状态”的时候才会使用这个函数。
断开关联是使用objc_setAssociatedObject函数,value传入nil值即可。

  • 开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别
开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别
  • UML类图


    UML类图
  • Xcode内部标明API信息的简单宏定义

NS_AVAILABLE_IOS(5_0)
这个方法可以在iOS5.0及以后的版本中使用,如果在比5.0更老的版本中调用这个方法,就会引起崩溃。

NS_DEPRECATED_IOS(2_0, 6_0)
这个宏中有两个版本号。前面一个表明了这个方法被引入时的iOS版本,后面一个表明它被废弃时的iOS版本。被废弃并不是指这个方法就不存在了,只是意味着我们应当开始考虑将相关代码迁移到新的API上去了。

NS_DEPRECATED(NA, NA, 5_0, 7_0)
这句表示iOS 5.0引用,7.0就废弃了;NA 表示缺省,参数无效不用填。

NS_AVAILABLE(10_8, 6_0)
这个宏告诉我们这方法分别随Mac OS 10.8和iOS 6.0被引入。

NS_DEPRECATED(10_0, 10_6, 2_0, 4_0)
这个方法随Mac OS 10.0和iOS 2.0被引入,在Mac OS 10.6和iOS 4.0后被废弃。

NS_CLASS_AVAILABLE(10_11, 9_0)
这个类分别随Mac OS 10.11和iOS9.0被引入。

NS_ENUM_AVAILABLE(10_11, 9_0)
这个枚举分别随Mac OS 10.11和iOS9.0被引入。

__OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_0,__MAC_10_5,__IPHONE_NA,__IPHONE_NA)
os x 10.0 开始引进这个方法 10.5之后废弃了,ios上从来没只支持过。

__TVOS_PROHIBITED
表示TVOS 禁止使用

参考:
简析KVOController实现原理
FBKVOController详解
如何优雅地使用 KVO
《iOS 7 Programming Pushing the Limits》系列:你可能不知道的ObjC技巧
开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别
利用plantuml绘制类图
plantuml_Skinparam command
[Objective-C]关联(objc_setAssociatedObject、objc_getAssociatedObject、objc_removeAssociatedObjects)
iOS系统库头文件中NS_AVAILABLE相关
iOS官方文档阅读
iOS开发的一些奇巧淫技3
NSMapTable: 不只是一个能放weak指针的 NSDictionary
Apple - NSMapTable
iOS - 基础集合类
iOS中保证线程安全的几种方式与性能对比
iOS 字典的 addEntriesFromDictionary使用注意点

你可能感兴趣的:(Facebook之KVOController详解)