在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,在我們滑動列表的時候,會計算當前列表的滾動偏移量,然后改變導航欄的背景色透明度。
12//添加監(jiān)聽者
[self.tableView?addObserver:?self ?forKeyPath:?@"contentOffset"options:?NSKeyValueObservingOptionNew?context:?nil];
/**
*監(jiān)聽屬性值發(fā)生改變時回調(diào)
*/
-?(void)observeValueForKeyPath:(NSString?*)keyPath?ofObject:(id)object?change:(NSDictionary?*)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);
}
毫無疑問,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指針,讓這個指針指向一個新生成的中間類。從某個意義上來說,這是一場騙局。
typedef?struct?objc_class?*Class;
typedef?struct?objc_object?{
Class?isa;
}?*id;
這里要說明的是isa這個指針,isa是一個Class類型的指針,用來指向類的類型,我們可以通過object_getClass方法來獲取這個值(正常來說,class方法內(nèi)部的實現(xiàn)就是獲取這個isa指針,但是在kvo中蘋果對監(jiān)聽對象的這個方法進行了重寫。在oc中,規(guī)定了只要擁有isa指針的變量,通通都屬于對象。上面的objc_object表示的是NSObject這個類的結構體表示,因此oc不允許出現(xiàn)非NSObject子類的對象(block是一個特殊的例外)
蘋果的黑魔法
根據(jù)蘋果的說法,在對象完成監(jiān)聽注冊后,修改了被監(jiān)聽對象的某些屬性,并且改變了isa指針,那么我們可以在監(jiān)聽前后輸出被監(jiān)聽對象的相關屬性來進一步探索kvo的原理。為了保證能夠得到對象的真實類型,我使用了object_getClass方法(class方法本質上是調(diào)用這個函數(shù)),這個方法在runtime.h頭文件中
10NSLog(@"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));
在看官們運行這段代碼之前,可以先思考一下上面的代碼會輸出什么。
72015-12-12?23:02:33.216?LXDAlphaNavigationController[1487:63171]?address:?0x7f927a81d200
2015-12-12?23:02:33.216?LXDAlphaNavigationController[1487:63171]?class?method:?UITableView
2015-12-12?23:02:33.217?LXDAlphaNavigationController[1487:63171]?description?method:?2015-12-12?23:02:33.217?LXDAlphaNavigationController[1487:63171]?use?runtime?to?get?class:?UITableView
2015-12-12?23:02:33.217?LXDAlphaNavigationController[1487:63171]?===================================================
2015-12-12?23:02:33.218?LXDAlphaNavigationController[1487:63171]?address:?0x7f927a81d200
2015-12-12?23:02:33.218?LXDAlphaNavigationController[1487:63171]?class?method:?UITableView
2015-12-12?23:02:33.218?LXDAlphaNavigationController[1487:63171]?description?method:?2015-12-12?23:02:33.230?LXDAlphaNavigationController[1487:63171]?use?runtime?to?get?class?NSKVONotifying_UITableView
除了通過object_getClass獲取的類型之外,其他的輸出沒有任何變化。class方法跟description方法可以重寫實現(xiàn)上面的效果,但是為什么連地址都是一樣的。
這里可以通過一句小代碼來說明一下:
NSLog(@"%@,?%@",?self.class,super.class);
上面這段代碼不管你怎么輸出,兩個結果都是一樣的。這是由于super本質上指向的是父類內(nèi)存。這話說起來有點繞口,但是我們可以通過對象內(nèi)存圖來表示:

類的內(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));
const?char?*?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([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_,那么判斷是否需要創(chuàng)建新類
if(![className?hasPrefix:?kLXDkvoClassPrefix])?{
observedClass?=?[self?createKVOClassWithOriginalClassName:?className];
//object_setClass將一個對象設置為別的類類型,返回原來的Class
object_setClass(self,?observedClass);
}
//重新實現(xiàn)setter方法,使其完成
const?char?*?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)聽者:
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)對象進行回調(diào)
NSMutableArray?*?observers?=?objc_getAssociatedObject(self,?(__bridge?const?void?*)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,?(__bridge?void?*)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)聽對象的成員對象的屬性這種更強大的功能。
尾言
對于基本類型的監(jiān)聽,蘋果可能是通過void *類型對對象進行橋接轉換,然后直接獲取內(nèi)存,通過type encoding我們可以獲取所有setter對象的具體類型,雖然實現(xiàn)比較麻煩,但是確實能夠達成類似的效果。
鉆研kvo的實現(xiàn)可以讓我們對蘋果的代碼實現(xiàn)有更深層次的了解,這些知識涉及到了更深層次的技術,探究它們對我們的開發(fā)視野有著很重要的作用。同時,對比其他的回調(diào)方式,KVO的實現(xiàn)在創(chuàng)建子類、重寫方法等等方面的內(nèi)存消耗是很巨大的,因此博主更加推薦使用delegate、block等回調(diào)方式,甚至直接使用method-swizzling來替換這種重寫setter方式也是可行的。