原文
前言
本文並不是Runtime原理從入門到精通之類的教程, 並不會涉及到過多的原理概念描述, 而是介紹在實際開發中如何使用Runtime解決相應的問題, 具體的應用在之前的兩篇網絡層博客和以後都博客中都會有所體現. 全文約八千字, 預計花費閱讀時間20 - 30分鐘.
目錄
Protobuf解析器
消息轉發三部曲
安全的JSon
安全的數組
多代理
通用打點器
ISA Swizzle 和 Method Swizzle
一、Protobuf解析器
在之前的博客中提到過, 我司網絡層用的是TCP+Protobuf的組合, 請求數據是Protobuf, 返回數據也是Protobuf, 這意味著市面上通用的JSon解析工具在我這並不通用, 於是就自己實現一套類似的解析的工具.
最後的實現效果是:
1.使用方法和已有JSon解析工具完全一致
2.在iPhone6上10000次Protobuf解析(對應Model有20個屬性)時間為0.085s~0.95s, 作為參考, 同樣數據量的JSon解析YYModel是0.08~0.09s, MJExtension則是3.2~3.3s.
具體的使用方法如下:
//SomeModel.h //...正常Property 略過 @property (copy, nonatomic) NSString *HHavatar;//Model屬性聲明和Protobuf不一致 @property (assign, nonatomic) NSInteger HHuserId;//Model屬性聲明和Protobuf不一致 @property (strong, nonatomic) NSArray *albumArray;//Model的屬性是一個數組, 數組裡面又是Model @property (strong, nonatomic) NSArray *strangeAlbumArray;//Model的屬性是一個數組, 數組裡面又是Model 而且Model屬性聲明和Protobuf不一致
//SomeModel.m + (NSDictionary *)replacedPropertyKeypathsForProtobuf { return @{@"HHavatar" : @"avatar", @"HHuserId" : @"userId"}; } + (NSDictionary *)containerPropertyKeypathsForProtobuf { return @{@"albumArray" : @"HHAlbum", @"strangeAlbumArray" : @{kHHObjectClassName : @"HHAlbum", kHHProtobufObjectKeyPath : @"result.albumArray"}}; }
實現思路很簡單: 首先通過class_copyPropertyList獲取輸出對象的變量信息, 然後根據這些變量信息走KVC從輸入對象那裡獲取相應的變量值, 最後走objc_msgSend挨個賦值給輸出對象即可.
ps: 這裡因為我本地的Model用的都是屬性, 所以用class_copyPropertyList就行了, 但像一些老項目可能還是直接聲明實例變量_iVar的話, 就需要用class_copyIvarList了.
具體到代碼中, 總共是如下幾步:
1.獲取輸出對象的變量信息:
typedef enum : NSUInteger { HHPropertyTypeUnknown = 0, HHPropertyTypeVoid = 1, HHPropertyTypeBool = 2, HHPropertyTypeInt8 = 3, HHPropertyTypeUInt8 = 4, HHPropertyTypeInt16 = 5, HHPropertyTypeUInt16 = 6, HHPropertyTypeInt32 = 7, HHPropertyTypeUInt32 = 8, HHPropertyTypeInt64 = 9, HHPropertyTypeUInt64 = 10, HHPropertyTypeFloat = 11, HHPropertyTypeDouble = 12, HHPropertyTypeLongDouble = 13, HHPropertyTypeArray = 14, HHPropertyTypeCustomObject = 15, HHPropertyTypeFoundionObject = 16 } HHPropertyType; @interface HHPropertyInfo : NSObject { @package SEL _setter; SEL _getter; Class _cls; NSString *_name; NSString *_getPath; HHPropertyType _type; } + (instancetype)propertyWithProperty:(objc_property_t)property; @end @interface HHClassInfo : NSObject + (instancetype)classInfoWithClass:(Class)cls ignoreProperties:(NSArray *)ignoreProperties replacePropertyKeypaths:(NSDictionary *)replacePropertyKeypaths; - (NSArray *)properties; @end
#define IgnorePropertyNames @[@"debugDescription", @"description", @"superclass", @"hash"] @implementation HHClassInfo + (instancetype)classInfoWithClass:(Class)cls ignoreProperties:(NSArray *)ignoreProperties replacePropertyKeypaths:(NSDictionary *)replacePropertyKeypaths { HHClassInfo *classInfo = [HHClassInfo new]; classInfo.cls = cls; NSMutableArray *properties = [NSMutableArray array]; while (cls != [NSObject class] && cls != [NSProxy class]) { [properties addObjectsFromArray:[self propertiesWithClass:cls ignoreProperties:ignoreProperties replacePropertyKeypaths:replacePropertyKeypaths]]; cls = [cls superclass]; } classInfo.properties = [properties copy]; return classInfo; } + (NSArray *)propertiesWithClass:(Class)cls ignoreProperties:(NSArray *)ignoreProperties replacePropertyKeypaths:(NSDictionary *)replacePropertyKeypaths { uint count; objc_property_t *properties = class_copyPropertyList(cls, &count); NSMutableArray *propertyInfos = [NSMutableArray array]; NSMutableSet *ignorePropertySet = [NSMutableSet setWithArray:IgnorePropertyNames]; [ignorePropertySet addObjectsFromArray:ignoreProperties]; for (int i = 0; i < count; i++) { objc_property_t property = properties[i]; NSString *propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding]; if ([ignorePropertySet containsObject:propertyName]) { continue; } HHPropertyInfo *propertyInfo = [HHPropertyInfo propertyWithProperty:property]; if (replacePropertyKeypaths.count > 0) { NSString *replaceKey = replacePropertyKeypaths[propertyInfo->_name]; if (replaceKey != nil) { propertyInfo->_getter = NSSelectorFromString(replaceKey); propertyInfo->_getPath = replaceKey; } } [propertyInfos addObject:propertyInfo]; } free(properties); return propertyInfos; } @end
HHClassInfo描述某個類所有需要解析的變量信息, 在其構造方法會根據參數中的類對象, 從該類一直遍歷到基類獲取遍歷過程中拿到的一切變量信息. 在這個過程中, 包裹在ignoreProperties數組中的變量會被忽略, 而在replacePropertyKeypaths中的變量信息會根據映射字典中的聲明進行映射.
HHPropertyInfo描述具體某個變量的相關信息, 包括變量類型, 變量名, 變量取值路徑... 針對我司的具體情況, Type裡面只聲明了基本數據類型, 系統對象, 自定義對象和Array.
需要說明的是Array並不包括在系統對象中, 這是因為Protobuf自己聲明了一個PBArray表示int/bool/long之類的基本數據類型集合, 而系統的NSArray對於基本數據類型都是統一包裝成NSNumber, 兩者不一致, 所以需要特殊處理.
獲取屬性相關信息的具體實現如下:
@implementation HHPropertyInfo NS_INLINE HHPropertyType getPropertyType(const char *type) { switch (*type) { case 'B': return HHPropertyTypeBool; case 'c': return HHPropertyTypeInt8; case 'C': return HHPropertyTypeUInt8; case 's': return HHPropertyTypeInt16; case 'S': return HHPropertyTypeUInt16; case 'i': return HHPropertyTypeInt32; case 'I': return HHPropertyTypeUInt32; case 'l': return HHPropertyTypeInt32; case 'L': return HHPropertyTypeUInt32; case 'q': return HHPropertyTypeInt64; case 'Q': return HHPropertyTypeUInt64; case 'f': return HHPropertyTypeFloat; case 'd': return HHPropertyTypeDouble; case 'D': return HHPropertyTypeLongDouble; case '@': { NSString *typeString = [NSString stringWithCString:type encoding:NSUTF8StringEncoding]; if ([typeString rangeOfString:@"Array"].length > 0) { return HHPropertyTypeArray; } if ([typeString rangeOfString:@"NS"].length > 0) { return HHPropertyTypeFoundionObject; } return HHPropertyTypeCustomObject; }; default: return 0; } } + (instancetype)propertyWithProperty:(objc_property_t)property { HHPropertyInfo *info = [HHPropertyInfo new]; char *propertyAttribute = property_copyAttributeValue(property, "T"); info->_name = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding]; info->_type = getPropertyType(propertyAttribute); info->_setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:",[[info->_name substringToIndex:1] uppercaseString],[info->_name substringFromIndex:1]]); info->_getter = NSSelectorFromString(info->_name); info->_getPath = info->_name; info->_property = property; if (info->_type >= 14) { NSString *propertyClassName = [NSString stringWithCString:propertyAttribute encoding:NSUTF8StringEncoding]; if (![propertyClassName isEqualToString:@"@"]) {//id類型沒有類名 info->_cls = NSClassFromString([[propertyClassName componentsSeparatedByString:@"\""] objectAtIndex:1]); } } free(propertyAttribute); return info; } @end
2.根據具體類的變量信息進行賦值
2.1獲取某個類的變量信息列表:
+ (HHClassInfo *)classInfoToParseProtobuf:(Class)cls { static NSMutableDictionary *objectClasses; static dispatch_semaphore_t lock; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ lock = dispatch_semaphore_create(1); objectClasses = [NSMutableDictionary dictionary]; }); HHClassInfo *classInfo = objectClasses[cls]; if (!classInfo) { //獲取 忽略解析的屬性數組 和 雙方聲明不一致的屬性字典 NSArray *ignoreProperties = [(id)cls respondsToSelector:@selector(igonrePropertiesForProtobuf)] ? [(id)cls igonrePropertiesForProtobuf] : nil; NSDictionary *replacePropertyKeypaths = [(id)cls respondsToSelector:@selector(replacedPropertyKeypathsForProtobuf)] ? [(id)cls replacedPropertyKeypathsForProtobuf] : nil; classInfo = [HHClassInfo classInfoWithClass:cls ignoreProperties:ignoreProperties replacePropertyKeypaths:replacePropertyKeypaths]; dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); objectClasses[(id)cls] = classInfo; dispatch_semaphore_signal(lock); } return classInfo; }
在解析某個類之前, 需要先調用上面的方法獲取該類的變量信息列表, 這個很簡單, 根據Model類和其聲明的忽略規則和映射規則就可以獲取到該類的變量信息列表了. 另外, 因為某個類的變量信息和相應Protobuf解析規則是不變的, 沒有必要每次都獲取, 所以我們將本次拿到的相應信息的緩存一下(這個緩存將解析效率直接提高了8倍).
2.2根據變量信息列表賦值
完整的類變量信息列表拿到以後, 就可以開始實際的解析了:
+ (instancetype)instanceWithProtoObject:(id)protoObject { if (!protoObject) { return nil; } static SEL toNSArraySEL;//PBArray特殊處理 if (toNSArraySEL == nil) { toNSArraySEL = NSSelectorFromString(@"toNSArray"); } Class cls = [self class]; id instance = [self new]; NSArray *properties = [NSObject classInfoToParseProtobuf:cls].properties;//1. 獲取對象的變量信息 NSDictionary *containerPropertyKeypaths;//2.獲取Model中屬性為數組, 數組中也是Model的映射字典 if ([(id)cls respondsToSelector:@selector(containerPropertyKeypathsForProtobuf)]) { containerPropertyKeypaths = [(id)cls containerPropertyKeypathsForProtobuf]; } for (HHPropertyInfo *property in properties) { if (containerPropertyKeypaths[property->_name]) {//針對2中的情況進行處理後賦值 id propertyValue = [self propertyValueForKeypathWithProtoObject:protoObject propertyName:property->_name]; if (propertyValue) { ((void (*)(id, SEL, id))(void *) objc_msgSend)(instance, property->_setter, propertyValue); } } else if ([protoObject respondsToSelector:property->_getter]) { id propertyValue = [protoObject valueForKey:property->_getPath]; if (propertyValue != nil) {//3.通過變量信息進行相應的賦值 HHPropertyType type = property->_type; switch (type) { case HHPropertyTypeBool: case HHPropertyTypeInt8: { if ([propertyValue respondsToSelector:@selector(boolValue)]) { ((void (*)(id, SEL, bool))(void *) objc_msgSend)(instance, property->_setter, [propertyValue boolValue]); } } break; //...略 case HHPropertyTypeCustomObject: { ((void (*)(id, SEL, id))(void *) objc_msgSend)(instance, property->_setter, [property->_cls instanceWithProtoObject:propertyValue]); } break; case HHPropertyTypeArray: { if ([propertyValue respondsToSelector:toNSArraySEL]) {//PBArray特殊處理 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" propertyValue = [propertyValue performSelector:toNSArraySEL]; #pragma clang diagnostic pop } ((void (*)(id, SEL, id))(void *) objc_msgSend)(instance, property->_setter, propertyValue); } break; default: { ((void (*)(id, SEL, id))(void *) objc_msgSend)(instance, property->_setter, propertyValue); } break; } } } } return instance; }
//解析容器類屬性方法 + (id)propertyValueForKeypathWithProtoObject:(id)protoObject propertyName:(NSString *)propertyName { Class cls = self; id map = [[cls containerPropertyKeypathsForProtobuf] objectForKey:propertyName]; NSString *keyPath; Class objectClass; if ([map isKindOfClass:[NSDictionary class]]) { keyPath = [map objectForKey:kHHProtobufObjectKeyPath]; objectClass = NSClassFromString(map[kHHObjectClassName]); } else { keyPath = propertyName; objectClass = NSClassFromString(map); } id value = [protoObject valueForKeyPath:keyPath]; if (![value isKindOfClass:[NSArray class]]) { return [objectClass instanceWithProtoObject:value]; } else { NSMutableArray *mArr = [NSMutableArray array]; for (id message in value) { [mArr addObject:[objectClass instanceWithProtoObject:message]]; } return mArr; } return nil; }
實際的解析過程就是簡單的遍歷變量列表, 根據之前拿到的變量取值路徑, 走KVC獲取相應的變量值, 然後根據相應的變量類型調用不同objc_msgSend進行賦值即可. 具體的:
2.2.1 Model屬性是普通系統對象的, 如NSString和普通的NSArray之類的直接賦值.
2.2.2 Model屬性是基本數據類型, 需要先將KVC拿到的NSNumber或者NSString轉化為int/bool/long後再賦值.
2.2.3 Model屬性是自定義類型, 需要將KVC拿到的另一個Protobuf類多走一次instanceWithProtoObject解析相應之後賦值
2.2.4 Model屬性是自定義類容器類型, 需要根據containerPropertyKeypathsForProtobuf中的規則獲取該容器屬性中的包含的自定義類的類名, 還需要該容器屬性的Protobuf取值路徑(這個多數情況下就是屬性名), 然後根據這些東西多次調用instanceWithProtoObject解析出一個數組後再進行賦值.
小總結:
HHClassInfo: 描述某個類的所有變量信息, 負責獲取該類的變量信息列表, 並根據相應規則進行忽略和映射.
HHPropertyInfo: 描述某個變量的具體信息, 包括變量名, 變量屬性, 變量取值路徑...等等
NSObject+ProtobufExtension: 解析的具體實現類, 根據待解析的類名獲取並緩存類變量信息, 再通過這些信息走KVC進行取值, objc_msgSend進賦值. 自定義類和自定義容器類的處理也在此.
消息轉發三部曲
接下來的內容都和消息轉發有關, 所以有必要先簡單介紹一下OC的消息轉發機制:
+ (BOOL)resolveInstanceMethod:(SEL)sel
當向對象發送消息而對象沒有對應的實現時, 消息會通過+(BOOL)resolveInstanceMethod:方法詢問具體的接收類: 沒有實現的話, 你能不能現在造一個實現出來?
通常現場造出消息實現都是走的class_addMethod添加對應的實現, 然後回答YES, 那麼此次消息發送算是成功的, 否則進入下一步.
- (id)forwardingTargetForSelector:(SEL)aSelector
上一步沒有結果的話消息會進行二次詢問: 造不出來沒關系, 你告訴我誰有這個消息的對應實現? 我去它那找也行的.
此時如果返回一個能響應該消息的對象, 那麼消息會轉發到返回對象那裡, 如果返回nil或者返回對象不能相應此消息, 進行最後一步.
- (void)forwardInvocation:(NSInvocation *)anInvocation
到了這一步, 消息發送其實算是失敗了, 不會再有詢問過程, 而是直接將消息攜帶的一切信息包裹在NSInvocation中交給對象自己處理. 另外, forwardInvocation:在構造Invocation時會調用methodSignatureForSelector:獲取方法簽名, 所以一般情況下還需要實現這個方法返回相應的方法簽名.
此時如果對象拿到invocation中的信息有能力發起[Invacation invoke], 那麼消息對應的實現還是能正常進行, 只是相對於正常的發送過程稍微麻煩耗時些, 否則就會觸發消息不識別的異常返回.
了解了消息轉發的相應流程後, 接下來看看通過消息轉發能具體能實現什麼功能.
安全的JSon
#define NSNullObjects @[@"",@0,@{},@[]] @implementation NSNull (SafeJson) - (id)forwardingTargetForSelector:(SEL)aSelector { for (id null in NSNullObjects) { if ([null respondsToSelector:aSelector]) { return null; } } return nil; }
Java後台對於空字段的默認處理就是返回一個null, 所以如果後台對返回的JSon不做任何處理的話, OC解析出來的也就是NSNull, NSNull表示空對象, 只是用來占位的, 什麼也做不了, 當對NSNull發送消息時, 就會crash.
因為JSon中只有數字, 字符串, 數組和字典四種類型, 所以只需要在觸發消息轉發時返回這四種類型中的某一種就可以解決了.
安全的數組
數組越界應該是日常開發中出現的蠻多的異常了, 針對這個異常, 正常情況下都是不辭辛勞每次取值前先判斷下標, 也有人通過Method Swizzle交換__NSArrayI和NSArrayM的objectAtIndex:方法(我不推薦這樣做, 原因會在文末給出), 這裡我給出另一種方法供大家參考, 先上具體效果:
NSMutableArray *array = [HHArray array]; [array addObject:@1]; [array addObject:@2]; [array addObject:@4]; [array addObjectsFromArray:@[@6, @8]]; [array addObject:nil];//safe [array removeObjectAtIndex:7];//safe [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSLog(@"e %@", obj); }];//log: 1 2 4 6 8 for (id x in array) { NSLog(@"- %@", x); }//log: 1 2 4 6 8 for (int i = 0; i < 10; i++) {//safe NSLog(@"~ %@", [array objectAtIndex:i]); }//log: 1 2 4 6 8 null null null... for (int i = 0; i < 10; i++) {//safe NSLog(@"_ %@", array[i]); }//log: 1 2 4 6 8 null null null...
HHArray是NSArray/NSMutableArray的裝飾類, 對外只提供兩個構造方法, 構造方法返回HHArray實例, 但是我們聲明返回值為NSMutableArray, 這樣就能騙過編譯器, 在不聲明NSMutableArray的各種接口的情況下外部調用HHArray的各個同名接口:
@interface HHArray : NSObject + (NSMutableArray *)array; + (NSMutableArray *)arrayWithArray:(NSArray *)array; @end
@interface HHArray () @property (strong, nonatomic) NSMutableArray *store; @end @implementation HHArray + (NSMutableArray *)array { return [HHArray arrayWithArray:nil]; } + (NSMutableArray *)arrayWithArray:(NSArray *)arr { HHArray *array = (id)[super allocWithZone:NULL]; return (id)[array initWithArray:arr] ; } - (instancetype)init { return [self initWithArray:nil]; } - (instancetype)initWithArray:(NSArray *)array { self.store = [NSMutableArray array]; [self.store addObjectsFromArray:array]; return self; } #pragma mark - Override - (ObjectType)objectAtIndex:(NSUInteger)index { IfValidIndexReturn(objectAtIndex:index); } - (ObjectType)objectAtIndexedSubscript:(NSUInteger)index { IfValidIndexReturn(objectAtIndexedSubscript:index); } - (void)addObject:(ObjectType)anObject { anObject == nil ?: [self.store addObject:anObject]; } - (void)insertObject:(ObjectType)anObject atIndex:(NSUInteger)index { IfValidObjectAndIndexPerform(insertObject:anObject atIndex:index); } - (void)removeObjectAtIndex:(NSUInteger)index { IfValidIndexPerform(removeObjectAtIndex:index); } - (void)replaceObjectAtIndex:(NSUInteger)index withObject:(ObjectType)anObject { IfValidObjectAndIndexPerform(replaceObjectAtIndex:index withObject:anObject); } #pragma mark - Forward - (id)forwardingTargetForSelector:(SEL)aSelector { return self.store; }
內部的實現很簡單, 聲明一個NSMutableArray做實際的數據存儲, 針對可能出錯的幾個接口進行參數判斷, 然後再調用相應的接口(這裡我只重寫了幾個典型接口, 有需要再加). 針對不會出錯的接口, 例如forin, removeAllObjects之類的, 我們通過forwardingTargetForSelector:直接轉發給內部的Array即可.
多代理
因為業務原因, 我的項目中有三個單例, 一般來說, 使用單例我都是拒絕的, 但是這仨還真只能是單例, 一個全局音樂播放器, 一個藍牙管理者, 一個智能硬件遙控器.
大家都知道, 單例是不能走單代理的, 因為單例會被多處訪問, 任意一處如果設置代理為自身, 之前的代理就會被覆蓋掉, 不好好維護的話, 一不小心就會出錯, 維護什麼的最麻煩了(這裡也有例外, 例如UIApplication, 它是單例且單代理, 不過那是因為它的代理不可能被覆蓋掉). 所以單例一般都是走通知或者多代理通知外部進行回調, 而我又不喜歡麻煩的通知, 就弄了個多代理. 具體實現如下:
#define HHNotifObservers(action) if (self.observers.hasObserver) { [self.observers action]; } @interface HHNotifier : NSProxy + (instancetype)notifier; + (instancetype)ratainNotifier; - (BOOL)hasObserver; - (void)addObserver:(id)observer; - (void)removeObserver:(id)observer; @end
@interface HHNotifier () @property (strong, nonatomic) NSHashTable *observers; @end @implementation HHNotifier + (instancetype)notifier:(BOOL)shouldRetainObserver { HHNotifier *notifier = [super alloc]; notifier.observers = [NSHashTable hashTableWithOptions:shouldRetainObserver ? NSPointerFunctionsStrongMemory : NSPointerFunctionsWeakMemory]; return notifier; } + (id)alloc { return [HHNotifier notifier:NO]; } + (instancetype)notifier { return [HHNotifier notifier:NO]; } + (instancetype)ratainNotifier { return [HHNotifier notifier:YES]; } #pragma mark - Interface - (BOOL)hasObserver { return self.observers.allObjects.count > 0; } - (void)addObserver:(id)observer { if (observer) { dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); [self.observers addObject:observer]; dispatch_semaphore_signal(self.lock); } } - (void)removeObserver:(id)observer { if (observer) { dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); [self.observers removeObject:observer]; dispatch_semaphore_signal(self.lock); } } #pragma mark - Override - (BOOL)respondsToSelector:(SEL)aSelector { for (id observer in self.observers.allObjects) { if ([observer respondsToSelector:aSelector]) { return YES; } } return NO; } #pragma mark - Forward - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { for (id observer in self.observers.allObjects) { NSMethodSignature *signature = [observer methodSignatureForSelector:sel]; if (signature) { return signature; } } return [super methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { for (id observer in self.observers.allObjects) { ![observer respondsToSelector:invocation.selector] ?: [invocation invokeWithTarget:observer]; } } #pragma mark - Getter - (dispatch_semaphore_t)lock { static dispatch_semaphore_t lock; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ lock = dispatch_semaphore_create(1); }); return lock; } @end
HHNotifier對外提供添加和移除代理的接口, 內部通過NSHashTable存儲代理的弱引用確保不會持有代理對象, 在向HHNotifier發送消息時, 它就會走消息轉發將此消息轉發給所有響應此消息的代理對象.
具體用法如下:
@interface ViewControllerNotifier : HHNotifier(ViewController)(此處用圓括號代替尖括號) @end @implementation ViewControllerNotifier @end //哪個類需要用到多代理, 就在這個類聲明一個HHNotifier的子類, 然後讓這個HHNotifier子類遵守相應的協議. //這樣做只是為了有代碼提示, 你也可以直接聲明一個id, 那就用不著聲明一個子類了
self.observers = [ViewControllerNotifier notifier]; for (int i = 0; i < 5; i++) { SomeObject *object = [SomeObject objectWithName:[NSString stringWithFormat:@"objcet%d", i]]; [self.observers addObserver:object];//實際的代理對象 } [self.observers addObserver:self];//無所謂的代理對象, 反正不響應 HHNotifObservers(doAnything);//輸出5次doAnything HHNotifObservers(doSomething);//輸出5次doSomething
需要說明的一點是, HHNotifier只是一個轉發器, 本身並沒有任何方法實現, 當內部沒有任何可轉發的對象或者所有對象都不響應這個消息時還是會觸發異常的, 所以在向Notifier發送消息前, 嚴謹的做法是先通過HHNotifier的respondsToSelector:做個判斷, 或者不嚴謹的通過hasObserver判斷也行.
通用打點器
關於打點, 網上的文章有很多, 但是幾乎都是走Method Swizzle來實現, 雖然能實現效果, 但是不夠通用, 有多少需要打點的類, 就要建立多少個category. 另外, 因為打點通常都是後期強行加的需求, 到了實際實現的時候可能有不同的方法名需要走的都是同一個打點邏輯, 比如某個發送事件, 程序員A的方法名是send:, 程序員B卻是sendContent:, 然而這兩對於打點而言都是相同的邏輯. 所以, 搞一個通用的打點器, 還是有必要滴.
照例, 先上實現效果:
+ (NSDictionary(NSString *,id)圓括號代替尖括號*)observeItems { return @{@"UIControl" : @"sendAction:to:forEvent:", @"Person" : @"personFunc:", @"SecondViewController" : @[@"aFunc", @"aFunc:", @"aFunc1:", @"aFunc2:", @"aFunc3:", @"aFunc4:", @"aFunc:objcet:", @"aFunc:frame:size:point:object:", @"dasidsadbisaidsabidsbaibdsai"]}; }//在這裡聲明需要打點的類和對應的方法, 多個方法放在一個數組中即可, 對於不響應的方法不會被打點 + (void)object:(id)object willInvokeFunction:(NSString *)function withArguments:(NSArray *)arguments { //打點方法執行前會調用 參數分別是方法執行對象 方法名和方法參數 } + (void)object:(id)object didInvokeFunction:(NSString *)function withArguments:(NSArray *)arguments { //打點方法執行後會調用 參數分別是方法執行對象 方法名和方法參數 }
實現思路: 上面有介紹過, forwardInvocation:會在消息轉發時被調用, 並帶回該消息的一切信息:方法名, 方法參數, 執行對象等等, 所以我們需要做的就是讓被打點的方法全都先走一次消息轉發, 我們在消息轉發拿到需要的信息以後, 再調用方法的原實現, 借此實現通用打點.具體的:
1.根據observeItems中的信息拿到被打點類和對應方法method.
2.替換method到forwardInvocation:, 同時添加一個newMethod指向method的原實現.
3.在forwardInvocation:中解析invocation獲取需要的信息進行打點.
4.調用newMethod執行原來的方法實現
其實說到這裡, 看過JSPatch源碼的同學應該已經想到了, 這個套路就是JSPatch.overrideMethod()的原理.
對於沒看過JSPatch源碼的同學, 我在此解說一波, 先看看代碼實現:
+ (void)load { _nilObject = [NSObject new]; [[self observeItems] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull className, id _Nonnull selectors, BOOL * _Nonnull stop) { //遍歷打點容器獲取類名和打點方法進行打點 Class cls = NSClassFromString(className); if ([selectors isKindOfClass:[NSString class]]) { [self replaceClass:cls function:selectors]; } else if ([selectors isKindOfClass:[NSArray class]]) { for (NSString *selectorName in selectors) { [self replaceClass:cls function:selectorName]; } } }]; } + (void)replaceClass:(Class)cls function:(NSString *)selectorName { SEL selector = NSSelectorFromString(selectorName);//被打點的方法名 SEL forwardSelector = HHOriginSeletor(selectorName);//指向方法原實現的新方法名 Method method = class_getInstanceMethod(cls, selector);//獲取方法實現 下文使用 if (method != nil) {//如果沒有實現, 那就不用打點了 IMP msgForwardIMP = _objc_msgForward;//消息轉發IMP #if !defined(__arm64__) if (typeDescription[0] == '{') { NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription]; if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) { msgForwardIMP = (IMP)_objc_msgForward_stret; }//某些返回值為結構體的API返回的結構體太大, 在非64位架構上寄存器可能存不下, 所以需要特殊處理 } #endif IMP originIMP = class_replaceMethod(cls, selector , msgForwardIMP, method_getTypeEncoding(method));//替換原方法實現到forwardInvocation: class_addMethod(cls, forwardSelector, originIMP, method_getTypeEncoding(method));//添加一個新的方法指向原來的方法實現 class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)HHForwardInvocation, "v@:@");//替換系統的forwardInvocation:實現指向自己的HHForwardInvocation實現, 在這裡進行方法解析, 拿到信息後打點 } }
static void HHForwardInvocation(__unsafe_unretained id target, SEL selector, NSInvocation *invocation) { NSMutableArray *arguments = [NSMutableArray array]; NSMethodSignature *methodSignature = [invocation methodSignature]; for (NSUInteger i = 2; i < methodSignature.numberOfArguments; i++) { const char *argumentType = [methodSignature getArgumentTypeAtIndex:i]; switch(argumentType[0] == 'r' ? argumentType[1] : argumentType[0]) { //...各種參數類型解析 略 HH_FWD_ARG_CASE('c', char) HH_FWD_ARG_CASE('C', unsigned char) HH_FWD_ARG_CASE('s', short) //...各種參數類型解析 略 default: { NSLog(@"error type %s", argumentType); } break; } } NSString *selectorName = NSStringFromSelector(invocation.selector); [HHObserver object:target willInvokeFunction:selectorName withArguments:arguments];//拿到方法信息後向外傳 [invocation setSelector:HHOriginSeletor(selectorName)]; [invocation invoke];//執行方法的原實現 [HHObserver object:target didInvokeFunction:selectorName withArguments:arguments];//拿到方法信息後向外傳 }
簡單解釋一下整個打點的實現代碼:
1.在+load方法中獲取需要打點的類和方法調用replaceClass: function:, load方法會保證打點中進行的方法替換只走一次, replaceClass: function:進行實際的方法替換.
2.replaceClass: function:先走class_replaceMethod替換打點方法到forwardInvocation:, 再走class_addMethod添加一個新的方法指向原來的方法實現, 最後將該類的forwardInvocation:指向通用的HHForwardInvocation方法實現.
3.在通用的HHForwardInvocation中解析invocation(這裡直接是用的Bang哥的代碼, Bang在這裡做了很多事, 參數解析, 內存問題什麼的, 在代碼中都有解決, 不做贅述), 根據解析出的信息執行打點邏輯, 最後設置Invacation.selector為2中添加的新方法, 走[invocation invoke]執行方法原實現.
整個過程中的方法調用過程如下
class.method->class.forwardInvocation->HHObserver.HHForwardInvocationIMP->class.newMethod->class.methodIMP
上面的邏輯走完以後, 一個通用的打點器就完成了. 但是有一個問題,我們的打點方法是借鑒的JSPatch, 那在使用JSPatch重寫打點方法時,會沖突嗎?
答案是, 完全重寫不會沖突, 但是在重寫方法中調用ORIGFunc執行原實現時就會沖突.
先解釋第一種情況, 我們的打點邏輯是在HHObserver類加載的時候執行的, 而JSPatch的熱修復是在從網絡下載到JS腳本後再執行的, 這個時間點比我們要晚很多 ,所以完全重寫的情況下我們的邏輯會被JSPatch完全覆蓋, 不會沖突.
接著解釋第二種情況, 這部分要貼一下JSPatch的代碼:
//JPEngine.m - overrideMethod() 1.這裡會替換類的forwardInvocation:為JPForwardInvocation, 原因和我們一樣, 在JPForwardInvocation解析Invacation獲取方法信息, 不過JSPatch拿這些東西是為了重寫 if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) { IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@"); if (originalForwardImp) { class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@"); }//如果復寫類有實現forwardInvocation:, 那麼會添加一個方法指向原始的forwardInvocation:, 因為我們的打點邏輯會先替換打點方法到forwardInvocation:, 所以這裡會認為有實現這個forwardInvocation: } [cls jp_fixMethodSignature]; //2.重點在這一步, 這裡會添加一個ORIGsomeFunction指向被重寫方法的原實現, 注意, 此時的方法原實現已經被我們替換成了_objc_msgForward if (class_respondsToSelector(cls, selector)) { NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName]; SEL originalSelector = NSSelectorFromString(originalSelectorName); if(!class_respondsToSelector(cls, originalSelector)) { class_addMethod(cls, originalSelector, originalImp, typeDescription); } } //3.將被重寫的方法拼上_JP前綴, 放入_JSOverideMethods全局字典中, 這個全局字典用cls做key存儲的value也是一個字典, 這個內部字典以_JPSelector為key存放著具體的重寫邏輯JSFunction NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName]; _initJPOverideMethods(cls); _JSOverideMethods[cls][JPSelectorName] = function; class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);//替換class.selector到forwardInvocation:, oc調用selector就會走forwardInvocation:, 然後上面已經把forwardInvocation:指向到了JPForwardInvocation
//JPEngine.m - JPForwardInvocation() static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation) { //...對我們來說不重要 略 NSString *selectorName = NSStringFromSelector(invocation.selector); NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName]; JSValue *jsFunc = getJSFunctionInObjectHierachy(slf, JPSelectorName); if (!jsFunc) {//將調用方法名拼上_JP後判斷是否有對應的JSFunction實現, 沒有的話那就是OC端的未實現方法, 走原始的消息轉發 JPExecuteORIGForwardInvocation(slf, selector, invocation); return; } //...各種參數解析 略 }
大家看著注釋應該能看懂, JSPatch添加了一個ORIGfunc指向被重寫方法的原實現, 而這個原實現在打點的時候被我們替換到了_objc_msgForward, 所以JS端在調用class.ORIGfunc時其實又會走到forwardInvocation:, 然後又走到JPForwardInvocation, 但是這裡傳過來的方法名是ORIGfunc, 這裡會根據overrideMethod中的拼裝規則先拼上_JP, 最後拿著這個_JPORIGfunc在全局字典中找JS實現, 顯然這個多次拼裝的方法名是沒有對應實現的, 此時會拿著這個ORIGfunc走JPExecuteORIGForwardInvocation調用原始的消息轉發, 然而原始的消息轉發在打點時早就被我們替換到了HHForwardInvocation, 所以會走到HHForwardInvocation, 在這裡我們根據傳過來ORIGfunc再拼裝上自己的方法前綴名HH_ORIG, 變成了HH_ORIGORIGfunc, 顯然也是沒有實現的, 那麼就會crash.
整個流程的方法調用走向如下:
JS調用ORIGfunc走OC原實現->原實現就是 _objc_msgForward(打點時替換)-> 走到forwardInvocation:->走到JPForwardInvocation(JSPatch替換)-> JPForwardInvocation判斷方法沒有實現走原始的消息轉發->原始的消息轉發走到HHForwardInvocation(打點時替換)-> HHForwardInvocation判斷方法沒有實現->crash
找到沖突原因後就很好解決了, 因為JS調用ORIGfunc最終還是會走到我們自己的HHForwardInvocation中, 只是此時傳過來的方法名多了一個ORIG前綴, 所以我們需要做的就是將這個前綴去掉再拼上我們自己的前綴就能調用方法原實現了, 就這樣:
NSString *selectorName = NSStringFromSelector(invocation.selector); if ([selectorName hasPrefix:@"ORIG"]) { selectorName = [selectorName substringFromIndex:4]; } [HHObserver object:target willInvokeFunction:selectorName withArguments:arguments]; [invocation setSelector:HHOriginSeletor(selectorName)]; [invocation invoke]; [HHObserver object:target didInvokeFunction:selectorName withArguments:arguments];
ISA Swizzle 和 Method Swizzle
ISA Swizzle可能是Runtime中實際使用最少的方法了, 原因很簡單, 通過 object_setClass(id, Class)設置某個對象的isa指針時, 這個對象在內存中已經加載完成了, 這意味著你設置的新class能使用的內存只有原來class對象的內存那麼大, 所以新的class聲明的iVar/Property不能多不能少, 類型也不能不一致, 不然內存布局對不上, 一不小心就是野指針.
iVar不能亂用, 那就只能打打Method的注意了, 但是對於Method我們又有Method Swizzle來做這事兒, 比ISA Swizzle方便還安全.
這兩點造成了ISA Swizzle的尴尬境地. 基本上它的出場對白都是: 知道KVO的實現原理嗎? 知道, ISA Swizzle!
話是這麼說, ISA Swizzle倒是可以實現一點, 在不改變類的任何邏輯的前提下, 增加類的功能性, 相比同樣能做此事的繼承和裝飾而言, 它顯得神不知鬼不覺, 可能這就是它的優點吧.
實際開發中我沒用過, 就不寫了.
反之, Method Swizzle可能是Runtime系列用的最多, 也是被寫的最多的文章了, 從原理到實現都有無數大同小異的博客, 所以這一節我也不寫, 我是來提問的...
這裡先簡單描述一下Method Swizzle的應用場景, 下文會引出我的問題:
@implementation UIViewController (LogWhenDealloc) + (void)load { Method originDealloc = class_getInstanceMethod(self, NSSelectorFromString(@"dealloc")); Method swizzleDealloc = class_getInstanceMethod(self, @selector(swizzleDealloc)); method_exchangeImplementations(originDealloc, swizzleDealloc); } - (void)swizzleDealloc { NSString *className = NSStringFromClass([self class]); if (![className hasPrefix:@"UI"] && ![className hasPrefix:@"_UI"]) { NSLog(@"------------------------------Dealloc : %@------------------------------",className); } [self swizzleDealloc]; }
@implementation UIControl (Statistic) + (void)load { Method originMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:)); Method swizzleMethod = class_getInstanceMethod(self, @selector(swizzleSendAction:to:forEvent:)); method_exchangeImplementations(originMethod, swizzleMethod); } - (void)swizzleSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { //打點邏輯 [self swizzleSendAction:action to:target forEvent:event]; }
普遍的Method Swizzle大概都是這樣的格式, 前者用來提示某個VC是否在返回後正確釋放, 後者則是用來統計Button點擊的打點工具.
正常情況下大部分系統類都可以通過Method Swizzle進行方法交換, 從而在方法執行前後執行一些自己的邏輯, 但是對於NSArray/NSNumber/NSUUID之類的類簇卻行不通. 這是因為這些類簇通常只有一個暴露通用接口的基類, 而這些接口的實現卻是其下對應的各個子類, 所以如果要對這些接口進行Method Swizzle就必須找准具體的實現類, 於是就有了下面的代碼:
@implementation NSArray (SafeArray) + (void)load { Method originMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:)); Method swizzleMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(swizzleObjectAtIndex:)); method_exchangeImplementations(originMethod, swizzleMethod); } - (id)swizzleObjectAtIndex:(NSUInteger)index { // NSLog(@"1"); return index < self.count ? [self swizzleObjectAtIndex:index] : nil; } @end
該Category交換了不可變數組__NSArrayI的objectAtIndex:方法, 並對入參的index進行判斷以防止出現數組越界的異常情況. 注意這裡我注釋了一行NSLog, 如果將此行注釋打開, 不可變數組調用objectAtIndex:後控制台應該會輸出無數的1, 然後主線程進入休眠, 點擊屏幕後又開始輸出1, 主線程再休眠, 如此反復, 表現跟特意使用runloop實現不卡UI的線程阻塞一樣.
好了, 這就是本小節乃至本文的目的所在了, 我特別好奇為什麼會出現這種情況, 為什麼只是一行簡單NSLog就導致了主線程的休眠? 有知道具體原因的朋友, 歡迎在評論區留言或者簡信我.
本文附帶的demo地址