美文网首页
IOS高级工程师成长之路 -- 中级篇 《runtime详解》

IOS高级工程师成长之路 -- 中级篇 《runtime详解》

作者: IT小菜逼 | 来源:发表于2018-10-07 23:38 被阅读0次

    Runtime的特性主要是消息(方法)传递,如果消息(方法)在对象中找不到,就进行转发,具体怎么实现的呢。我们从下面几个方面探寻Runtime的实现机制。

    • Runtime介绍
    • Runtime消息传递
    • Runtime消息转发
    • Runtime应用

    Runtime介绍

    Runtime消息传递

    一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo),Runtime时执行的流程是这样的:

    • 首先,通过obj的isa指针找到它的 class ;
    • 在 class 的 method list 找 foo ;
    • 如果 class 中没到 foo,继续往它的 superclass 中找 ;
    • 一旦找到 foo 这个函数,就去执行它的实现IMP 。

    但这种实现有个问题,效率低。但一个class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class 中另一个重要成员objc_cache 做的事情 - 再找到foo 之后,把foo 的method_name 作为key ,method_imp作为value 给存起来。当再次收到foo 消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list。从前面的源代码可以看到objc_cache是存在objc_class 结构体中的。

    Runtime消息转发

    前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。

    • 动态方法解析
    • 备用接收者
    • 完整消息转发


      3333333.png

    动态方法解析

    首先,Objective-C运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。
    实现一个动态方法解析的例子如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        //执行foo函数
        [self performSelector:@selector(foo:)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
            class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    void fooMethod(id obj, SEL _cmd) {
        NSLog(@"Doing foo");//新的foo函数
    }
    

    打印结果:
    2018-04-01 12:23:35.952670+0800 ocram[87546:23235469] Doing foo

    可以看到虽然没有实现foo:这个函数,但是我们通过class_addMethod动态添加fooMethod函数,并执行fooMethod这个函数的IMP。从打印结果看,成功实现了。
    如果resolve方法返回 NO ,运行时就会移到下一步:forwardingTargetForSelector。

    备用接收者

    如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。

    实现一个备用接收者的例子如下:

    #import "ViewController.h"
    #import "objc/runtime.h"
    
    @interface Person: NSObject
    
    @end
    
    @implementation Person
    
    - (void)foo {
        NSLog(@"Doing foo");//Person的foo函数
    }
    
    @end
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        //执行foo函数
        [self performSelector:@selector(foo)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        return YES;//返回YES,进入下一步转发
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (aSelector == @selector(foo)) {
            return [Person new];//返回Person对象,让Person对象接收这个消息
        }
        
        return [super forwardingTargetForSelector:aSelector];
    }
    
    @end
    

    打印结果:
    2018-04-01 12:45:04.757929+0800 ocram[88023:23260346] Doing foo

    可以看到我们通过forwardingTargetForSelector把当前ViewController的方法转发给了Person去执行了。打印结果也证明我们成功实现了转发。

    完整消息转发

    如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
    首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil ,Runtime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送 -forwardInvocation:消息给目标对象。

    实现一个完整转发的例子如下:

    #import "ViewController.h"
    #import "objc/runtime.h"
    
    @interface Person: NSObject
    
    @end
    
    @implementation Person
    
    - (void)foo {
        NSLog(@"Doing foo");//Person的foo函数
    }
    
    @end
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        //执行foo函数
        [self performSelector:@selector(foo)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        return YES;//返回YES,进入下一步转发
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        return nil;//返回nil,进入下一步转发
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
        }
        
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        SEL sel = anInvocation.selector;
    
        Person *p = [Person new];
        if([p respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:p];
        }
        else {
            [self doesNotRecognizeSelector:sel];
        }
    
    }
    
    @end
    

    打印结果:
    2018-04-01 13:00:45.423385+0800 ocram[88353:23279961] Doing foo

    从打印结果来看,我们实现了完整的转发。通过签名,Runtime生成了一个对象anInvocation,发送给了forwardInvocation,我们在forwardInvocation方法里面让Person对象去执行了foo函数。签名参数v@:怎么解释呢,这里苹果文档Type Encodings有详细的解释。

    Runtime应用

    Runtime简直就是做大型框架的利器。它的应用场景非常多,下面就介绍一些常见的应用场景。

    • 关联对象(Objective-C Associated Objects)给分类增加属性
    • 方法魔法(Method Swizzling)方法添加和替换和KVO实现
    • 消息转发(热更新)解决Bug(JSPatch)
    • 实现NSCoding的自动归档和自动解档
    • 实现字典和模型的自动转换(MJExtension)

    关联对象(Objective-C Associated Objects)给分类增加属性

    我们都是知道分类是不能自定义属性和变量的。下面通过关联对象实现给分类添加属性。

    关联对象Runtime提供了下面几个接口:

    //关联对象
    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)
    

    参数解释

    id object:被关联的对象
    const void *key:关联的key,要求唯一
    id value:关联的对象
    objc_AssociationPolicy policy:内存管理的策略
    

    内存管理的策略

    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
        OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                                *   The association is not made atomically. */
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                                *   The association is not made atomically. */
        OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                                *   The association is made atomically. */
        OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                                *   The association is made atomically. */
    };
    

    下面实现一个UIView的Category添加自定义属性defaultColor。

    #import "ViewController.h"
    #import "objc/runtime.h"
    
    @interface UIView (DefaultColor)
    
    @property (nonatomic, strong) UIColor *defaultColor;
    
    @end
    
    @implementation UIView (DefaultColor)
    
    @dynamic defaultColor;
    
    static char kDefaultColorKey;
    
    - (void)setDefaultColor:(UIColor *)defaultColor {
        objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (id)defaultColor {
        return objc_getAssociatedObject(self, &kDefaultColorKey);
    }
    
    @end
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        UIView *test = [UIView new];
        test.defaultColor = [UIColor blackColor];
        NSLog(@"%@", test.defaultColor);
    }
    
    @end
    

    打印结果:
    2018-04-01 15:41:44.977732+0800 ocram[2053:63739] UIExtendedGrayColorSpace 0 1

    方法魔法(Method Swizzling)方法添加和替换和KVO实现

    方法添加
    实际上添加方法刚才在讲消息转发的时候,动态方法解析的时候就提到了。

    //class_addMethod(Class  _Nullable __unsafe_unretained cls, SEL  _Nonnull name, IMP  _Nonnull imp, const char * _Nullable types)
    class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
    
    • cls 被添加方法的类
    • name 添加的方法的名称的SEL
    • imp 方法的实现。该函数必须至少要有两个参数,self,_cmd
    • 类型编码

    方法替换
    下面实现一个替换ViewController的viewDidLoad方法的例子。

    @implementation ViewController
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];
            SEL originalSelector = @selector(viewDidLoad);
            SEL swizzledSelector = @selector(jkviewDidLoad);
            
            Method originalMethod = class_getInstanceMethod(class,originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
            
            //judge the method named  swizzledMethod is already existed.
            BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
            // if swizzledMethod is already existed.
            if (didAddMethod) {
                class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
            }
            else {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        });
    }
    
    - (void)jkviewDidLoad {
        NSLog(@"替换的方法");
        
        [self jkviewDidLoad];
    }
    
    - (void)viewDidLoad {
        NSLog(@"自带的方法");
        
        [super viewDidLoad];
    }
    
    @end
    

    swizzling应该只在+load中完成。 在 Objective-C 的运行时中,每个类有两个方法都会自动调用。+load 是在一个类被初始装载时调用,+initialize 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。

    swizzling应该只在dispatch_once 中完成,由于swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once满足了所需要的需求,并且应该被当做使用swizzling 的初始化单例方法的标准。

    KVO实现

    全称是Key-value observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。再MVC大行其道的Cocoa中,KVO机制很适合实现model和controller类之间的通讯。

    KVO的实现依赖于 Objective-C 强大的 Runtime,当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。

    Apple 使用了 isa-swizzling 来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。

    消息转发(热更新)解决Bug(JSPatch)

    实现NSCoding的自动归档和自动解档

    原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。
    核心方法:在Model的基类中重写方法:

    - (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];
        }
    }
    

    相关文章

      网友评论

          本文标题:IOS高级工程师成长之路 -- 中级篇 《runtime详解》

          本文链接:https://www.haomeiwen.com/subject/lqylaftx.html