KVO奧秘

在iOS開發(fā)中,蘋果提供了許多機制給我們進行回調(diào)。KVO(key-value-observing)是一種十分有趣的回調(diào)機制,在某個對象注冊監(jiān)聽者后,在被監(jiān)聽的對象發(fā)生改變時,對象會發(fā)送一個通知給監(jiān)聽者,以便監(jiān)聽者執(zhí)行回調(diào)操作。最常見的KVO運用是監(jiān)聽scrollView的contentOffset屬性,來完成用戶滾動時動態(tài)改變某些控件的屬性實現(xiàn)效果,包括漸變導航欄、下拉刷新控件等效果。

漸變導航欄

使用

KVO的使用非常簡單,使用KVO的要求是對象必須能支持kvc機制——所有NSObject的子類都支持這個機制。拿上面的漸變導航欄做,我們?yōu)閠ableView添加了一個監(jiān)聽者controller,在我們滑動列表的時候,會計算當前列表的滾動偏移量,然后改變導航欄的背景色透明度。

//添加監(jiān)聽者[self.tableView addObserver:selfforKeyPath:@"contentOffset"options:NSKeyValueObservingOptionNewcontext:nil];/**

*? 監(jiān)聽屬性值發(fā)生改變時回調(diào)

*/- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context{CGFloatoffset =self.tableView.contentOffset.y;CGFloatdelta = offset /64.f +1.f;? ? delta = MAX(0, delta);? ? [selfalphaNavController].barAlpha = MIN(1, delta);}

毫無疑問,kvo是一種非常便捷的回調(diào)方式,但是編譯器是怎么完成監(jiān)聽這個任務的呢?先來看看蘋果文檔對于KVO的實現(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)聽的注冊后,編譯器會修改監(jiān)聽對象(上文中的tableView)的isa指針,讓這個指針指向一個新生成的中間類。從某個意義上來說,這是一場騙局。

typedefstructobjc_class*Class;typedefstructobjc_object{Class isa;} *id;

這里要說明的是isa這個指針,isa是一個Class類型的指針,對象的首地址一般是isa變量,同時isa又保存了對象的類對象的首地址。我們通過object_getClass方法來獲取這個對象的元類,即是對象的類對象的類型(正常來說,class方法內(nèi)部的實現(xiàn)就是獲取這個isa保存的對象的類型,在kvo的實現(xiàn)中蘋果對被監(jiān)聽對象的class方法進行了重寫隱藏了實現(xiàn))。class方法是獲得對象的類型,雖然這兩個返回的結(jié)果是一樣的,但是兩個方法在本質(zhì)上得到的結(jié)果不是同一個東西

在oc中,規(guī)定了只要擁有isa指針的變量,通通都屬于對象。上面的objc_object表示的是NSObject這個類的結(jié)構體表示,因此oc不允許出現(xiàn)非NSObject子類的對象

(block是一個特殊的例外)*

當然了,蘋果并不想講述更多的實現(xiàn)細節(jié),但是我們可以通過運行時機制來完成一些有趣的調(diào)試。

蘋果的黑魔法

根據(jù)蘋果的說法,在對象完成監(jiān)聽注冊后,修改了被監(jiān)聽對象的某些屬性,并且改變了isa指針,那么我們可以在監(jiān)聽前后輸出被監(jiān)聽對象的相關屬性來進一步探索kvo的原理。為了保證能夠得到對象的真實類型,我使用了object_getClass方法,這個方法在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:selfforKeyPath:@"contentOffset"options:NSKeyValueObservingOptionNewcontext: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));

在看官們運行這段代碼之前,可以先思考一下上面的代碼會輸出什么。

2015-12-1223:02:33.216LXDAlphaNavigationController[1487:63171] address:0x7f927a81d2002015-12-1223:02:33.216LXDAlphaNavigationController[1487:63171]classmethod:UITableView2015-12-1223:02:33.217LXDAlphaNavigationController[1487:63171] description method: ; layer = ; contentOffset: {0,0}; contentSize: {600,0}>2015-12-1223:02:33.217LXDAlphaNavigationController[1487:63171] use runtime to getclass:UITableView2015-12-1223:02:33.217LXDAlphaNavigationController[1487:63171] ===================================================2015-12-1223:02:33.218LXDAlphaNavigationController[1487:63171] address:0x7f927a81d2002015-12-1223:02:33.218LXDAlphaNavigationController[1487:63171]classmethod:UITableView2015-12-1223:02:33.218LXDAlphaNavigationController[1487:63171] description method: ; layer = ; contentOffset: {0,0}; contentSize: {600,0}>2015-12-1223:02:33.230LXDAlphaNavigationController[1487:63171] use runtime to getclassNSKVONotifying_UITableView

除了通過object_getClass獲取的類型之外,其他的輸出沒有任何變化。class方法跟description方法可以重寫實現(xiàn)上面的效果,但是為什么連地址都是一樣的。

這里可以通過一句小代碼來說明一下:

NSLog(@"%@, %@",self.class,super.class);

上面這段代碼不管你怎么輸出,兩個結(jié)果都是一樣的。這是由于super本質(zhì)上指向的是父類內(nèi)存。這話說起來有點繞口,但是我們可以通過對象內(nèi)存圖來表示:

need-to-insert-img

類的內(nèi)存

每一個對象占用的內(nèi)存中,一部分是父類屬性占用的;在父類占用的內(nèi)存中,又有一部分是父類的父類占用的。前文已經(jīng)說過isa指針指向的是父類,因此在這個圖中,Son的地址從Father開始,F(xiàn)ather的地址從NSObject開始,這三個對象內(nèi)存的地址都是一樣的。通過這個,我們可以猜到蘋果文檔中所提及的中間類就是被監(jiān)聽對象的子類。并且為了隱藏實現(xiàn),蘋果還重寫了這個子類的class方法跟description方法來掩人耳目。另外,我們還看到了新類相對于父類添加了一個NSKVONotifying_前綴,添加這個前綴是為了避免多次創(chuàng)建監(jiān)聽子類,節(jié)省資源

怎么實現(xiàn)類似效果

既然知道了蘋果的實現(xiàn)過程,那么我們可以自己動手通過運行時機制來實現(xiàn)KVO。runtime允許我們在程序運行時動態(tài)的創(chuàng)建新類、拓展方法、method-swizzling、綁定屬性等等這些有趣的事情。

在創(chuàng)建新類之前,我們應該學習蘋果的做法,判斷當前是否存在這個類,如果不存在我們再進行創(chuàng)建,并且重新實現(xiàn)這個新類的class方法來掩蓋具體實現(xiàn)。基于這些原則,我們用下面的方法來獲取新類

- (Class)createKVOClassWithOriginalClassName: (NSString*)className{NSString* kvoClassName = [kLXDkvoClassPrefix stringByAppendingString: className];? ? Class observedClass =NSClassFromString(kvoClassName);if(observedClass) {returnobservedClass; }//創(chuàng)建新類,并且添加LXDObserver_為類名新前綴Class originalClass = object_getClass(self);? ? Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String,0);//獲取監(jiān)聽對象的class方法實現(xiàn)代碼,然后替換新建類的class實現(xiàn)Method classMethod = class_getInstanceMethod(originalClass,@selector(class));constchar* types = method_getTypeEncoding(classMethod);? ? class_addMethod(kvoClass,@selector(class), (IMP)kvo_Class, types);? ? objc_registerClassPair(kvoClass);returnkvoClass;}

另外,在判斷是否需要中間類來完成監(jiān)聽的注冊前,我們還要判斷監(jiān)聽的屬性的有效性。通過獲取變量的setter方法名(將首字母大寫并加上前綴set),以此來獲取setter實現(xiàn),如果不存在實現(xiàn)代碼,則拋出異常使程序崩潰。

SEL setterSelector =NSSelectorFromString(setterForGetter(key));Method setterMethod = class_getInstanceMethod([selfclass], setterSelector);if(!setterMethod) {@throw[NSExceptionexceptionWithName:NSInvalidArgumentExceptionreason: [NSStringstringWithFormat:@"unrecognized selector sent to instance %p",self] userInfo:nil];return;}Class observedClass = object_getClass(self);NSString* className =NSStringFromClass(observedClass);//如果被監(jiān)聽者沒有LXDObserver_,那么判斷是否需要創(chuàng)建新類if(![className hasPrefix: kLXDkvoClassPrefix]) {? ? observedClass = [selfcreateKVOClassWithOriginalClassName: className];? ? object_setClass(self, observedClass);}//重新實現(xiàn)setter方法,使其完成constchar* types = method_getTypeEncoding(setterMethod);class_addMethod(observedClass, setterSelector, (IMP)KVO_setter, types);

在重新實現(xiàn)setter方法的時候,有兩個重要的方法:willChangeValueForKey和didChangeValueForKey,分別在賦值前后進行調(diào)用。此外,還要遍歷所有的回調(diào)監(jiān)聽者,然后通知這些監(jiān)聽者:

staticvoidKVO_setter(idself, SEL _cmd,idnewValue){NSString* setterName =NSStringFromSelector(_cmd);NSString* getterName = getterForSetter(setterName);if(!getterName) {@throw[NSExceptionexceptionWithName:NSInvalidArgumentExceptionreason: [NSStringstringWithFormat:@"unrecognized selector sent to instance %p",self] userInfo:nil];return;? ? }idoldValue = [selfvalueForKey: getterName];structobjc_super superClass = {? ? ? ? .receiver =self,? ? ? ? .super_class = class_getSuperclass(object_getClass(self))? ? };? ? [selfwillChangeValueForKey: getterName];void(*objc_msgSendSuperKVO)(void*, SEL,id) = (void*)objc_msgSendSuper;? ? objc_msgSendSuperKVO(&superClass, _cmd, newValue);? ? [selfdidChangeValueForKey: getterName];//獲取所有監(jiān)聽回調(diào)對象進行回調(diào)NSMutableArray* observers = objc_getAssociatedObject(self, (__bridgeconstvoid*)kLXDkvoAssiociateObserver);for(LXD_ObserverInfo * infoinobservers) {if([info.key isEqualToString: getterName]) {dispatch_async(dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{? ? ? ? ? ? info.handler(self, getterName, oldValue, newValue);? ? ? ? ? ? });? ? ? ? }? ? }}

所有的監(jiān)聽者通過動態(tài)綁定的方式將其存儲起來,但這樣也會產(chǎn)生強引用,所以我們還需要提供釋放監(jiān)聽的方法:

- (void)LXD_removeObserver:(NSObject*)object forKey:(NSString*)key{NSMutableArray* observers = objc_getAssociatedObject(self, (__bridgevoid*)kLXDkvoAssiociateObserver);? ? LXD_ObserverInfo * observerRemoved =nil;for(LXD_ObserverInfo * observerInfoinobservers) {if(observerInfo.observer == object && [observerInfo.key isEqualToString: key]) {? ? ? ? ? ? ? ? ? ? observerRemoved = observerInfo;break;? ? ? ? }? ? }? ? [observers removeObject: observerRemoved];}

雖然上面已經(jīng)粗略的實現(xiàn)了kvo,并且我們還能自定義回調(diào)方式。使用target-action或者block的方式進行回調(diào)會比單一的系統(tǒng)回調(diào)要全面的多。但kvo真正的實現(xiàn)并沒有這么簡單,上述代碼目前只能實現(xiàn)對象類型的監(jiān)聽,基本類型無法監(jiān)聽,況且還有keyPath可以監(jiān)聽對象的成員對象的屬性這種更強大的功能。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 在iOS開發(fā)中,蘋果提供了許多機制給我們進行回調(diào)。KVO(key-value-observing)是一種十分有趣的...
    流沙3333閱讀 426評論 0 0
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,072評論 0 9
  • 序言在iOS開發(fā)中,蘋果提供了許多機制給我們進行回調(diào)。KVO(key-value-observing)是一種十分有...
    陌尚煙雨遙閱讀 557評論 0 0
  • 設計模式是什么? 你知道哪些設計模式,并簡要敘述? 設計模式是一種編碼經(jīng)驗,就是用比較成熟的邏輯去處理某一種類型的...
    iOS菜鳥大大閱讀 812評論 0 1
  • 寫在前面 程序設計語言中有各種各樣的設計模式(pattern)和與此對應的反設計模式(anti-pattern),...
    Frankxp閱讀 5,012評論 0 23

友情鏈接更多精彩內(nèi)容