iOS面試題·項(xiàng)目中用過(guò) Runtime 嗎?
前言
我們知道靜態(tài)語(yǔ)言在編譯時(shí)期,就已經(jīng)確定了函數(shù)的具體調(diào)用,而動(dòng)態(tài)語(yǔ)言要等到運(yùn)行時(shí)期才能真正確定調(diào)用哪個(gè)函數(shù); Objective-C 是一門(mén)動(dòng)態(tài)語(yǔ)言,它是通過(guò) Runtime 這個(gè)運(yùn)行時(shí)機(jī)制來(lái)實(shí)現(xiàn)的。
雖然說(shuō) Runtime 是相對(duì)于底層的機(jī)制,但是在項(xiàng)目過(guò)程中也經(jīng)常用來(lái)解決一些問(wèn)題。下面我們就來(lái)看看利用 Runtime 可以解決項(xiàng)目中什么問(wèn)題。
項(xiàng)目中用 Runtime 實(shí)現(xiàn)的功能
利用關(guān)聯(lián)對(duì)象為分類增加偽屬性
在項(xiàng)目的開(kāi)發(fā)中,經(jīng)常會(huì)遇到要為已經(jīng)存在的類添加屬性。面對(duì)這種情況,我們一般都是創(chuàng)建一個(gè)分類,來(lái)實(shí)現(xiàn)為已有的類增加屬性,但是由于分類結(jié)構(gòu)的特殊性,在分類添加屬性,并不會(huì)為我們自動(dòng)創(chuàng)建實(shí)例變量和存儲(chǔ)方法。
首先我們要知道,常規(guī)定義一個(gè) @property,其實(shí)編譯器會(huì)為我們做三件事情:
- 生成實(shí)例變量 _property
- 生成 getter 方法
- 生成 setter 方法
但是,在分類中并不會(huì)幫我們?nèi)ド蓪?shí)例變量和存取方法,所以我們需要自己去實(shí)現(xiàn)存取方法,這里我們會(huì)通過(guò)關(guān)聯(lián)對(duì)象去將鍵值關(guān)聯(lián)到對(duì)象上面去,以下是代碼示例:
- @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);
- }
這個(gè)我們暫時(shí)只講如何通過(guò)關(guān)聯(lián)對(duì)象為分類增加偽屬性,至于分類為什么不會(huì)為我們自動(dòng)添加實(shí)例變量和存取方法,以及關(guān)聯(lián)對(duì)象的實(shí)現(xiàn)原理等,我們會(huì)在后面的面試題繼續(xù)涉及到這一話題。
利用 Method Swizzling 交換方法
我們可以用 Method Swizzling 來(lái)交換兩個(gè)方法的實(shí)現(xiàn),以便達(dá)到 Hook 的效果;例如交換 ViewController 生命周期方法來(lái)實(shí)現(xiàn)頁(yè)面埋點(diǎn),或者在不影響原有的功能增加一些特殊的功能。
交換方法主要是利用到 Runtime 中的class_addMethod 、class_replaceMethod、method_exchangeImplementations 方法來(lái)實(shí)現(xiàn)的,以下是 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沒(méi)有實(shí)現(xiàn)IMP的情況
- BOOL didAddMethod = class_addMethod(class, originalSeletor, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
- if (didAddMethod) {
- //添加成功:說(shuō)明源SEL沒(méi)有實(shí)現(xiàn)IMP,將源SEL的IMP替換到交換SEL的IMP
- class_replaceMethod(class, swizzledSeletor, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
- } else {
- //添加失?。赫f(shuō)明源SEL已經(jīng)有IMP,直接將兩個(gè)SEL的IMP交換即可
- method_exchangeImplementations(originMethod, swizzledMethod);
- }
- }
利用 class_copyIvarList 實(shí)現(xiàn) NSCoding 的自動(dòng)歸檔解檔
在利用 NSKeyedArchiver 歸檔解檔對(duì)象的時(shí)候,對(duì)象 Model 需要實(shí)現(xiàn) NSCoding 協(xié)議,并且要實(shí)現(xiàn) encodeWithCoder、initWithCoder 兩個(gè)方法,在這兩個(gè)方法中要為每個(gè)屬性進(jìn)行 code 和 encode,不然就會(huì) crash。
在項(xiàng)目開(kāi)發(fā)過(guò)程中,經(jīng)常會(huì)出現(xiàn) Model 中的屬性會(huì)變更,這個(gè)時(shí)候總是會(huì)忘記去修改對(duì)應(yīng)的屬性 code 和 encode,這里就會(huì)導(dǎo)致 crash;為了避免這個(gè)現(xiàn)象和讓 Model 中的方法更加簡(jiǎn)潔可控,這里我們會(huì)利用 class_copyIvarList 來(lái)獲取對(duì)象中的成員變量列表,然后利用 KVC 來(lái) code 和 encode。實(shí)例代碼如下:(這里我們將這個(gè)通用的代碼抽象成宏,這樣子在需要的 Model 中直接調(diào)用就可以了)
- #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;\
- \
- // 對(duì)應(yīng)調(diào)用
- - (void)encodeWithCoder:(NSCoder *)aCoder {
- PXYNSCodingRuntime_EncodeWithCoder(Father)
- }
- - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
- PXYNSCodingRuntime_InitWithCoder(Father)
- }
利用 objc_allocateClassPair、object_setClass 等 API 來(lái)實(shí)現(xiàn) KVO Block
在項(xiàng)目中,會(huì)經(jīng)常使用 KVO 來(lái)監(jiān)聽(tīng)某個(gè)屬性的變化。先給出系統(tǒng)調(diào)用的方式,添加監(jiān)聽(tīng)后,在 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(@"%@ 對(duì)象的 %@ 屬性改變了:%@",object,keyPath,change);
- }
但是在開(kāi)發(fā)過(guò)程中,有時(shí)候想將代碼增加內(nèi)聚性和在 observeValueForKeyPath 減少判斷,我們可以通過(guò) Runtime 來(lái)實(shí)現(xiàn)一個(gè) KVO Block,這樣調(diào)用地方即處理消息的地方,代碼上比較直觀,簡(jiǎn)單 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 主要是動(dòng)態(tài)派生出一個(gè)中間類,然后在這個(gè)中間類處理相關(guān)通知邏輯,具體代碼可以 Demo 中的 NSObject+PXYKVO 具體實(shí)現(xiàn);
利用消息轉(zhuǎn)發(fā)機(jī)制實(shí)現(xiàn)多播委托(蹦床模式)
首先,在對(duì)象收到無(wú)法處理的消息之后,會(huì)執(zhí)行消息轉(zhuǎn)發(fā),消息轉(zhuǎn)發(fā)有三個(gè)步驟:
- 調(diào)用 resolveInstanceMethod 方法。動(dòng)態(tài)方法解析,這里會(huì)給類使用 class_addMethod 來(lái)增加方法的機(jī)會(huì)。
- 調(diào)用 forwardingTargetForSelector 方法,看是否有備用接收者,將消息轉(zhuǎn)發(fā)給備用接收者處理。
- 調(diào)用 methodSignatureForSelector 和 forwardInvocation 方法,進(jìn)行完成的消息轉(zhuǎn)發(fā)。
如果經(jīng)過(guò)上面三個(gè)步驟,還不能正確處理消息,程序就會(huì)走 doesNotRecognizeSelector 方法,crash 掉。
蹦床模式:就是把一條消息 “反彈” 到另外一個(gè)對(duì)象,蹦床一般使用 forwardInvocation 來(lái)實(shí)現(xiàn)。
在項(xiàng)目開(kāi)發(fā)中,事件回調(diào)一般使用:Block、Delegate、NSNotificationCenter;但是在多個(gè)模塊需要監(jiān)聽(tīng)一個(gè)事件的場(chǎng)景:使用通知會(huì)將項(xiàng)目變得不可控,因?yàn)槿魏我粋€(gè)地方都可以監(jiān)聽(tīng)這個(gè)通知,在排查問(wèn)題的時(shí)候就會(huì)變得異常困難,這個(gè)時(shí)候我們可以使用多播委托,實(shí)現(xiàn)一對(duì)多回調(diào)。
大致原理:實(shí)現(xiàn)一個(gè)管理類,將需要回調(diào)的對(duì)象注冊(cè)進(jìn)來(lái),然后將事件消息發(fā)送給這個(gè)管理類,由于這個(gè)管理類是沒(méi)有實(shí)現(xiàn)委托方法的,就不能正常處理這個(gè)消息,這個(gè)時(shí)候就會(huì)走消息轉(zhuǎn)發(fā)流程;然后我們通過(guò)消息轉(zhuǎn)發(fā)流程,將消息轉(zhuǎn)發(fā)到注冊(cè)進(jìn)來(lái)的對(duì)象中去,這樣子就要可以實(shí)現(xiàn)我們的多播委托了。
具體代碼可以看 Demo 中的 PXYMulticastDelegate 多播委托實(shí)現(xiàn)類。
總結(jié)
Objective-C 利用 Runtime 運(yùn)行時(shí)變成一門(mén)動(dòng)態(tài)語(yǔ)言,在開(kāi)發(fā)過(guò)程中,使用 Runtime 相關(guān) API 可以實(shí)現(xiàn)一些很強(qiáng)大的功能,這里我們簡(jiǎn)單講到使用 Runtime 完成為分類增加偽屬性、利用 Method SWizzling 來(lái) Hook 方法、實(shí)現(xiàn) NSCoding 自動(dòng)歸檔解檔、實(shí)現(xiàn) KVO Block、多播委托。
當(dāng)然還可以實(shí)現(xiàn)更多的功能,比如字典模型之間的轉(zhuǎn)換、頁(yè)面無(wú)侵入埋點(diǎn)、監(jiān)聽(tīng) App 網(wǎng)絡(luò)流量等等。
還有可以實(shí)現(xiàn)什么好玩的功能,歡迎留言,感激不盡。