本文涉及到兩個(gè)面試中經(jīng)常被問到的知識(shí)點(diǎn),runtime 和 KVO。希望通過本文將兩個(gè)知識(shí)點(diǎn)結(jié)合起來一起學(xué)習(xí),加深印象。
Runtime 經(jīng)常被用來做什么?
- 動(dòng)態(tài)添加屬性
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy) // 給某個(gè)對(duì)象添加一個(gè)屬性
- 動(dòng)態(tài)添加類
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name,
size_t extraBytes) // 生成類
objc_registerClassPair(Class _Nonnull cls) // 注冊(cè)類
- 動(dòng)態(tài)添加方法
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types) // 添加方法
- 交換方法實(shí)現(xiàn)
void method_exchangeImplementations(Method m1, Method m2);
KVO的內(nèi)部實(shí)現(xiàn)原理?
當(dāng)你觀察一個(gè)對(duì)象時(shí),也就是執(zhí)行方法- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;后,一個(gè)新的類會(huì)動(dòng)態(tài)被創(chuàng)建。這個(gè)類繼承自該對(duì)象的原本的類,并重寫了被觀察屬性的 setter 方法。自然,重寫的 setter 方法會(huì)負(fù)責(zé)在調(diào)用原 setter 方法之前和之后,通知所有觀察對(duì)象值的更改。最后把這個(gè)對(duì)象的 isa 指針 ( isa 指針告訴 Runtime 系統(tǒng)這個(gè)對(duì)象的類是什么 ) 指向這個(gè)新創(chuàng)建的子類,對(duì)象就神奇的變成了新創(chuàng)建的子類的實(shí)例。
原來,這個(gè)中間類,繼承自原本的那個(gè)類。不僅如此,Apple 還重寫了 -class 方法,企圖欺騙我們這個(gè)類沒有變,就是原本那個(gè)類。
如圖可以看到 isa 指針的改變:

添加觀察者之前.jpg

添加觀察者之后.jpg
通過 runtime 實(shí)現(xiàn) KVO
1.創(chuàng)建 User 類,繼承于 NSObject。該類包含一個(gè)屬性 name。
2. 給 NSObject添加一個(gè)分類,NSObject+KVO。模仿系統(tǒng)方法,添加一個(gè)方法- (void)xy_addObserver:(NSObject *_Nullable)observer forKeyPath:(NSString *_Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
3.實(shí)現(xiàn)分類方法:
#import "NSObject+KVO.h"
#import <objc/message.h>
@implementation NSObject (KVO)
- (void)xy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
// 獲取當(dāng)前類名
NSString *oldClassName = NSStringFromClass(self.class);
// 給新類取名
NSString *newClassName = [@"xykvo_" stringByAppendingString:oldClassName];
const char *name = [newClassName UTF8String];
// 定義新類
Class myclass = objc_allocateClassPair(self.class, name, 0);
// 注冊(cè)這個(gè)類
objc_registerClassPair(myclass);
// 改變 isa 指針
object_setClass(self, myclass);
// 添加 setName 方法 (重寫)
class_addMethod(myclass, @selector(setName:), (IMP)setName, "v@:@");
// 保存觀察者
objc_setAssociatedObject(self, @"wxy", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
void setName(id self, SEL _cmd, NSString *newName) { // 所有 OC 方法的調(diào)用都會(huì)傳遞兩個(gè)隱式參數(shù) 哪個(gè)對(duì)象的哪個(gè)方法 _cmd文檔就這么寫
NSLog(@"%@-%@-%@", self,NSStringFromSelector(_cmd),newName);
// 拿到當(dāng)前類 Class
id class = [self class];
// 將 self 類型轉(zhuǎn)換為父類,改回 isa 指針
object_setClass(self, class_getSuperclass([self class]));
// 調(diào)用父類的 setName 方法
objc_msgSend(self, @selector(setName:), newName);
// 拿出觀察者
id observer = objc_getAssociatedObject(self, @"wxy");
// 通知外界
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), newName,nil ,nil );
NSLog(@"%@", [self class]);
// 改回子類
object_setClass(self, class);
}
#import "ViewController.h"
#import "User.h"
#import "NSObject+KVO.h"
@interface ViewController ()
@property (nonatomic, strong) User *user;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
User *user = [[User alloc] init];
[user xy_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
_user = user;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@ %@", keyPath, self.user.name);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
_user.name = @"??????";
}
本文的目的在于熟悉 runtime 的基本使用,了解 KVO 的內(nèi)部實(shí)現(xiàn)原理。代碼很少,但是值得品味。