「每日一面」iOS 面試題·項目中用過 Runtime 嗎?

前言

我們知道靜態語言在編譯時期,就已經確定了函數的具體調用,而動態語言要等到運行時期才能真正確定調用哪個函數; Objective-C 是一門動態語言,它是通過 Runtime 這個運行時機制來實現的。

雖然說 Runtime 是相對於底層的機制,但是在項目過程中也經常用來解決一些問題。下面我們就來看看利用 Runtime 可以解決項目中什麼問題。

項目中用 Runtime 實現的功能

利用關聯對象為分類增加偽屬性

在項目的開發中,經常會遇到要為已經存在的類添加屬性。面對這種情況,我們一般都是創建一個分類,來實現為已有的類增加屬性,但是由於分類結構的特殊性,在分類添加屬性,並不會為我們自動創建實例變量和存儲方法。

首先我們要知道,常規定義一個 @property,其實編譯器會為我們做三件事情:

  1. 生成實例變量 _property
  2. 生成 getter 方法
  3. 生成 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

*)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具體實現;

利用消息轉發機制實現多播委託(蹦床模式)

首先,在對象收到無法處理的消息之後,會執行消息轉發,消息轉發有三個步驟:

  1. 調用resolveInstanceMethod方法。動態方法解析,這裡會給類使用class_addMethod來增加方法的機會。
  2. 調用forwardingTargetForSelector方法,看是否有備用接收者,將消息轉發給備用接收者處理。
  3. 調用methodSignatureForSelector和forwardInvocation方法,進行完成的消息轉發。

如果經過上面三個步驟,還不能正確處理消息,程序就會走doesNotRecognizeSelector方法,crash 掉。

蹦床模式:就是把一條消息 “反彈” 到另外一個對象,蹦床一般使用forwardInvocation來實現。

在項目開發中,事件回調一般使用:Block、Delegate、NSNotificationCenter;但是在多個模塊需要監聽一個事件的場景:使用通知會將項目變得不可控,因為任何一個地方都可以監聽這個通知,在排查問題的時候就會變得異常困難,這個時候我們可以使用多播委託,實現一對多回調。

大致原理:實現一個管理類,將需要回調的對象註冊進來,然後將事件消息發送給這個管理類,由於這個管理類是沒有實現委託方法的,就不能正常處理這個消息,這個時候就會走消息轉發流程;然後我們通過消息轉發流程,將消息轉發到註冊進來的對象中去,這樣子就要可以實現我們的多播委託了。

具體代碼可以看 Demo 中的PXYMulticastDelegate多播委託實現類。

總結

Objective-C 利用 Runtime 運行時變成一門動態語言,在開發過程中,使用 Runtime 相關 API 可以實現一些很強大的功能,這裡我們簡單講到使用 Runtime 完成 為分類增加偽屬性、利用 Method SWizzling 來 Hook 方法、實現 NSCoding 自動歸檔解檔、實現 KVO Block、多播委託 。

當然還可以實現更多的功能,比如字典模型之間的轉換、頁面無侵入埋點、監聽 App 網絡流量等等。

還有可以實現什麼好玩的功能,歡迎留言,感激不盡。

參考文獻

Associated Objects

Objective-C Associated Objects 的實現原理

http://blog.leichunfeng.com/blog/2015/06/26/objective-c-associated-objects-implementation-principle/

Runtime Method Swizzling開發實例彙總(持續更新中)

https://www.jianshu.com/p/f6dad8e1b848

iOS 模塊分解—「Runtime面試、工作」看我就

https://www.jianshu.com/p/19f280afcb24

「每日一面」iOS 面試題·項目中用過 Runtime 嗎?


分享到:


相關文章: