Runtime在实际开发中的应用

前言

本文并不是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地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容