今天看啥  ›  专栏  ›  茶底世界之下

iOS Runtime详细介绍及实战使用

茶底世界之下  · 掘金  ·  · 2021-01-24 23:32
阅读 22

iOS Runtime详细介绍及实战使用

前言

  • 熟练的运用Runtime相关的技术,能够更好的解决复杂问题和实现复杂需求

类在runtime中的表示

//类在runtime中的表示
struct objc_class {
    // 实例的isa指向类对象,类对象的isa指向元类
    Class isa;//指针,顾名思义,表示是一个什么
#if !__OBJC2__
    Class super_class;  //指向父类
    const char *name;  //类名
    long version;
    long info;
    long instance_size
    struct objc_ivar_list *ivars //成员变量列表
    struct objc_method_list **methodLists; //方法列表
    struct objc_cache *cache;//缓存
    // 调用过的方法存入缓存列表,下次调用先找缓存(优化)
    struct objc_protocol_list *protocols //协议列表
    #endif
} OBJC2_UNAVAILABLE;
复制代码

API介绍

objc系列函数

例如类与协议的空间分配、注册、注销等操作

函数函数作用
objc_getClass获取Class对象
objc_getMetaClass获取MetaClass对象
objc_allocateClassPair分配空间,创建类
objc_registerClassPair注册一个类
objc_disposeClassPair注销某个类
objc_allocateProtocol开辟空间创建协议
objc_registerProtocol注册一个协议
objc_setAssociatedObject为实例对象关联对象
objc_getAssociatedObject获取实例对象的关联对象
objc_removeAssociatedObjects清空实例对象的所有关联对象
objc_getProtocol获取某个协议
objc_copyProtocolList拷贝在运行时中注册过的协议列表

class系列函数

例如实例变量、方法、属性、协议等相关问题

函数函数作用
class_addIvar为类添加实例变量
class_addProperty为类添加属性
class_addMethod为类添加方法
class_addProtocol为类遵循协议
class_replaceMethod替换类某方法的实现
class_getName获取类名
class_isMetaClass判断是否为元类
class_getSuperclass获取某类的父类
class_setSuperclass设置某类的父类
class_getProperty获取某类的属性
class_getInstanceVariable获取实例变量
class_getClassVariable获取类变量
class_getInstanceMethod获取实例方法
class_getClassMethod获取类方法
class_getMethodImplementation获取方法的实现
class_getInstanceSize获取类的实例的大小
class_respondsToSelector判断类是否实现某方法
class_conformsToProtocol判断类是否遵循某协议
class_createInstance创建类的实例
class_copyIvarList拷贝类的实例变量列表
class_copyMethodList拷贝类的方法列表
class_copyProtocolList拷贝类遵循的协议列表
class_copyPropertyList拷贝类的属性列表

object系列函数

例如实例变量

函数函数作用
object_getClassName获取对象的类名
object_getClass获取对象的Class
object_setClass设置对象的Class
object_getIvar获取对象中实例变量的值
object_setIvar设置对象中实例变量的值
object_getInstanceVariable获取对象中实例变量的值 (ARC中无效,使用object_getIvar)
object_setInstanceVariable设置对象中实例变量的值 (ARC中无效,使用object_setIvar)

method系列函数

例如方法的参数及返回值类型和方法的实现

函数函数作用
method_getName获取方法名
method_getImplementation获取方法的实现
method_getTypeEncoding获取方法的类型编码
method_getNumberOfArguments获取方法的参数个数
method_copyReturnType拷贝方法的返回类型
method_getReturnType获取方法的返回类型
method_copyArgumentType拷贝方法的参数类型
method_getArgumentType获取方法的参数类型
method_getDescription获取方法的描述
method_setImplementation设置方法的实现
method_exchangeImplementations替换方法的实现

property系列函数

如属性名、属性的特性等

函数函数作用
property_getName获取属性名
property_getAttributes获取属性的特性列表
property_copyAttributeList拷贝属性的特性列表
property_copyAttributeValue拷贝属性中某特性的值

protocol系列函数

如获取协议名称、是否遵循协议等

函数函数作用
protocol_conformsToProtocol判断一个协议是否遵循另一个协议
protocol_isEqual判断两个协议是否一致
protocol_getName获取协议名称
protocol_copyPropertyList拷贝协议的属性列表
protocol_copyProtocolList拷贝某协议所遵循的协议列表
protocol_copyMethodDescriptionList拷贝协议的方法列表
protocol_addProtocol为一个协议遵循另一协议
protocol_addProperty为协议添加属性
protocol_getProperty获取协议中的某个属性
protocol_addMethodDescription为协议添加方法描述
protocol_getMethodDescription获取协议中某方法的描述

ivar系列函数

函数函数作用
ivar_getName获取Ivar名称
ivar_getTypeEncoding获取类型编码
ivar_getOffset获取偏移量

sel系列函数

函数函数作用
sel_getName获取名称
sel_getUid注册方法
sel_registerName注册方法
sel_isEqual判断方法是否相等

imp系列函数

函数函数作用
imp_implementationWithBlock通过代码块创建IMP
imp_getBlock获取函数指针中的代码块
imp_removeBlock移除IMP中的代码块

Runtime实战使用

获取列表

/// 描述类中的一个方法
typedef struct objc_method *Method;
/// 实例变量
typedef struct objc_ivar *Ivar;
/// 类别Category
typedef struct objc_category *Category;
/// 类中声明的属性
typedef struct objc_property *objc_property_t;
复制代码

有时候会有这样的需求,我们需要知道当前类中每个属性的名字(比如字典转模型,字典的Key和模型对象的属性名字不匹配)
我们可以通过runtime的一系列方法获取类的一些信息

  • 属性列表
  • 方法列表
  • 成员变量列表
  • 遵循的协议列表
/// 属性列表
@dynamic propertyTemps;
- (NSArray<NSString*>*)propertyTemps{
    NSMutableArray *temps = [NSMutableArray array];
    unsigned int outCount, i;
    Class targetClass = [self class];
    while (targetClass != [NSObject class]) {
        objc_property_t *properties = class_copyPropertyList(targetClass, &outCount);
        for (i = 0; i < outCount; i++) {
            objc_property_t property = properties[i];
            const char *char_f = property_getName(property);
            NSString *propertyName = [NSString stringWithUTF8String:char_f];
            if (propertyName) [temps addObject:propertyName];
        }
        free(properties);
        targetClass = [targetClass superclass];
    }
    return temps.mutableCopy;
}
/// 成员变量列表 
@dynamic ivarTemps;
- (NSArray<NSString*>*)ivarTemps{
    unsigned int count;
    Ivar *ivar = class_copyIvarList([self class], &count);
    NSMutableArray *temp = [NSMutableArray arrayWithCapacity:count];
    for (int i = 0; i < count; i++) {
        const char *char_f = ivar_getName(ivar[i]);
        NSString *name = [NSString stringWithCString:char_f encoding:NSUTF8StringEncoding];
        if (name) [temp addObject:name];
    }
    return temp.mutableCopy;
}
/// 方法列表
@dynamic methodTemps;
- (NSArray<NSString*>*)methodTemps{
    unsigned int count;
    Method *method = class_copyMethodList([self class], &count);
    NSMutableArray *temp = [NSMutableArray arrayWithCapacity:count];
    for (int i = 0; i < count; i++) {
        NSString *name = NSStringFromSelector(method_getName(method[i]));
        if (name) [temp addObject:name];
    }
    return temp.mutableCopy;
}
/// 遵循的协议列表
@dynamic protocolTemps;
- (NSArray<NSString*>*)protocolTemps{
    unsigned int count;
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    NSMutableArray *temp = [NSMutableArray arrayWithCapacity:count];
    for (unsigned int i = 0; i<count; i++) {
        const char *protocolName = protocol_getName(protocolList[i]);
        NSString *name = [NSString stringWithCString:protocolName encoding:NSUTF8StringEncoding];
        if (name) [temp addObject:name];
    }
    return temp.mutableCopy;
}
复制代码

实战示例:实现NSCoding的自动归档和解档

@implementation KJTestModel

- (void)encodeWithCoder:(NSCoder *)encoder{
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([KJTestModel class], &count);
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char *name = ivar_getName(ivar);
        NSString *key = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:key];
        [encoder encodeObject:value forKey:key];
    }
    free(ivars);
}
- (id)initWithCoder:(NSCoder *)decoder{
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([KJTestModel class], &count);
        for (int i = 0; i<count; i++) {
            Ivar ivar = ivars[i];
            const char *name = ivar_getName(ivar);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [decoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivars);
    }
    return self;
}
@end
复制代码

方法调用

  • 如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)操作。
  • 如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中操作。
  • 对象调用方法经过三个阶段

消息发送:查询cache和方法列表,找到了直接调用,找不到方法会进入下个阶段
动态解析:调用实例方法resolveInstanceMethod或类方法resolveClassMethod里面可以有一次动态添加方法的机会
消息转发:首先会判断是否有其他对象可以处理方法forwardingTargetForSelector返回一个新的对象,如果没有新的对象进行处理,会调用methodSignatureForSelector方法返回方法签名,然后调用forwardInvocation

这里可以做一个简单的防止调用未实现方法崩溃处理:选择在消息转发的最后一步来做处理,methodSignatureForSelector:消息获得函数的参数和返回值,然后[self respondsToSelector:aSelector]判断是否有该方法,如果没有返回函数签名,创建一个NSInvocation对象并发送给forwardInvocation

@implementation NSObject (KJUnrecognizedSelectorException)
+ (void)kj_openUnrecognizedSelectorExchangeMethod{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        kExceptionMethodSwizzling(self, @selector(methodSignatureForSelector:), @selector(kj_methodSignatureForSelector:));
        kExceptionMethodSwizzling(self, @selector(forwardInvocation:), @selector(kj_forwardInvocation:));
        kExceptionClassMethodSwizzling(self, @selector(methodSignatureForSelector:), @selector(kj_methodSignatureForSelector:));
        kExceptionClassMethodSwizzling(self, @selector(forwardInvocation:), @selector(kj_forwardInvocation:));
    });
}
- (NSMethodSignature*)kj_methodSignatureForSelector:(SEL)aSelector{
    if ([self respondsToSelector:aSelector]) {
        return [self kj_methodSignatureForSelector:aSelector];
    }
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)kj_forwardInvocation:(NSInvocation*)anInvocation{
    NSString *string = [NSString stringWithFormat:@"🍉🍉 crash:%@ 类出现未找到实例方法",NSStringFromClass([self class])];
    NSString *reason = [NSStringFromSelector(anInvocation.selector) stringByAppendingString:@" 🚗🚗实例方法未找到🚗🚗"];
    NSException *exception = [NSException exceptionWithName:@"没找到方法" reason:reason userInfo:@{}];
    [KJCrashManager kj_crashDealWithException:exception CrashTitle:string];
}

+ (NSMethodSignature*)kj_methodSignatureForSelector:(SEL)aSelector{
    if ([self respondsToSelector:aSelector]) {
        return [self kj_methodSignatureForSelector:aSelector];
    }
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
+ (void)kj_forwardInvocation:(NSInvocation*)anInvocation{
    NSString *string = [NSString stringWithFormat:@"🍉🍉 crash:%@ 类出现未找到类方法",NSStringFromClass([self class])];
    NSString *reason = [NSStringFromSelector(anInvocation.selector) stringByAppendingString:@" 🚗🚗类方法未找到🚗🚗"];
    NSException *exception = [NSException exceptionWithName:@"没找到方法" reason:reason userInfo:@{}];
    [KJCrashManager kj_crashDealWithException:exception CrashTitle:string];
}

@end
复制代码

重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了。
如果想调用已经重写过的方法的父类的实现,只需使用super这个编译器标识,它会在运行时跳过在当前的类对象中寻找方法的过程

高频调用方法

Runtime源码中的IMP作为函数指针,指向方法的实现。通过它我们可以绕开发送消息的过程来提高函数调用的效率

void (*test)(id, SEL, BOOL);
test = (void(*)(id, SEL, BOOL))[target methodForSelector:@selector(xxx:)];
for (int i = 0; i < 100000; i++) {
    test(targetList[i], @selector(xxx:), YES);
}
复制代码

拦截调用

在方法调用中说到了,如果没有找到方法就会转向拦截调用,那么什么是拦截调用呢?
拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理。

  • resolveClassMethod:当你调用一个不存在的类方法的时候,会调用这个方法,默认返回NO,你可以加上自己的处理然后返回YES。
  • resolveInstanceMethod:和第一个方法相似,只不过处理的是实例方法。

后两个方法需要转发到其他的类处理

  • forwardingTargetForSelector:将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
  • forwardInvocation:将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法。

动态添加方法

重写了拦截调用的方法并且返回了YES,我们要怎么处理呢?
根据传进来的SEL类型的selector动态添加一个方法

// 隐式调用一个不存在的方法
[target performSelector:@selector(xxx:) withObject:@"test"];
复制代码

然后在target对象内部重写拦截调用的方法,动态添加方法。

void testAddMethod(id self, SEL _cmd, NSString *string){
    NSLog(@"xxxx");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if ([NSStringFromSelector(sel) isEqualToString:@"xxx:"]) {
        class_addMethod(self, sel, (IMP)testAddMethod, "v@:*");
    }
    return YES;
}
复制代码

其中class_addMethod的四个参数分别是:

  • Class cls 给哪个类添加方法,本例中是self
  • SEL name 添加的方法,本例中是重写的拦截调用传进来的selector。
  • IMP imp是C的方法实现可以直接获得。OC获得方法的实现+ (IMP)instanceMethodForSelector:(SEL)aSelector
  • "v@:*"方法的签名,代表有一个参数的方法

动态继承

动态继承修改NSBundle对象的isa指针使其指向子类KJLanguageManager,便可以调用子类的方法

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        object_setClass([NSBundle mainBundle], [KJLanguageManager class]);
    });
}
复制代码

关联对象

原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
现有这样一个需求:系统的类并不能满足你的需求,你需要额外添加一个属性,这种情况的一般解决办法就是继承。但是只增加一个属性,就去继承一个类,总是觉得太麻烦。 这时就可以使用runtime的关联对象来处理

// 全局变量 - 关联对象key
static char associatedObjectKey;
// 设置关联对象
objc_setAssociatedObject(target, &associatedObjectKey, @"关联测试", OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
// 获取关联对象
NSString *string = objc_getAssociatedObject(target, &associatedObjectKey);
NSLog(@"----:%@", string);
复制代码

objc_setAssociatedObject的四个参数:

  • id object给谁设置关联对象。
  • const void *key关联对象唯一的key,获取时会用到。
  • id value关联对象。
  • objc_AssociationPolicy关联策略,有以下几种策略:
enum {
    OBJC_ASSOCIATION_ASSIGN = 0,
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, 
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
    OBJC_ASSOCIATION_RETAIN = 01401,
    OBJC_ASSOCIATION_COPY = 01403 
};
复制代码

其实,你还可以把添加和获取关联对象的方法写在类别中,方便使用。

// 获取关联对象
- (CGFloat)timeInterval{
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
// 添加关联对象
- (void)setTimeInterval:(CGFloat)timeInterval{
    objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_ASSIGN);
}
复制代码

注意:这里面我们把timeInterval方法的地址作为唯一的key,_cmd代表当前调用方法的地址。

方法交换

方法交换,顾名思义,就是将两个方法的实现交换
原理是:通过Runtime获取到方法实现的地址,进而动态交换两个方法的功能

交换实例方法

void kExceptionMethodSwizzling(Class clazz, SEL original, SEL swizzled){
    Method originalMethod = class_getInstanceMethod(clazz, original);
    Method swizzledMethod = class_getInstanceMethod(clazz, swizzled);
    if (class_addMethod(clazz, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(clazz, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
复制代码

交换类方法

void kExceptionClassMethodSwizzling(Class clazz, SEL original, SEL swizzled){
    Method originalMethod = class_getClassMethod(clazz, original);
    Method swizzledMethod = class_getClassMethod(clazz, swizzled);
    Class metaclass = objc_getMetaClass(NSStringFromClass(clazz).UTF8String);
    if (class_addMethod(metaclass, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(metaclass, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
复制代码

方法交换对于我来说更像是实现一种思想的最佳技术:AOP面向切面编程
交换完再调回自己,要保证只交换一次,否则会乱套

例如,将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之调用B方法时候执行A方法
下面是一个数组越界的runtime实现:

// 调用原方法以及新方法进行交换,处理崩溃问题。
+ (void)load {
    // 利用GCD只执行一次,防止多线程问题
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 获得不可变数组objectAtIndex的selector
        SEL A_sel = @selector(objectAtIndex:);
        // 自己实现的将要被交换的方法的selector
        SEL B_sel = @selector(kj_objectAtIndex:);
        // 两个方法的Method
        Method A_Method = class_getInstanceMethod(objc_getClass("__NSArrayI"), A_sel);
        // 自己实现的将要被交换的方法的selector
        Method B_Method = class_getInstanceMethod(objc_getClass("__NSArrayI"), B_sel);
        // 首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
        BOOL isAdd = class_addMethod(self, A_sel, method_getImplementation(B_Method), method_getTypeEncoding(B_Method));
        if (isAdd) {
            // 如果成功,说明类中不存在这个方法的实现
            // 将被交换方法的实现替换到这个并不存在的实现
            class_replaceMethod(self, B_sel, method_getImplementation(A_Method), method_getTypeEncoding(A_Method));
        }else{
            // 否则,交换两个方法的实现
            method_exchangeImplementations(A_Method, B_Method);
        }
    });   
}
- (instancetype)kj_objectAtIndex:(NSUInteger)index{
    NSArray *temp = nil;
    @try {
        temp = [self kj_objectAtIndex:index];
    }@catch (NSException *exception) {
        NSString *string = @"🍉🍉 crash:";
        if (self.count == 0) {
            string = [string stringByAppendingString:@"数组个数为零"];
        }else if (self.count <= index) {
            string = [string stringByAppendingString:@"数组索引越界"];
        }
        [KJCrashManager kj_crashDealWithException:exception CrashTitle:string];
    }@finally {
        return temp;
    }
}
复制代码

备注:这里内部调用了temp = [self kj_objectAtIndex:index];,看上去有点像递归死循环,其实不是,这里正因为交换了方法,其实是调用的原始方法objectAtIndex:

再举个例子:在CollectionView上面移动Item并且不影响正常CollectionView的左右滑动,这时就可以交换获取到Touch事件,然后以回调的方式传递出来,那么换做是你,你会采取怎么样的方式来处理呢?

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self kj_swizzleMethod:@selector(touchesBegan:withEvent:) Method:@selector(kj_touchesBegan:withEvent:)];
        [self kj_swizzleMethod:@selector(touchesMoved:withEvent:) Method:@selector(kj_touchesMoved:withEvent:)];
        [self kj_swizzleMethod:@selector(touchesEnded:withEvent:) Method:@selector(kj_touchesEnded:withEvent:)];
        [self kj_swizzleMethod:@selector(touchesCancelled:withEvent:) Method:@selector(kj_touchesCancelled:withEvent:)];
    });
}
- (void)kj_touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event{
    if (self.kOpenExchange && self.moveblock) {
        CGPoint point = [[touches anyObject] locationInView:self];
        self.moveblock(KJMoveStateTypeBegin,point);
    }
    [self kj_touchesBegan:touches withEvent:event];
}
- (void)kj_touchesMoved:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event{
    if (self.kOpenExchange && self.moveblock) {
        CGPoint point = [[touches anyObject] locationInView:self];
        self.moveblock(KJMoveStateTypeMove,point);
    }
    [self kj_touchesMoved:touches withEvent:event];
}
- (void)kj_touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event{
    if (self.kOpenExchange && self.moveblock) {
        CGPoint point = [[touches anyObject] locationInView:self];
        self.moveblock(KJMoveStateTypeEnd,point);
    }
    [self kj_touchesEnded:touches withEvent:event];
}
- (void)kj_touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event{
    if (self.kOpenExchange && self.moveblock) {
        CGPoint point = [[touches anyObject] locationInView:self];
        self.moveblock(KJMoveStateTypeCancelled,point);
    }
    [self kj_touchesEnded:touches withEvent:event];
}
复制代码

Runtime介绍就到此完毕,后面有相关再补充,写文章不容易,还请点个**小星星**传送门




原文地址:访问原文地址
快照地址: 访问文章快照