Runtime在實際開發中的應用

黑花白花發表於2017-12-18

從簡書遷移到掘金...

前言

本文並不是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"}};
}
複製程式碼
//SomeAPIManager
[SomeModl instanceWithProtoObject:aProtoObject];
複製程式碼

實現思路很簡單: 首先通過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<HHPropertyInfo *> *)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<Class, HHClassInfo *> *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地址

相關文章