KVO(Key Value Observing, 鍵值觀察)是Objective-C對觀察者模式的實現,每次當被觀察對象的某個屬性值發生改變時,註冊的觀察者便能得到通知。 使用KVO很簡單,分爲三個基本步驟:html
註冊觀察者,指定被觀察對象的屬性:
其中,person即爲被觀察對象,它的name屬性即爲被觀察的屬性。ios
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
在觀察者中實現如下回調方法:git
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(voidvoid *)context { // use the context to make sure this is a change in the address, // because we may also be observing other things NSString *name = [object valueForKey:@"name"]; NSLog(@"new name is: %@", name); }
只要person對象中的name屬性發生變化,系統會自動調用該方法。github
最後,不要忘記在dealloc中移除觀察者數組
-(void)dealloc { // must stop observing everything before this object is // deallocated, otherwise it will cause crashes for(Person *p in m_observedPeople){ [p removeObserver:self forKeyPath:@"name"]; } [m_observedPeople release]; m_observedPeople = nil; }
想快速地瞭解OC中使用的某項技術,最快捷高效的莫過於查看Apple的官方文檔。可是關於KVO的具體實現原理,Apple的文檔介紹的真是Can't Be Simple Any More!app
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.ide
從介紹能夠看出,KVO的實現用了所謂的「isa-swizzling」的技術,可是具體是怎麼實現的卻不得而知。不過,若是用runtime提供的方法去深刻探究,即可以窺探其詳細的原理。得益於Mike Ash的文章,咱們能夠詳細瞭解KVO實現的技術細節。this
簡單介紹一下KVO的實現原理:spa
當設置一個類爲觀察對象時,系統會動態地建立一個新的類,這個新的類繼承自被觀察對象的類,還重寫了基類被觀察屬性的setter方法。派生類在被重寫的setter方法中實現真正的通知機制。最後,系統將這個對象的isa指針指向這個新建立的派生類,這樣,被觀察對象就變成了新建立的派生類的實例。(注:runtime中,對象的isa指針指向該對象所屬的類,類的isa指針指向該類的metaclass。有關OC的對象、類對象、元類對象metaclass object和isa指針)。同時,新的派生類還重寫了dealloc方法(removeObserver)。指針
順便提一下KVO是創建在runtime的基礎之上。
不能否認,KVO的功能確實很強大,可是它的缺點也很明顯:
過於簡單的API
KVO中只有經過重寫-observeValueForKeyPath:ofObject:change:context方法來獲取通知,該方法有諸多限制:不能使用自定義的selector,不能使用block,並且當父類也要監聽對象時,每每要寫一大坨代碼。
父類和子類同時存在KVO時(監聽同一個對象的同一個屬性),很容易出現對同一個keyPath進行兩次removeObserver操做,從而致使程序crash。要避免這個問題,就須要區分出KVO是self註冊的,仍是superClass註冊的,咱們能夠在 -addObserver:forKeyPath:options:context:和-removeObserver:forKeyPath:context這兩個方法中傳入不一樣的context進行區分。
廢話那麼多進入正題,咱們本身實現KVO,並封裝block。
2種方式實現KVO的方式:
重點 : 1. 保存block信息,2. 執行block
observed 和 observer 能夠是同一個對象。
建立Observed的子類KVO_Observed
複製Observed的setter方法,並重寫加入
[self willChangeValueForKey:key]; [super setter:newValue]; [self didChangeValueForKey:key];
執行block
重寫-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(voidvoid *)context 或者 在剛剛重寫的setter方法中調用block
代碼:
代碼都放出來了,這一節不用看了
😁😁😁😁
這是大坑。首先感謝 Sindri的小巢詳解蘋果的黑魔法 - KVO 的奧祕
徹底重寫KVO實現: LXD_KeyValueObserve
這個代碼也是他的。
坑一: 我就問: 徹底本身重寫是否是很牛逼! 當我很開心的用的時候,就發現坑了。 研究一了發現,是基本數據類型或結構體都會出現BAD_ACCESS錯誤。 恰好咱們可愛的OC又不支持方法重載,就必須實現多個set方法。對不能類型的屬性,實現不一樣的方法。
大致上分三類:
坑二:
typedef void (^LXD_ObservingHandler) (id observedObject, NSString * observedKey, id oldValue, id newValue);
回調的 oldValue newValue類型都是id。
因此對於基本數據類型和結構體要進行對象包裝。基本數據類型->NSNumber 結構體->NSValue OC不支持方法重載!OC不支持方法重載!OC不支持方法重載!因此咱們不能一個方法搞定一切。那就是寫if-else吧。
if (![self hasSelector: setterSelector]) { //在類中添加方法和實現 const char * types = method_getTypeEncoding(setterMethod); Class observedClass = object_getClass(self); objc_property_t property_t = class_getProperty(observedClass, key.UTF8String); NSString *attributes = [NSString stringWithCString:property_getAttributes(property_t) encoding:NSUTF8StringEncoding]; IMP setterIMP; if ([attributes hasPrefix:@"T@"]) { setterIMP = (IMP)KVO_setter_id; }else if ([attributes hasPrefix:@"T{"]) { if ([attributes hasPrefix:@"T{CGPoint"]) { setterIMP = (IMP)KVO_setterValue_CGPoint; } else if ([attributes hasPrefix:@"T{CGRect"]) { setterIMP = (IMP)KVO_setterValue_CGRect; } else if ([attributes hasPrefix:@"T{CGVector"]) { setterIMP = (IMP)KVO_setterValue_CGVector; } else if ([attributes hasPrefix:@"T{CGSize"]) { setterIMP = (IMP)KVO_setterValue_CGSize; } else if ([attributes hasPrefix:@"T{CGAffineTransform"]) { setterIMP = (IMP)KVO_setterValue_CGAffineTransform; } else if ([attributes hasPrefix:@"T{UIEdgeInsets"]) { setterIMP = (IMP)KVO_setterValue_UIEdgeInsets; } else if ([attributes hasPrefix:@"T{UIOffset"]) { setterIMP = (IMP)KVO_setterValue_UIOffset; } else if ([attributes hasPrefix:@"T{_NSRange"]) { setterIMP = (IMP)KVO_setterValue_NSRange; } else if ([attributes hasPrefix:@"T{CATransform3D"]) { setterIMP = (IMP)KVO_setterValue_CATransform3D; }else { NSAssert(NO, @"Can't identify Struct"); } }else { if ([attributes hasPrefix:@"Tc"]) { setterIMP = (IMP)KVO_setterNumber_char; } else if ([attributes hasPrefix:@"TC"]) { setterIMP = (IMP)KVO_setterNumber_UnsignedChar; } else if ([attributes hasPrefix:@"Ts"]) { setterIMP = (IMP)KVO_setterNumber_short; } else if ([attributes hasPrefix:@"TS"]) { setterIMP = (IMP)KVO_setterNumber_UnsignedShort; } else if ([attributes hasPrefix:@"Ti"]) { setterIMP = (IMP)KVO_setterNumber_int; } else if ([attributes hasPrefix:@"TI"]) { setterIMP = (IMP)KVO_setterNumber_UnsignedInt; } else if ([attributes hasPrefix:@"Tq"]) { setterIMP = (IMP)KVO_setterNumber_long; } else if ([attributes hasPrefix:@"TQ"]) { setterIMP = (IMP)KVO_setterNumber_UnsignedLong; } else if ([attributes hasPrefix:@"Tf"]) { setterIMP = (IMP)KVO_setterNumber_float; } else if ([attributes hasPrefix:@"Td"]) { setterIMP = (IMP)KVO_setterNumber_double; } else if ([attributes hasPrefix:@"TB"]) { setterIMP = (IMP)KVO_setterNumber_BOOL; }else{ NSAssert(NO, @"Can't identify Basic data types"); } } class_addMethod(observedClass, setterSelector, setterIMP, types); }
上面的問題,我已經發給做者issue了。我也提交了個人版本。 最後放上個人版本,兼容基本數據類型和結構體的自實現KVO
歡迎你們拍磚。