iOS中KVO的套路

KVC

KVC的一些使用技巧,能夠參考以前的一個簡單記錄: iOS中關於KVC使用的一些小技巧數組

KVO

KVO是基於KVC基礎的鍵值觀察機制。 KVO的基本使用就不說了,來看看添加KVO對Person的影響:bash

本文中的代碼都基於Person類的定義:框架

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

@end
複製代碼
static void *kContextPersonKVO = &kContextPersonKVO;

...

Person *p = [[Person alloc] init];
[p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:kContextPersonKVO];

p.age = 10; // 斷點
複製代碼

在斷點處查看:異步

isa-swizzling

KVO的實現機制是isa-swizzling。async

Automatic key-value observing is implemented using a technique called isa-swizzling. The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data. When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.post

KVO作了isa-swizzling以後,會動態建立一個繼承自原來Class的子類。如原類爲Person,則動態建立一個NSKVONotifying_Person繼承自Person類。 對Person類的一個實例對象p而言,對其某個屬性添加KVO以後, 使用object_getClass(self)方法來查看其isa爲NSKVONotifying_Person, 使用class_getSuperclass(object_getClass(self))來查看其super class爲Person。 因此, isa-swizzling的關鍵點在於將被觀察對象(實例對象p)的isa指針指向新建立的類NSKVONotifying_Person。 而使用[self class]獲得的依然是Person,[self superClass]獲得的是NSObject。 因此,對於咱們來說,能夠理解爲:添加KVO以後,被觀察對象(實例對象p)的isa,super_class,檢測屬性的setter方法被改變了。好比,咱們調用屬性age的setter方法,實際上會去NSKVONotifying_Person中找到對應重寫的setAge:方法,使用willChangeForValue:和didChangeForValue:來實現setter方法的監聽。 若是不經過setter方法,而是直接給實例變量_age賦值,是不會觸發KVO的響應方法的。優化

removeObserver方法將isa再指向原來的Person類便可。ui

KVO的使用場景

監聽對象的屬性變化

這一點是KVO最基本的用法,就很少說了atom

觀察者模式

KVO的另外一個最多見使用場景就是觀察者模式。 如對Person的實例變量p的age屬性進行KVO監控,能夠隨時獲取age的變化,作出對應的響應。spa

雙向綁定

使用KVO能夠實現雙向綁定,用於封裝一個響應式的框架。 這一點,RAC和RxSwift值得研究一番。

KVO的注意事項

不是全部屬性均可以監聽。

若是使用KVO監聽了UIView的frame屬性,改變其center屬性,是不會觸發KVO的。由於改變center並未調用frame屬性的setter方法,能夠在center的setter方法中使用willChangeValueFor:和didChangeValueFor:來觸發frame屬性的KVO。

- (void)setCenter:(CGPoint)center
{
    [aView willChangeValueForKey:@"center"];
    // 根據center計算new frame
    CGRect newFrame = xxx;
    aView.frame = newFrame;
    [aView didChangeValueForKey:@"center"];
}
複製代碼

手動監聽NSOperation的屬性

默認狀況下,NSOperation的這三個屬性是隻讀的,

@interface NSOperation : NSObject

@property (readonly, getter=isCancelled) BOOL cancelled;
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;

@end
複製代碼

那咱們若是想給這三個屬性賦值,已達到本身控制NSOperation狀態的目的呢? 可使用以下方式:

@interface CSDownloadOperation : NSOperation
@end


@interface CSDownloadOperation ()

// 因這些屬性是readonly, 不會自動觸發KVO. 須要手動觸發KVO, 見setter方法. 
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;

@end

@implementation CSDownloadOperation

@synthesize executing = _executing;
@synthesize finished = _finished;
@synthesize cancelled = _cancelled;

// MARK: - setter

- (void)setExecuting:(BOOL)executing
{
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}


/**
 finished
 設置isFinished狀態, 不能在start以前執行, 不然會crash.
 */
- (void)setFinished:(BOOL)finished
{
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setCancelled:(BOOL)cancelled
{
    [self willChangeValueForKey:@"isCancelled"];
    _cancelled = cancelled;
    [self didChangeValueForKey:@"isCancelled"];
}

@end
複製代碼

在須要設置新值的時候,手動觸發KVO,而後給對應的實例變量賦值。 這種方式能夠用來自定義一個異步執行的NSOperation,好比使用NSURLSession封裝的下載操做。

對可變集合進行監控

使用KVO的常見方法不能對可變集合進行監控,只能經過mutableArrayValueForKey:, mutableSetValueForKey:, mutableOrderedSetValueForKey:來分別對NSMutableArray,NSMutableSet,NSMutableOrderedSet進行監控。

We would also like to point out that collections as such are not observable. KVO is about observing relationships rather than collections. We cannot observe an NSArray; we can only observe a property on an object – and that property may be an NSArray. As an example, if we have a ContactList object, we can observe its contacts property, but we cannot pass an NSArray to -addObserver:forKeyPath:... as the object to be observed.

mutableArrayValueForKey

好比以下代碼,咱們想要對一個可變數組selectedMaterials進行KVO監控,以便對UI和代碼邏輯進行更新。

@property (nonatomic, strong) NSMutableArray *selectedMaterials;


[self addObserver:self
       forKeyPath:@"selectedMaterials"
          options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
          context:&ctxKVOSelectedMaterials];
       
              
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == &ctxKVOSelectedMaterials) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self updateSelectedCount];
            
            [self.collectionView reloadData];
        });
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
       
[self removeObserver:self
          forKeyPath:@"selectedMaterials"
             context:&ctxKVOSelectedMaterials];
                 
複製代碼

同時,觸發KVO的代碼也有所不一樣。必定要先獲取到可變集合[self mutableArrayValueForKey:@"selectedMaterials"]

// 每次add都會觸發一次KVO
[[self mutableArrayValueForKey:@"selectedMaterials"] addObject:material.number];
[[self mutableArrayValueForKey:@"selectedMaterials"] removeObject:material.number];
[[self mutableArrayValueForKey:@"selectedMaterials"] removeAllObjects];
複製代碼
// 屢次add僅觸發一次KVO
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(self.selectedMaterials.count, materials.count)];
[[self mutableArrayValueForKey:@"selectedMaterials"] insertObjects:materials atIndexes:indexSet];
複製代碼

參考文檔:

In order to be key-value coding compliant for a mutable ordered to-many relationship you must implement the following methods: -insertObject:inAtIndex: or -insert:atIndexes:. At least one of these methods must be implemented. These are analogous to the NSMutableArray methods insertObject:atIndex: and insertObjects:atIndexes:. -removeObjectFromAtIndex: or -removeAtIndexes:. At least one of these methods must be implemented. These methods correspond to the NSMutableArray methods removeObjectAtIndex: and removeObjectsAtIndexes: respectively. -replaceObjectInAtIndex:withObject: or -replaceAtIndexes:with:. Optional. Implement if benchmarking indicates that performance is an issue. The -insertObject:inAtIndex: method is passed the object to insert, and an NSUInteger that specifies the index where it should be inserted. The -insert:atIndexes: method inserts an array of objects into the collection at the indices specified by the passed NSIndexSet. You are only required to implement one of these two methods.

KVO的缺點

不能使用block

針對這一點,有一些第三方庫本身對KVO進行了封裝,添加了可傳遞block的API,相似於NSNotificationCenter的某些方法。

移除已經dealloc的對象則會crash

這一點尤爲要注意。通常確保addObserver與removeObserver成對出現便可。

Swift原生類不支持KVO

KVO是runtime的一個特性,因此在Swift中KVO僅對NSObject的子類有效,且須要對監聽的屬性使用dynamic關鍵字。不過,Swift中的屬性有了willSet和didSet方法,相比KVO更加實用。

同一線程

KVO的observeValueForKeyPath方法執行的線程,始終與執行被監聽屬性的setter方法的代碼處於同一線程。若要在observeValueForKeyPath執行其餘線程的任務,可使用dispatch_async(xxx). 這一點與NSNotification相似。NSNotification不能跨線程:即響應通知的action,默認是與postNotification在同一個線程的,若想在指定線程中執行響應通知的方法,可使用帶有block的addObserver方法,或者使用dispatch_async(xxx)。

FBKVOController

FBKVOController是Facebook開源的KVO封裝庫,針對KVO的一些缺點作了優化,使用也更加簡便。

參考

Key-Value Coding and Observing

相關文章
相關標籤/搜索