RunTime的使用

runtime是属于OC的底层,是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。在我们平时编写的OC代码中, 程序运行过程时, 其实最终都是转成了runtime的C语言代码, runtime算是OC的幕后工作者。runtime是属于OC的底层, 可以进行一些非常底层的操作(用OC是无法现实的, 不好实现)

RunTime相关函数

1、 objc_msgSend: 给对象发送消息

    // 创建person对象
    Person *p = [[Person alloc] init];
    // 调用对象方法
    [p sleep];
    // 本质:让对象发送消息
    objc_msgSend(p, @selector(sleep));

2、method_exchangeImplementations:方法的交换

+ (void)load {
    Method method1 = class_getClassMethod(self, @selector(ViewWillAppear));
    Method method2 = class_getClassMethod(self, @selector(FXViewWillAppear));

method_exchangeImplementations(method1, method2);
}

3、 class_copyPropertyList: 遍历某个类所有的属性

        unsigned int outCount;
        objc_property_t * properties = class_copyPropertyList([self class], &outCount);

4、 resolveInstanceMethod: 异常捕获

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(sleep:)) {
        class_addMethod([self class], sel, (IMP)sleepMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

5、objc_getAssociatedObject:属性的绑定、添加

- (void)setDefaultColor:(UIColor *)defaultColor {
    objc_setAssociatedObject(self, &kCommonColorKey, commonColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)defaultColor {
    return objc_getAssociatedObject(self, &kCommonColorKey);
}

等等方法

RunTime消息传递

讲消息传递我们得先爱弄清楚几个函数
isa指针:isa指针是一个指向所属类的指针,它标注着一个实例对象的真实类型
IMP指针:imp指针是指向selector的具体实现的指针
selector:其实就是方法的名称
我们先来看一下object对象的结构体

//我们看到对象里面就一个isa指针
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

然后我们来看一看我们的class类的结构体

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

该类的结构体,里面会有一个Class isa指针、该类的Class super_class 、该类的实例变量列表struct objc_ivar_list *ivars、该类的方法列表struct objc_method_list **methodLists、该类的缓存列表struct objc_cache *cache、还有该类的协议列表struct objc_protocol_list *protocols等等其他的属性。

然后我们再看看该类的方法列表的结构体

扫描二维码关注公众号,回复: 1582092 查看本文章
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

里面有方法的个数int method_count、struct objc_method method_list[1]的方法等等属性

然后我们再看看集体方法的结构体

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

里面有方法的名称SEL method_name、方法的类型char *method_types和方法的imp指针IMP method_imp

其实我们的消息的传递的过程
1、系统先找到该消息的接受对象,然后通过该对象的isa指针找到他的真实类
2、通过他的类找到该类的缓存列表objc_cache
注意这里先去缓存列表里面去找而不是直接去方法列表里面去找的原因是:方法列表里面可能有很多不常用的方法,我们不可能每一次消息传递我们就去遍历一遍方法列表。这样我们可以把我们用的一些方法放到缓存列表里面,这样每次到缓存列表里面去找,如果没找到,我们再去方法列表里面去找,这样节省我们的效率
3、 如果缓存列表里面没有该方法,我们再去方法列表里面去找
4、如果方法列表里面没有找到,我们再去上层super_class里面去找该方法,一直到最上层、 如果还是没有找到,我们会做消息转发,这个会在下文讲到。
5、如果找到了,我们就会找到该方法的IMP指针,再通过IMP指针来找到该方法的真实实现。

RunTime的消息转发

如果上文提消息发送都已经失败了,就会启动消息转发
消息转发的过程
1、Objective-C在运行的时候,会调用+resolveInstanceMethod:或者+resolveClassMethod:方法,个你机会提供一个函数的实现。如果你在该方法里面添加了函数并且返回了Yes,那么运行时会重载一次消息发送。
2、如果返回的是NO,而且目标对象实现了forwardingTargetForSelector:,运行时就会调用这个方法,让你有机会把这个消息转发给其他对象。如果成功了运行时就会重载一次消息发送。
3、如果失败了,我们就会进行最后一步。先是会发送-methodSignatureForSelector:方法,获取函数的类型和返回值的类型。如果该方法返回的是nil,运行时就会抛出doesNotRecognizeSelector:,这个时候消息转发也就失败了,报unrecognized selector错,如果返回的是一个函数的签名,那么运行时就会创建一个NSInvocation对象,并且发送forwardInvocation:消息消息给目标对象。
4、 如果失败了,就会发送doesNotRecognizeSelector:消息,程序结束,报unrecognized selector错。如果成功了就会重载一次消息发送。

以下是流程图
这里写图片描述

runtime的hook黑魔法和类的扩展

什么是黑魔法–Method Swizzling本质上就是对IMP和SEL进行交换。

黑魔法的原理 —Method Swizzing是发生在运行时,主要用于在运行时将两个Method进行交换。
具体使用的方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) — 替换类中已有方法的实现,如果该方法不存在添加该方法
IMP method_getImplementation(Method m) — 获取Method中的IMP
void method_exchangeImplementations(Method m1, Method m2) — 替换Method
Method class_getInstanceMethod(Class cls, SEL name) — 获取类中的某个实例方法(减号方法)
这里写图片描述
黑魔法的使用
之前我们公司有个app上面的字体都是使用systemFontOfSize:来设置的大小。最近来也个UI总监,看了看我们的app说,我们的字体太粗,不好看。但是我的app好几百个地方使用systemFontOfSize:来设置字体的地方。你不可能每个地方挨个的改吧,这样改很麻烦。这个时候我们的黑魔法方法交换就派上用场了。具体看一下我们的实现

#import "UIFont+Addition.h"
#import "Swizzling.h"

@implementation UIFont (Addition)

+ (void)load {
    [self swizzelMethod];
}

+ (void)swizzelMethod {
    SwizzleClassMethods([self class], @selector(systemFontOfSize:), @selector(mySystemFontOfSize:));
}

+ (UIFont *)mySystemFontOfSize:(CGFloat)fontSize {
    return [self systemFontOfSize:fontSize weight:UIFontWeightLight];
}

@end

然后我们来看看void SwizzelInstanceMethods(Class c, SEL orig, SEL new)
方法。

#import "Swizzling.h"

#pragma mark - Swizzling
void SwizzelInstanceMethods(Class c, SEL orig, SEL new)
{
    Method origMethod = class_getInstanceMethod(c, orig);
    Method newMethod = class_getInstanceMethod(c, new);
    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
        method_exchangeImplementations(origMethod, newMethod);
}

void SwizzleClassMethods(Class c, SEL orig, SEL new)
{
    Method origMethod = class_getClassMethod(c, orig);
    Method newMethod = class_getClassMethod(c, new);
    c = object_getClass((id)c);

    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
        method_exchangeImplementations(origMethod, newMethod);
}

针对于上面的黑魔法还有一个弊端,就是现在我们知道我们全局改变的字体的粗细,但是以后换了一批开放人员,又换了新的UI,认为细的字体不好看,这个时候新的开放人员发现使用systemFontOfSize:不起作用,这个时候就比较难发现我们使用了黑魔法的。所以有时候这个方法是给我们节省了很多的事情,但是也会存在一些弊端。

还有一个我们常用的方法,来获取数组的越界,防止崩溃。

#import "NSArray+Addition.h"
#import "Swizzling.h"

@implementation NSArray (Addition)

+ (void)load {
    [self swizzelMethod];
}

+ (void)swizzelMethod {
    SwizzleClassMethods([self class], @selector(objectAtIndex:), @selector(safeObjectAtIndex:));
}

-(id)safeObjectAtIndex:(NSUInteger)index{

    if (index>(self.count-1)) {
        NSAssert(NO, @"数组越界了");
        return nil;
    }
    else{
        return [self safeObjectAtIndex:index];
    }
}

黑魔法还有很多其他运用的场景,我就不一一说明了。

RunTime常用的场景

1、关联对象
关联对象,提供了下面的几个方法

//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)

这个在JSONModel中有使用,你可以去看看我之前写的JSONModel的源码解析

还可以个一个分类中添加属性,比如我们主题切换就使用这个方法,给我们的label家了一个colorType的属性

#import "ThemeLabel.h"
#import "ThemeColor.h"

@implementation  UILabel (theme)

static const char *kUILableTextColorKey;
- (void)setTextColorType:(int)colorType{
    objc_setAssociatedObject(self, &kUILableTextColorKey, @(colorType), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)textColorType{
    return [objc_getAssociatedObject(self, &kUILableTextColorKey) intValue];
}

static const char *kUILableHighlightedTextColorKey;
- (void)setHighlightedTextColorType:(int)colorType{
    if (colorType > 0) {
        objc_setAssociatedObject(self, &kUILableHighlightedTextColorKey, @(colorType), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
}

- (int)highlightedTextColorType{
    return [objc_getAssociatedObject(self, &kUILableHighlightedTextColorKey) intValue];
}

static const char *kUILableShadowTextColorKey;
- (void)setShadowColorType:(int)colorType{
    objc_setAssociatedObject(self, &kUILableShadowTextColorKey, @(colorType), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)shadowColorType{
    return [objc_getAssociatedObject(self, &kUILableShadowTextColorKey) intValue];
}

- (void)applyTheme{
    [super applyTheme];

    if (self.textColorType > 0) {
        self.textColor = ThemeColor(self.textColorType);
    }
    if (self.highlightedTextColorType > 0) {
        self.highlightedTextColor = ThemeColor(self.highlightedTextColorType);
    }
    if (self.shadowColorType > 0) {
        self.shadowColor = ThemeColor(self.shadowColorType);
    }
}

- (void)mySetTextColor:(UIColor *)textColor{
    self.textColorType = textColor.colorType;
    [self mySetTextColor:textColor];
}


- (void)mySetHighlightedTextColor:(UIColor *)highlightedTextColor{
    self.highlightedTextColorType = highlightedTextColor.colorType;
    [self mySetHighlightedTextColor:highlightedTextColor];
}


- (void)mySetShadowColor:(UIColor *)shadowColor{
    self.shadowColorType = shadowColor.colorType;
    [self mySetShadowColor:shadowColor];
}


@end

2、黑魔法-hook
黑魔法的运用,看看我们上面在详解黑魔法的时候讲到过了,可以去看看上面的具体应用

3、消息转发–热更新
JSPatch–热更新更新
JSPatch的原理
JSPatch 能做到通过 JS 调用和改写 OC 方法最根本的原因是 Objective-C 是动态语言,OC 上所有方法的调用/类的生成都通过 Objective-C Runtime 在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法

4、字典和模型的自动转换
JSONModel的应用,这个可以看我们之前的文章JSONModel的源码解析

5、NSCoding的自动归档和自动解档
NSCoding的自动归档和自动解档的原理,是通过runtime提供的class_copyIvarList函数来获取model自身的所有属性,然后遍历model的所有属性,然后对属性进行encode和decode操作。

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    Ivar * ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar ivar = ivars[i];
        NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
}

6、KVO的实现
KVO的原理 — KVO使用了isa-swizzling,当观察Person对象时,KVO的机制会动态的为Person创建一个新的中间类叫:NSKVONotifying_Person,该类继承自对象Person原本的类。而且KVO为NSKVONotifying_Person类重写了该观察属性的setter方法。该setter方法会在调用原类的setter方法之前和之后,通知所有观察对象属性值的更改情况。
其实就是把被观察对象的isa指针,指向被创建中间类NSKVONotifying_Person,移除KVO以后,该中间类NSKVONotifying_Person会被移除掉,该isa指针就会从该中间类NSKVONotifying_Person上指向原本的类。

其实KVO的键值观察通知依赖于 NSObject 的两个方法
willChangeValueForKey:在属性值改变之前通知系统该keyPath的属性值即将改变。
didChangeValueForKey:属性改变了之后,会通知系统该keyPath的属性值已经改变了。

参考文件

https://www.jianshu.com/p/6ebda3cd8052

总结

如果有写的不正确或者侵权的,希望大家给我提出来,我会及时修改。谢谢大家。

猜你喜欢

转载自blog.csdn.net/u014644610/article/details/80596142