序言
在iOS開發(fā)中,蘋果提供了許多機(jī)制給我們進(jìn)行回調(diào)。KVO(key-value-observing)
是一種十分有趣的回調(diào)機(jī)制,在某個(gè)對(duì)象注冊(cè)監(jiān)聽者后,在被監(jiān)聽的對(duì)象發(fā)生改變時(shí),對(duì)象會(huì)發(fā)送一個(gè)通知給監(jiān)聽者,以便監(jiān)聽者執(zhí)行回調(diào)操作。最常見的KVO運(yùn)用是監(jiān)聽scrollView
的contentOffset
屬性,來(lái)完成用戶滾動(dòng)時(shí)動(dòng)態(tài)改變某些控件的屬性實(shí)現(xiàn)效果,包括漸變導(dǎo)航欄、下拉刷新控件等效果。
漸變導(dǎo)航欄
使用
KVO的使用非常簡(jiǎn)單,使用KVO的要求是對(duì)象必須能支持kvc機(jī)制——所有NSObject的子類都支持這個(gè)機(jī)制。拿上面的漸變導(dǎo)航欄做,我們?yōu)閠ableView添加了一個(gè)監(jiān)聽者controller,在我們滑動(dòng)列表的時(shí)候,會(huì)計(jì)算當(dāng)前列表的滾動(dòng)偏移量,然后改變導(dǎo)航欄的背景色透明度。
//添加監(jiān)聽者[self.tableView addObserver: self forKeyPath: @"contentOffset" options: NSKeyValueObservingOptionNew context: nil];/** * 監(jiān)聽屬性值發(fā)生改變時(shí)回調(diào) */- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{ CGFloat offset = self.tableView.contentOffset.y; CGFloat delta = offset / 64.f + 1.f; delta = MAX(0, delta); [self alphaNavController].barAlpha = MIN(1, delta);}
毫無(wú)疑問(wèn),kvo是一種非常便捷的回調(diào)方式,但是編譯器是怎么完成監(jiān)聽這個(gè)任務(wù)的呢?先來(lái)看看蘋果文檔對(duì)于KVO的實(shí)現(xiàn)描述
Automatic key-value observing is implemented using a technique called isa-swizzling... 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 ..
簡(jiǎn)要的來(lái)說(shuō),在我們對(duì)某個(gè)對(duì)象完成監(jiān)聽的注冊(cè)后,編譯器會(huì)修改監(jiān)聽對(duì)象(上文中的tableView)的isa指針,讓這個(gè)指針指向一個(gè)新生成的中間類。從某個(gè)意義上來(lái)說(shuō),這是一場(chǎng)騙局。
typedef struct objc_class *Class;typedef struct objc_object { Class isa;} *id;
這里要說(shuō)明的是isa這個(gè)指針,isa是一個(gè)Class類型的指針,對(duì)象的首地址一般是isa變量,同時(shí)isa又保存了對(duì)象的類對(duì)象的首地址。我們通過(guò)object_getClass方法來(lái)獲取這個(gè)對(duì)象的元類,即是對(duì)象的類對(duì)象的類型(正常來(lái)說(shuō),class方法內(nèi)部的實(shí)現(xiàn)就是獲取這個(gè)isa保存的對(duì)象的類型,在kvo的實(shí)現(xiàn)中蘋果對(duì)被監(jiān)聽對(duì)象的class方法進(jìn)行了重寫隱藏了實(shí)現(xiàn))。class方法是獲得對(duì)象的類型,雖然這兩個(gè)返回的結(jié)果是一樣的,但是兩個(gè)方法在本質(zhì)上得到的結(jié)果不是同一個(gè)東西在oc中,規(guī)定了只要擁有isa指針的變量,通通都屬于對(duì)象。上面的objc_object表示的是NSObject這個(gè)類的結(jié)構(gòu)體表示,因此oc不允許出現(xiàn)非NSObject子類的對(duì)象(block是一個(gè)特殊的例外)*當(dāng)然了,蘋果并不想講述更多的實(shí)現(xiàn)細(xì)節(jié),但是我們可以通過(guò)運(yùn)行時(shí)機(jī)制來(lái)完成一些有趣的調(diào)試。
蘋果的黑魔法
根據(jù)蘋果的說(shuō)法,在對(duì)象完成監(jiān)聽注冊(cè)后,修改了被監(jiān)聽對(duì)象的某些屬性,并且改變了isa指針,那么我們可以在監(jiān)聽前后輸出被監(jiān)聽對(duì)象的相關(guān)屬性來(lái)進(jìn)一步探索kvo的原理。為了保證能夠得到對(duì)象的真實(shí)類型,我使用了object_getClass方法,這個(gè)方法在runtime.h頭文件中
NSLog(@"address: %p", self.tableView);NSLog(@"class method: %@", self.tableView.class);NSLog(@"description method: %@", self.tableView);NSLog(@"use runtime to get class: %@", object_getClass(self.tableView));[self.tableView addObserver: self forKeyPath: @"contentOffset" options: NSKeyValueObservingOptionNew context: nil];NSLog(@"===================================================");NSLog(@"address: %p", self.tableView);NSLog(@"class method: %@", self.tableView.class);NSLog(@"description method: %@", self.tableView);NSLog(@"use runtime to get class %@", object_getClass(self.tableView));
在看官們運(yùn)行這段代碼之前,可以先思考一下上面的代碼會(huì)輸出什么。
2015-12-12 23:02:33.216 LXDAlphaNavigationController[1487:63171] address: 0x7f927a81d2002015-12-12 23:02:33.216 LXDAlphaNavigationController[1487:63171] class method: UITableView2015-12-12 23:02:33.217 LXDAlphaNavigationController[1487:63171] description method: <UITableView: 0x7f927a81d200; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x7f927971f9a0>; layer = <CALayer: 0x7f9279706f50>; contentOffset: {0, 0}; contentSize: {600, 0}>2015-12-12 23:02:33.217 LXDAlphaNavigationController[1487:63171] use runtime to get class: UITableView2015-12-12 23:02:33.217 LXDAlphaNavigationController[1487:63171] ===================================================2015-12-12 23:02:33.218 LXDAlphaNavigationController[1487:63171] address: 0x7f927a81d2002015-12-12 23:02:33.218 LXDAlphaNavigationController[1487:63171] class method: UITableView2015-12-12 23:02:33.218 LXDAlphaNavigationController[1487:63171] description method: <UITableView: 0x7f927a81d200; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x7f927971f9a0>; layer = <CALayer: 0x7f9279706f50>; contentOffset: {0, 0}; contentSize: {600, 0}>2015-12-12 23:02:33.230 LXDAlphaNavigationController[1487:63171] use runtime to get class NSKVONotifying_UITableView
除了通過(guò)object_getClass獲取的類型之外,其他的輸出沒有任何變化。class
方法跟description
方法可以重寫實(shí)現(xiàn)上面的效果,但是為什么連地址都是一樣的。這里可以通過(guò)一句小代碼來(lái)說(shuō)明一下:
NSLog(@"%@, %@", self.class, super.class);
上面這段代碼不管你怎么輸出,兩個(gè)結(jié)果都是一樣的。這是由于super本質(zhì)上指向的是父類內(nèi)存。這話說(shuō)起來(lái)有點(diǎn)繞口,但是我們可以通過(guò)對(duì)象內(nèi)存圖來(lái)表示:
類的內(nèi)存
每一個(gè)對(duì)象占用的內(nèi)存中,一部分是父類屬性占用的;在父類占用的內(nèi)存中,又有一部分是父類的父類占用的。前文已經(jīng)說(shuō)過(guò)isa指針指向的是父類,因此在這個(gè)圖中,Son的地址從Father開始,F(xiàn)ather的地址從NSObject開始,這三個(gè)對(duì)象內(nèi)存的地址都是一樣的。通過(guò)這個(gè),我們可以猜到蘋果文檔中所提及的中間類就是被監(jiān)聽對(duì)象的子類。并且為了隱藏實(shí)現(xiàn),蘋果還重寫了這個(gè)子類的class方法跟description方法來(lái)掩人耳目。另外,我們還看到了新類相對(duì)于父類添加了一個(gè)NSKVONotifying_
前綴,添加這個(gè)前綴是為了避免多次創(chuàng)建監(jiān)聽子類,節(jié)省資源
怎么實(shí)現(xiàn)類似效果
既然知道了蘋果的實(shí)現(xiàn)過(guò)程,那么我們可以自己動(dòng)手通過(guò)運(yùn)行時(shí)機(jī)制來(lái)實(shí)現(xiàn)KVO。runtime允許我們?cè)诔绦蜻\(yùn)行時(shí)動(dòng)態(tài)的創(chuàng)建新類、拓展方法、method-swizzling、綁定屬性等等這些有趣的事情。在創(chuàng)建新類之前,我們應(yīng)該學(xué)習(xí)蘋果的做法,判斷當(dāng)前是否存在這個(gè)類,如果不存在我們?cè)龠M(jìn)行創(chuàng)建,并且重新實(shí)現(xiàn)這個(gè)新類的class方法來(lái)掩蓋具體實(shí)現(xiàn)。基于這些原則,我們用下面的方法來(lái)獲取新類
- (Class)createKVOClassWithOriginalClassName: (NSString *)className{ NSString * kvoClassName = [kLXDkvoClassPrefix stringByAppendingString: className]; Class observedClass = NSClassFromString(kvoClassName); if (observedClass) { return observedClass; } //創(chuàng)建新類,并且添加LXDObserver_為類名新前綴 Class originalClass = object_getClass(self); Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0); //獲取監(jiān)聽對(duì)象的class方法實(shí)現(xiàn)代碼,然后替換新建類的class實(shí)現(xiàn) Method classMethod = class_getInstanceMethod(originalClass, @selector(class)); const char * types = method_getTypeEncoding(classMethod); class_addMethod(kvoClass, @selector(class), (IMP)kvo_Class, types); objc_registerClassPair(kvoClass); return kvoClass;}
另外,在判斷是否需要中間類來(lái)完成監(jiān)聽的注冊(cè)前,我們還要判斷監(jiān)聽的屬性的有效性。通過(guò)獲取變量的setter方法名(將首字母大寫并加上前綴set),以此來(lái)獲取setter實(shí)現(xiàn),如果不存在實(shí)現(xiàn)代碼,則拋出異常使程序崩潰。
SEL setterSelector = NSSelectorFromString(setterForGetter(key));Method setterMethod = class_getInstanceMethod([self class], setterSelector);if (!setterMethod) { @throw [NSException exceptionWithName: NSInvalidArgumentException reason: [NSString stringWithFormat: @"unrecognized selector sent to instance %p", self] userInfo: nil]; return;}Class observedClass = object_getClass(self);NSString * className = NSStringFromClass(observedClass);//如果被監(jiān)聽者沒有LXDObserver_,那么判斷是否需要?jiǎng)?chuàng)建新類if (![className hasPrefix: kLXDkvoClassPrefix]) { observedClass = [self createKVOClassWithOriginalClassName: className]; object_setClass(self, observedClass);}//重新實(shí)現(xiàn)setter方法,使其完成const char * types = method_getTypeEncoding(setterMethod);class_addMethod(observedClass, setterSelector, (IMP)KVO_setter, types);
在重新實(shí)現(xiàn)setter方法的時(shí)候,有兩個(gè)重要的方法:willChangeValueForKey
和didChangeValueForKey
,分別在賦值前后進(jìn)行調(diào)用。此外,還要遍歷所有的回調(diào)監(jiān)聽者,然后通知這些監(jiān)聽者:
static void KVO_setter(id self, SEL _cmd, id newValue){ NSString * setterName = NSStringFromSelector(_cmd); NSString * getterName = getterForSetter(setterName); if (!getterName) { @throw [NSException exceptionWithName: NSInvalidArgumentException reason: [NSString stringWithFormat: @"unrecognized selector sent to instance %p", self] userInfo: nil]; return; } id oldValue = [self valueForKey: getterName]; struct objc_super superClass = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)) }; [self willChangeValueForKey: getterName]; void (*objc_msgSendSuperKVO)(void *, SEL, id) = (void *)objc_msgSendSuper; objc_msgSendSuperKVO(&superClass, _cmd, newValue); [self didChangeValueForKey: getterName]; //獲取所有監(jiān)聽回調(diào)對(duì)象進(jìn)行回調(diào) NSMutableArray * observers = objc_getAssociatedObject(self, (__bridge const void *)kLXDkvoAssiociateObserver); for (LXD_ObserverInfo * info in observers) { if ([info.key isEqualToString: getterName]) { dispatch_async(dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ info.handler(self, getterName, oldValue, newValue); }); } }}
所有的監(jiān)聽者通過(guò)動(dòng)態(tài)綁定的方式將其存儲(chǔ)起來(lái),但這樣也會(huì)產(chǎn)生強(qiáng)引用,所以我們還需要提供釋放監(jiān)聽的方法:
- (void)LXD_removeObserver:(NSObject *)object forKey:(NSString *)key{ NSMutableArray * observers = objc_getAssociatedObject(self, (__bridge void *)kLXDkvoAssiociateObserver); LXD_ObserverInfo * observerRemoved = nil; for (LXD_ObserverInfo * observerInfo in observers) { if (observerInfo.observer == object && [observerInfo.key isEqualToString: key]) { observerRemoved = observerInfo; break; } } [observers removeObject: observerRemoved];}
雖然上面已經(jīng)粗略的實(shí)現(xiàn)了kvo,并且我們還能自定義回調(diào)方式。使用target-action或者block的方式進(jìn)行回調(diào)會(huì)比單一的系統(tǒng)回調(diào)要全面的多。但kvo真正的實(shí)現(xiàn)并沒有這么簡(jiǎn)單,上述代碼目前只能實(shí)現(xiàn)對(duì)象類型的監(jiān)聽,基本類型無(wú)法監(jiān)聽,況且還有keyPath可以監(jiān)聽對(duì)象的成員對(duì)象的屬性這種更強(qiáng)大的功能。
尾言
對(duì)于基本類型的監(jiān)聽,蘋果可能是通過(guò)void *
類型對(duì)對(duì)象進(jìn)行橋接轉(zhuǎn)換,然后直接獲取內(nèi)存,通過(guò)type encoding我們可以獲取所有setter對(duì)象的具體類型,雖然實(shí)現(xiàn)比較麻煩,但是確實(shí)能夠達(dá)成類似的效果。鉆研kvo的實(shí)現(xiàn)可以讓我們對(duì)蘋果的代碼實(shí)現(xiàn)有更深層次的了解,這些知識(shí)涉及到了更深層次的技術(shù),探究它們對(duì)我們的開發(fā)視野有著很重要的作用。同時(shí),對(duì)比其他的回調(diào)方式,KVO的實(shí)現(xiàn)在創(chuàng)建子類、重寫方法等等方面的內(nèi)存消耗是很巨大的,因此博主更加推薦使用delegate、block等回調(diào)方式,甚至直接使用method-swizzling來(lái)替換這種重寫setter方式也是可行的。*ps:昨天有人問(wèn)我說(shuō)為什么kvo不直接通過(guò)重寫setter方法的方式來(lái)進(jìn)行回調(diào),而要?jiǎng)?chuàng)建一個(gè)中間類。誠(chéng)然,method_swizzling是一個(gè)很贊的機(jī)制,完全能用它來(lái)滿足監(jiān)聽需求。但是,如果我們要監(jiān)聽的對(duì)象是tableView呢?正常而言,一款應(yīng)用中遠(yuǎn)不止一個(gè)列表,使用method_swizzling會(huì)導(dǎo)致所有的列表都添加了監(jiān)聽回調(diào),先不考慮這可能導(dǎo)致的崩潰風(fēng)險(xiǎn),所有繼承自tableView的視圖(包括自身)的setter都受到了影響。而使用中間類卻避免了這個(gè)問(wèn)題
轉(zhuǎn)載自http://www.lxweimin.com/p/742b4b248da9
KVO監(jiān)聽數(shù)組,kvo是無(wú)法直接去監(jiān)聽數(shù)組的,通常需要做一下處理
1.[self.dataArray addObject:[NSString stringWithFormat:@"第%d條數(shù)據(jù)",i]];//不會(huì)觸發(fā)KVO
NSMutableArray*mut=[[NSMutableArray alloc] init];
for (int i=0; i<count; i++) {
[mut addObject:[NSString stringWithFormat:@"第%d條數(shù)據(jù)",i]];
}
self.dataArray=mut;//數(shù)組指針改變 觸發(fā)KVO
注:該方法可以觸發(fā)KVO,即用[self mutableArrayValueForKey:@"dataArray"]獲取數(shù)組時(shí)觸發(fā)KVO
2.數(shù)組專有的監(jiān)聽方法