Runtime在實際開發中的應用

從簡書遷移到掘金...git

前言

本文並非Runtime原理從入門到精通之類的教程, 並不會涉及到過多的原理概念描述, 而是介紹在實際開發中如何使用Runtime解決相應的問題, 具體的應用在以前的兩篇網絡層博客和之後都博客中都會有所體現. 全文約八千字, 預計花費閱讀時間20 - 30分鐘.程序員

目錄

  • Protobuf解析器
  • 消息轉發三部曲
  • 安全的JSon
  • 安全的數組
  • 多代理
  • 通用打點器
  • ISA Swizzle 和 Method Swizzle

一.Protobuf解析器

在以前的博客中提到過, 我司網絡層用的是TCP+Protobuf的組合, 請求數據是Protobuf, 返回數據也是Protobuf, 這意味着市面上通用的JSon解析工具在我這並不通用, 因而就本身實現一套相似的解析的工具. 最後的實現效果是:github

  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了.安全

具體到代碼中, 總共是以下幾步:bash

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, 二者不一致, 因此須要特殊處理. 獲取屬性相關信息的具體實現以下:ide

@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地址

相關文章
相關標籤/搜索