iOS面試題·項目中用過 Runtime 嗎?
前言
我們知道靜態語言在編譯時期,就已經確定了函數的具體調用,而動態語言要等到運行時期才能真正確定調用哪個函數; Objective-C 是一門動態語言,它是通過 Runtime 這個運行時機制來實現的。
雖然說 Runtime 是相對于底層的機制,但是在項目過程中也經常用來解決一些問題。下面我們就來看看利用 Runtime 可以解決項目中什么問題。
項目中用 Runtime 實現的功能
利用關聯對象為分類增加偽屬性
在項目的開發中,經常會遇到要為已經存在的類添加屬性。面對這種情況,我們一般都是創建一個分類,來實現為已有的類增加屬性,但是由于分類結構的特殊性,在分類添加屬性,并不會為我們自動創建實例變量和存儲方法。
首先我們要知道,常規定義一個 @property,其實編譯器會為我們做三件事情:
- 生成實例變量 _property
- 生成 getter 方法
- 生成 setter 方法
但是,在分類中并不會幫我們去生成實例變量和存取方法,所以我們需要自己去實現存取方法,這里我們會通過關聯對象去將鍵值關聯到對象上面去,以下是代碼示例:
- @property (nonatomic, strong) NSString *title;
- - (NSString *)title {
- return objc_getAssociatedObject(self, _cmd);
- }
- - (void)setTitle:(NSString *)title {
- objc_setAssociatedObject(self, @selector(title), title, OBJC_ASSOCIATION_RETAIN);
- }
這個我們暫時只講如何通過關聯對象為分類增加偽屬性,至于分類為什么不會為我們自動添加實例變量和存取方法,以及關聯對象的實現原理等,我們會在后面的面試題繼續涉及到這一話題。
利用 Method Swizzling 交換方法
我們可以用 Method Swizzling 來交換兩個方法的實現,以便達到 Hook 的效果;例如交換 ViewController 生命周期方法來實現頁面埋點,或者在不影響原有的功能增加一些特殊的功能。
交換方法主要是利用到 Runtime 中的class_addMethod 、class_replaceMethod、method_exchangeImplementations 方法來實現的,以下是 Method Swizzling 代碼示例:
- /**
- 交換方法
- */
- + (void)pxy_swizzleMethodWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector {
- Class class = [self class];
- SEL originalSeletor = originalSelector;
- SEL swizzledSeletor = swizzledSelector;
- Method originMethod = class_getInstanceMethod(class, originalSeletor);
- Method swizzledMethod = class_getInstanceMethod(class, swizzledSeletor);
- //先嘗試給源SEL添加IMP,這里是為了避免源SEL沒有實現IMP的情況
- BOOL didAddMethod = class_addMethod(class, originalSeletor, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
- if (didAddMethod) {
- //添加成功:說明源SEL沒有實現IMP,將源SEL的IMP替換到交換SEL的IMP
- class_replaceMethod(class, swizzledSeletor, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
- } else {
- //添加失敗:說明源SEL已經有IMP,直接將兩個SEL的IMP交換即可
- method_exchangeImplementations(originMethod, swizzledMethod);
- }
- }
利用 class_copyIvarList 實現 NSCoding 的自動歸檔解檔
在利用 NSKeyedArchiver 歸檔解檔對象的時候,對象 Model 需要實現 NSCoding 協議,并且要實現 encodeWithCoder、initWithCoder 兩個方法,在這兩個方法中要為每個屬性進行 code 和 encode,不然就會 crash。
在項目開發過程中,經常會出現 Model 中的屬性會變更,這個時候總是會忘記去修改對應的屬性 code 和 encode,這里就會導致 crash;為了避免這個現象和讓 Model 中的方法更加簡潔可控,這里我們會利用 class_copyIvarList 來獲取對象中的成員變量列表,然后利用 KVC 來 code 和 encode。實例代碼如下:(這里我們將這個通用的代碼抽象成宏,這樣子在需要的 Model 中直接調用就可以了)
- #define PXYNSCodingRuntime_EncodeWithCoder(Class) \
- unsigned int outCount = 0;\
- Ivar *ivars = class_copyIvarList([Class class], &outCount);\
- for (int i = 0; i < outCount; i++) {\
- Ivar ivar = ivars[i];\
- NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];\
- [aCoder encodeObject:[self valueForKey:key] forKey:key];\
- }\
- free(ivars);\
- \
- #define PXYNSCodingRuntime_InitWithCoder(Class)\
- if (self = [super init]) {\
- unsigned int outCount = 0;\
- Ivar *ivars = class_copyIvarList([Class class], &outCount);\
- for (int i = 0; i < outCount; i++) {\
- Ivar ivar = ivars[i];\
- NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];\
- id value = [aDecoder decodeObjectForKey:key];\
- if (value) {\
- [self setValue:value forKey:key];\
- }\
- }\
- free(ivars);\
- }\
- return self;\
- \
- // 對應調用
- - (void)encodeWithCoder:(NSCoder *)aCoder {
- PXYNSCodingRuntime_EncodeWithCoder(Father)
- }
- - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
- PXYNSCodingRuntime_InitWithCoder(Father)
- }
利用 objc_allocateClassPair、object_setClass 等 API 來實現 KVO Block
在項目中,會經常使用 KVO 來監聽某個屬性的變化。先給出系統調用的方式,添加監聽后,在 observeValueForKeyPath 方法中處理變化:
- - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
- NSLog(@"%@ 對象的 %@ 屬性改變了:%@",object,keyPath,change);
- }
但是在開發過程中,有時候想將代碼增加內聚性和在 observeValueForKeyPath 減少判斷,我們可以通過 Runtime 來實現一個 KVO Block,這樣調用地方即處理消息的地方,代碼上比較直觀,簡單 API 如下:
- typedef void(^PXYKVOCompleteBlock)(id observer, NSString *keyPath, id oldValue, id newValue);
- /**
- 添加 KVO Block
- */
- - (void)pxy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath completeBlock:(PXYKVOCompleteBlock)completeBlock;
- /**
- 移除 KVO Block
- */
- - (void)pxy_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
KVO 主要是動態派生出一個中間類,然后在這個中間類處理相關通知邏輯,具體代碼可以 Demo 中的 NSObject+PXYKVO 具體實現;
利用消息轉發機制實現多播委托(蹦床模式)
首先,在對象收到無法處理的消息之后,會執行消息轉發,消息轉發有三個步驟:
- 調用 resolveInstanceMethod 方法。動態方法解析,這里會給類使用 class_addMethod 來增加方法的機會。
- 調用 forwardingTargetForSelector 方法,看是否有備用接收者,將消息轉發給備用接收者處理。
- 調用 methodSignatureForSelector 和 forwardInvocation 方法,進行完成的消息轉發。
如果經過上面三個步驟,還不能正確處理消息,程序就會走 doesNotRecognizeSelector 方法,crash 掉。
蹦床模式:就是把一條消息 “反彈” 到另外一個對象,蹦床一般使用 forwardInvocation 來實現。
在項目開發中,事件回調一般使用:Block、Delegate、NSNotificationCenter;但是在多個模塊需要監聽一個事件的場景:使用通知會將項目變得不可控,因為任何一個地方都可以監聽這個通知,在排查問題的時候就會變得異常困難,這個時候我們可以使用多播委托,實現一對多回調。
大致原理:實現一個管理類,將需要回調的對象注冊進來,然后將事件消息發送給這個管理類,由于這個管理類是沒有實現委托方法的,就不能正常處理這個消息,這個時候就會走消息轉發流程;然后我們通過消息轉發流程,將消息轉發到注冊進來的對象中去,這樣子就要可以實現我們的多播委托了。
具體代碼可以看 Demo 中的 PXYMulticastDelegate 多播委托實現類。
總結
Objective-C 利用 Runtime 運行時變成一門動態語言,在開發過程中,使用 Runtime 相關 API 可以實現一些很強大的功能,這里我們簡單講到使用 Runtime 完成為分類增加偽屬性、利用 Method SWizzling 來 Hook 方法、實現 NSCoding 自動歸檔解檔、實現 KVO Block、多播委托。
當然還可以實現更多的功能,比如字典模型之間的轉換、頁面無侵入埋點、監聽 App 網絡流量等等。
還有可以實現什么好玩的功能,歡迎留言,感激不盡。