面試驅動技術合集(初中級iOS開發),關注倉庫,及時獲取更新 Interview-series
KVO
- KVO是key-value observing的縮寫
- KVO 是Objective-C對觀察者模式的又一實現
- Apple使用的isa混寫(isa-swizzling)來實現KVO
面試題來襲!
友情提示,智力問答即將開始~
- addObserver:forKeyPath:options:context:各個參數的作用分別是什么,observer中需要實現哪個方法才能獲得KVO回調?
/**
添加KVO監聽
@param observer 添加觀察者,被觀察者屬性變化通知的目標對象
@param keyPath 監聽的屬性路徑
@param options 監聽類型 - options支持按位或來監聽多個事件類型
@param context 監聽上下文context主要用于在多個監聽器對象監聽相同keyPath時進行區分
*/
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
- 需實現這個方法獲得KVO回調
/**
監聽器對象的監聽回調方法
@param keyPath 監聽的屬性路徑
@param object 被觀察者
@param change 監聽內容的變化
@param context context為監聽上下文,由add方法回傳
*/
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context;
** 2. apple用什么方式實現對一個對象的KVO?**
- 答:使用了isa混寫技術(isa-swizzling)
** 3. 接著2追問,什么是isa-swizzling?**
以實際開發中,使用KVO的場景分析:
self.person1 = [[MNPerson alloc]init];
self.person1.age = 1;
NSLog(@"添加KVO之前,person的class是 = %s",object_getClassName(self.person1));
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
NSLog(@"添加KVO之后,person的class是 = %s",object_getClassName(self.person1));
--------------------------------------------------------
2019-03-04 20:59:24 添加KVO之前,person的class是 = MNPerson
2019-03-04 20:59:24 添加KVO之后,person的class是 = NSKVONotifying_MNPerson
what?怎么跑出來一個NSKVONotifying_MNPerson
?person的class 不是MNPerson
嗎?
KVO 原理分析分析
- 查看 NSKVONotifying_MNPerson 類內部的方法
//打印某個類中的所有方法
- (void)printMethonNamesFromClass:(Class)cls{
unsigned int count;
//獲取方法列表
Method *methodList = class_copyMethodList(cls, &count);
//保存方法名
NSMutableString *methonNames = @"".mutableCopy;
for (int i = 0; i < count; i++) {
//獲取方法
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methonNames appendFormat:@"%@", [NSString stringWithFormat:@"%@, ",methodName]];
}
NSLog(@"methonNames = %@",methonNames);
//c語音創建的list記得釋放
free(methodList);
}
結果如下:
[self printMethonNamesFromClass:object_getClass(self.person1)];
----------------------------------------------
methonNames = setAge:, class, dealloc, _isKVOA,
畫圖分析KVO內部結構
- NSKVONotifying_MNPerson 內部為啥要重寫
setAge:
方法呢?
如果自己創建NSKVONotifying_MNPerson
對象,會發現KVO直接失效,因為我們自己創建聲明了一個NSKVONotifying_MNPerson
,導致系統無法動態生成NSKVONotifying_MNPerson
這個類,KVO就失效
[general] KVO failed to allocate class pair for name NSKVONotifying_MNPerson, automatic key-value observing will not work for this class
使用- (IMP)methodForSelector:(SEL)aSelector
函數,獲取實際的方法實現!
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MNPerson alloc]init];
self.person2 = [[MNPerson alloc]init];
NSLog(@"添加KVO之前,person1的setAge是 = %p,person2的setAge是 = %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
NSLog(@"添加KVO之后,person1的setAge是 = %p,person2的setAge是 = %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
}
使用 p(IMP) + 函數地址
,可以查看方法實現!這里可以看到,添加kvo之后,setAge:
被重寫了,變成了_NSSetLongLongValueAndNotify
方法
(lldb) p (IMP)0x10c1107d0
(IMP) $0 = 0x000000010c1107d0 (KVO-Demo`-[MNPerson setAge:] at MNPerson.h:13)
(lldb) p (IMP)0x10c456bf4
(IMP) $1 = 0x000000010c456bf4 (Foundation`_NSSetLongLongValueAndNotify)
和屬性的類型有關,如果age 是 int 類型,重寫的setAge:方法,就是調用 _NSSetIntValueAndNotify
- 使用以下指令,查找Foundation中包含
ValueAndNotify
的方法
nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation | grep ValueAndNotify
可以看到各種類型的_NSSetXXXValueAndNotify
-
可以看到各種類型的_NSSetXXXValueAndNotify
內部實現的探究
因為我們不能手動創建NSKVONotifying_MNPerson
類,為了窺探_NSSetXXXValueAndNotify
內部的實現咋辦? => 在NSKVONotifying_MNPerson
的父類 - MNPerson
里面窺探,(子類會調用父類的super方法)
//偽代碼
@implementation NSKVONotifying_MNPerson
- (void)setAge:(NSInteger)age{
_NSSetLongLongValueAndNotify();
}
void _NSSetLongLongValueAndNotify(){
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
//通知監聽器,屬性值發生了改變
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
- 驗證
- (void)setAge:(NSInteger)age{
NSLog(@"setAge:");
_age = age;
}
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
-----------------------------------------------------------------
2019-03-04 21:53:46.574543+0800 KVO-Demo[55867:7772356] willChangeValueForKey
2019-03-04 21:53:46.575037+0800 KVO-Demo[55867:7772356] setAge:
2019-03-04 21:53:46.575518+0800 KVO-Demo[55867:7772356] didChangeValueForKey - begin
2019-03-04 21:53:46.575822+0800 KVO-Demo[55867:7772356] <MNPerson: 0x60000001aa00>對象的age屬性改變了 = {
kind = 1;
new = 2;
old = 0;
}
2019-03-04 21:53:46.576014+0800 KVO-Demo[55867:7772356] didChangeValueForKey - end
- 回答:什么是isa混寫
- 利用RuntimeAPI動態生成一個子類NSKVONotifying_XXX,并且讓當前的instance對象的isa指向這個全新子類
- 當修改 instance對象的屬性時,會觸發set方法,調用Foundation的 _NSSetXXXValueAndnotify函數
- willChangeValueForKey:
- [super set:](父類原來的setter方法)
- didChangeValueForKey
- 內部觸發監聽器(Oberser)的監聽方法 -
observeValueForKeyPath: ofObject: change: context:
RuntimeAPI :
objc_allocateClassPair
和objc_registerClassPair.
動態生成 NSKVONotifying_XXX
NSKVONotifying_X 類重寫class方法
2019-03-04 22:00:34 添加KVO之前, [self.person2 class] = MNPerson,object_getClass(self.person1) = MNPerson
2019-03-04 22:00:38 添加KVO之后, [self.person2 class] = MNPerson,object_getClass(self.person1) = NSKVONotifying_MNPerson
由上代碼發現 ==> NSKVONotifying_MNPerson
重寫了class 方法,如果通過 object_getClass
得到實際的isa指向的話,發現其真實的類是NSKVONotifying_MNPerson
,那么問題來了,為啥蘋果要重寫class方法呢?
- NSKVONotifying_MNPerson,重寫class方法的原因
Key-Value Observing Programming Guide 對 KVO的描述:
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 ...
人工智能翻譯:使用稱為isa-swizzling的技術實現自動鍵值觀察...當觀察者注冊對象的屬性時,觀察對象的isa指針被修改,指向中間類而不是真正的類,讓開發者只關心他需要關心的類(那些他自己創建出來的類)
人工智障解讀:因為他不想公開這個類,從開發者的角度來看,
NSKVONotifying_MNPerson
并不是用戶創建的,屏蔽內部實現,隱藏NSKVONotifying_MNPerson
類
猜測 NSKVONotifying_MNPerson
內部實現
@implementation NSKVONotifying_MNPerson
- (Class)class{
return [MNPerson class];
}
@END
不重寫的話
@implementation NSObject
- (Class)class{
return object_getClassName(self);
}
@end
不重寫的情況下,使用 [person class]
真實的類就暴露出來了NSKVONotifying_MNPerson
,這是蘋果所不希望看到的
- 如何手動觸發一個value的KVO?
手動調用
- willChangeValueForKey:
- didChangeValueForKey:
老實說,這種一般也只會存在于面試題中,正常開發中基本上不會存在,拿來應付面試足矣~
- 直接修改成員變量會觸發KVO嗎?
@interface MNPerson : NSObject{
//將成員變量暴露出來
@public NSInteger _age;
}
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) MNCar *car;
@end
------------------------------------------------
調用
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MNPerson alloc]init];
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
//直接修改成員變量
self.person1->_age = 20;
}
- 答:直接修改成員變量不會觸發KVO - 沒有調用Setter方法,除非手動觸發KVO
KVC
Key-Value Coding - 鍵值編碼
KVO 常用方法
- (void)setValue:(nullable id)value forKey:(NSString *)key;
就不說了,就簡單的設置對象的屬性值;
KVC和KVO的keyPath一定是屬性么
- KVC 是可以直接設置成員變量的
- KVO 必須手動實現 成員變量的監聽
講一下setValue:forKeyPath: 的作用
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
forKeyPath - 路徑,類似節點,
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MNPerson alloc]init];
self.person1.car = [[MNCar alloc]init];
[self.person1 setValue:@"testCar" forKeyPath:@"car.name"];
NSLog(@"carname = %@",self.person1.car.name);
}
--------------------------------------------------------
2019-03-04 22:37:26 carname = testCar
- 使用KVC,是否會破壞面向對象的編程方法(有違背于面向對象的編程思想)?
- 其實是會的,KVC 可以直接獲取、修改類不想暴露的私有變量,所以會破壞面向對象的編程思想
- TextView 設置placeholder的可以用到
KVC修改屬性是否會觸發KVO
答:會觸發KVO
WHY? (內心毫無波動,甚至有的想打代碼)
所以 - 如果沒有set方法,KVC 也不一定會報錯!
//能否直接訪問成員變量,默認YES
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
老實說,見過有面試題問查找順序的,如果說成員變量查找,比如屬性
name
聲明,會自動生成一個_name
,優先查找還能理解,問之后的什么_isKey
,key 的順序的,個人感覺完全毫無意義啊,并不能僅因為這個順序,就斷定面試者的水平啊,因為正常開發中,總不能有人寫個_name
,又寫個isName
,再寫個_isName
,然后來個你畫我猜,看看哪個順序,這腦瓜子估計得被人打放屁了都。其實這種大致能回答出流程就行了,KVO && KVC 其實考的一般也就到這,要問深度的話,完全可以在其他領域,比如runtime 、 runLoop之類的話題上深入,沒必要糾結具體內部成員變量的查找順序之類的(個人愚見,不喜請噴)
KVO & KVC 的常見考題應該大致逃不出這些了,其實KVO & KVC 在考題上挺常見了,也算是高頻考點了,但是感覺相對來說,題目還是偏初中級。之前有稍微搜下了一些這個話題類似的文字,發現都大同小異,因為一般的技術點也差不多這些,本來在猶豫這篇文章是否要發,后來因為是想做一個面試知識體系系列 (面試驅動技術合集) ,還是丟出來,如有雷同,純屬KVO & KVC 太常見了~請見諒
友情演出:小馬哥MJ
參考資料
Key-Value Observing Programming Guide