iOS 你所了解的runtime

作者: 1江春水 | 来源:发表于2019-01-10 19:32 被阅读0次
    起来搬砖啦

    我们说Objective-C这门语言是一门动态语言,哪个特性来体现的呢,就是runtime运行时系统来体现的。

    下面通过几个方面来介绍runtime运行时:

    • runtime介绍
    • 消息发送
    • 消息转发
    • runtime使用场景

    runtime介绍

    先看看官方文档的介绍:

    Objective-C语言从编译时间和链接时间到运行时推迟了尽可能多的决策。只要有可能,它就会动态地完成任务。这意味着该语言不仅需要编译器,还需要运行时系统来执行编译代码。运行时系统充当Objective-C语言的一种操作系统;这就是语言运作的原因。

    Objective-C runtime运行时有两个版本,lagecy(遗产)和modern(现代),modern版本运行时随Objective-C2.0推出,Objective-C2.02.0包含了许多新功能,其中一点是当更改类的实例变量布局时,不必重新编译。

    运行时系统的核心是消息传递和转发,以及在消息传递和转发中动态的加载类并查找有关对象的信息。

    不管你是显式的调用了runtime的API还是直接写OC有关的代码,编译器都会生成相应的数据结构和函数。

    runtime运行时系统是一个动态共享库,可以使用 #<objc/runtime>来查看内部的API及各种结构体定义,且runtime是开源的,你可以在这里下载。

    消息发送

    曾经有这么一个面试题:

    - (instancetype)init {
        self = [super init];
        if (self) {
            NSLog(@"class = %@",NSStringFromClass([self class]));
            NSLog(@"class = %@",NSStringFromClass([super class]));
        }
        return self;
    }
    打印:
    class = Person
    class = Person
    

    自己先想想为啥...
    接着看:
    举个例子:

    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self test];
    }
    
    - (void)test {
        NSLog(@"%@",NSStringFromSelector(_cmd));
    }
    
    @end
    

    我们经常说在viewDidLoad方法里通过self调用test方法,[self test],其实确切的应该说成给 self 发送 test 消息 当编译完后该代码转换成了如下代码:

    objc_msgSend(self,sel_registerName("test"));
    

    需要设置下xcode,不让xcode检查是否使用objc_msgSend发送消息,要不然会报错;

    项目target--build Setting --搜索objc_msgSend ---设置为NO即可

    objc_msgSend定义如下:

    id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...);
    

    在想弄清楚runtime是怎么完成消息发送前,我们先来看下,OC对象和类在runtime中的定义:

    /// An opaque type that represents an Objective-C class. 类的定义
    typedef struct objc_class *Class;
    
    /// Represents an instance of a class.代表类的一个实例
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
    //类结构体
    struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;//指向元类
    
    #if !__OBJC2__
        Class _Nullable super_class                              OBJC2_UNAVAILABLE;//指向父类(类也是一个对象)
        const char * _Nonnull name                               OBJC2_UNAVAILABLE;//name
        long version                                             OBJC2_UNAVAILABLE;//版本
        long info                                                OBJC2_UNAVAILABLE;//info
        long instance_size                                       OBJC2_UNAVAILABLE;//实例大小
        struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;//变量列表
        struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;//方法列表
        struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;//缓存
        struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;//协议列表
    #endif
    } OBJC2_UNAVAILABLE;
    

    看完上边的实例及类的定义,加上我们的开发经验,其实已经知道消息发送的一个流程了:

    1. 通过实例self的isa指针找到当前所属类
    2. 在当前类的objc_cache中查找,有没有test方法
    3. cache中没有找到test方法,再去methodList中去查找,找到就执行IMP,没有找到通过isa指针去父类中去找,直到找到,找不到就报unrecognized selector xxx

    方法查找首先去objc_cache中查找是为了提高效率,调用过的方法会存放在缓存中,不能每次调用一个方法都要查找一遍methodList,显然是不行的。缓存的时候会把test的method_name作为key,方法的method_imp实现作为value来存储。

    其他的一些结构体声明:

    • id
    • SEL
    • Method
    • Ivar
    • Category
    • objc_property_t
    • objc_cache
    id

    id类型,就是一个objc_object结构体指针,指向任意对象

    /// A pointer to an instance of a class.
    typedef struct objc_object *id;
    
    SEL

    方法选择器,可以理解为方法ID,可以联想到OC语言一个类中方法不能重载

    /// An opaque type that represents a method selector.
    typedef struct objc_selector *SEL;
    
    Method

    一个方法有方法名,有types,有方法实现;types举例:@"v@:" v代表返回值为void,@ 表示参数为参数为对象类型,: 表示selector方法选择器 具体请移步这里

    struct objc_method {
        SEL _Nonnull method_name  //方法名                               OBJC2_UNAVAILABLE;
        char * _Nullable method_types  //types(@"v@:")                          OBJC2_UNAVAILABLE;
        IMP _Nonnull method_imp   //方法实现IMP                               OBJC2_UNAVAILABLE;
    }                                                
    
    Ivar 实例变量
    struct objc_ivar {
        char * _Nullable ivar_name //变量名                              OBJC2_UNAVAILABLE;
        char * _Nullable ivar_type //变量类型                              OBJC2_UNAVAILABLE;
        int ivar_offset                                          OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space                                                OBJC2_UNAVAILABLE;
    #endif
    } 
    
    Category类别
    struct objc_category {
        char * _Nonnull category_name                            OBJC2_UNAVAILABLE;//分类名
        char * _Nonnull class_name                               OBJC2_UNAVAILABLE;//所属类名
        struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;//实例方法列表
        struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;//类方法列表
        struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;//协议列表
    } 
    
    objc_property_t 属性
    /// An opaque type that represents an Objective-C declared property.类声明的属性
    typedef struct objc_property *objc_property_t;
    
    //获取一个类中的属性列表
    objc_property_t _Nonnull * _Nullable class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount);
    
    objc_cache

    缓存,刚才讲过了,寻找类的方法先是去缓存中找的,内部有个 buckets实例变量,意思很明白,一个方法桶子

    typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;
    
    struct objc_cache {
        unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
        unsigned int occupied                                    OBJC2_UNAVAILABLE;
        Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;
    };
    

    以上简单列举了下,runtime运行时系统其他的结构体声明,理解了这些在加上内部的函数,就能更灵活的运用到自己的项目当中,内部更具体的函数、结构体声明请移步官方文档。

    消息转发

    我们都知道,假如你调用了一个没有实现的方法,会报unrecognised selector xxx,其实即便你没有实现这个方法,通过runtime的消息转发机制,你仍然还有机会让你的程序不崩溃。

    NSObject类中有这么几个方法,以下几个方法就是运行时系统在给你最后机会来自己处理消息转发:

    1、
    + (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    + (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
    2、- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
    3、
    - (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");
    
    挂
    - (void)doesNotRecognizeSelector:(SEL)aSelector;
    

    可以概括为三种方式:

    • 动态的添加方法实现,返回YES
    • 返回实现该方法的对象
    • 通过返回NSInvocation和MethodSignature完成一次完整转发

    动态添加方法实现

    举例:

    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        objc_msgSend(self,sel_registerName("test"));
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == NSSelectorFromString(@"test")) {
            BOOL added = class_addMethod([self class], NSSelectorFromString(@"test"), (IMP)testImp, "v@:");
            if (added) {
                return YES;
            }
            return NO;
        }
        //其他系统访问返回super调用
        return [super resolveInstanceMethod:sel];
    }
    
    
    void testImp(id self,SEL _cmd) {
        NSLog(@"method name = %@",NSStringFromSelector(_cmd));
    }
    @end
    
    打印:
    method name = test
    
    

    来看下官方文档的说明:

    Dynamically provides an implementation for a given selector for an instance method.动态提供一个给定实例的方法实现,说的很明白了。

    一个OC方法默认有两个参数(self 和 SEL),使用class_addMethod函数来动态添加一个函数实现,并返回YES

    返回实现该方法的对象

    如果你实现了forwardingTargetForSelector这个方法,系统就会调用它,会去你返回的那个对象实例中寻找方法,然后调用。

    现在我创建了一个Person类,里边声明了test方法并实现。

    @interface Person : NSObject
    - (void)test;
    @end
    
    @implementation Person
    - (void)test {
        NSLog(@"class_name = %@ , method_name = %@",[self class],NSStringFromSelector(_cmd));
    }
    @end
    
    VC内部:
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        objc_msgSend(self,sel_registerName("test"));
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == NSSelectorFromString(@"test")) {
            return NO;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (aSelector == NSSelectorFromString(@"test")) {
            return [Person new];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    @end
    
    打印:
    class_name = Person , method_name = test
    

    系统首先调用resolveInstanceMethod方法,发现没有找到方法实现,就会调用方法forwardingTargetForSelector,该方法内部返回Person对象实例,Person对象内部实现了test方法,然后调用了test方法。

    看下系统给的解释:

    如果对象实现(或继承)此方法,并返回非零(和非自身)结果,则返回的对象将用作新的接收器对象,并且消息调度将恢复到该新对象,如果这个方法返回self,会直接崩溃。

    也就是系统会重新进行一次消息转发,消息的接收者变成了该方法返回的对象。

    通过返回NSInvocation和MethodSignature完成一次完整转发

    如果以上两个机会你都没有把握,系统就会开启一次完整的消息转发,步骤如下:

    1. 通过实现methodSignatureForSelector方法,实例化一个方法签名对象NSMethodSignature返回,这个方法签名对象是对方法method的一个描述
    2. runtime动态生成一个NSInvocation对象传递给forwardInvocation方法
    3. 通过实现forwardInvocation来指定一个对象,通过这个对象完成消息转发

    看下面代码:

    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        objc_msgSend(self,sel_registerName("test"));
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if (aSelector == @selector(test)) {
            NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];
            return sig;
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    //这个官方有示例
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        SEL sel = [anInvocation selector];
        Person *p = [[Person alloc] init];
        if ([p respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:p];
        } else {
            [super forwardInvocation:anInvocation];
        }
    }
    @end
    
    打印:
    class_name = Person , method_name = test
    

    有关NSMethodSignature和NSInvocation类,内部很简单,可以点进去看下。

    以上就是消息转发的一个流程。

    runtime使用场景

    结合自己项目中runtime使用场景来列举下:

    1. 对象绑定
    2. 方法交换
    3. 自定义对象归档返归档
    4. dic-model
    5. KVO自定义实现

    1. 对象绑定

    直接看例子:

    @interface Person : NSObject
    - (void)bind;
    @end
    
    
    NSString * const personSource = @"personSource";
    
    @implementation Person
    - (NSString *)description {
        NSString *str = objc_getAssociatedObject(self, &personSource);
        return [NSString stringWithFormat:@"%@-刚出生就拥有:%@",self.name,str];
    }
    
    - (void)bind {
        objc_setAssociatedObject(self, &personSource, @"北京二环两套房+500万", OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (void)dealloc {
        objc_removeAssociatedObjects(self);
    }
    
    //VC中
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        Person *p = [[Person alloc] init];
        p.name = @"小明";
        [p bind];
        NSLog(@"%@",p);
    }
    
    打印:
    小明-刚出生就拥有:北京二环两套房+500万
    @end
    

    通过上边这个例子可以看出:现在是拼爹的时代,哈哈。

    看下绑定对象API:

    /** 
     * 通过给定key和绑定策略将value绑定到object上!
     * 
     * @param object 被绑定的对象
     * @param key 绑定value使用的key,注意:key-value一一对应
     * @param value 绑定的value,传空表示清空value
     * @param policy 绑定策略,用于绑定对象的内存管理
     * @see objc_setAssociatedObject
     * @see objc_removeAssociatedObjects
     */
    void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                             id _Nullable value, objc_AssociationPolicy policy)
        OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
        
      /** 
     * 根据key返回对象绑定的value值
     * @param object 被绑定的对象
     * @param key key
     * @return 绑定到对象上的value值
     * @see objc_setAssociatedObject
     */
    id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
        OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
     
    
    //清除所有绑定到对象上的value,一般不用这个方法,使用objc_setAssociatedObject方法,value传递nil即可
    void objc_removeAssociatedObjects(id _Nonnull object)
        OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
    

    绑定策略:

    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
        OBJC_ASSOCIATION_ASSIGN = 0,//弱引用+原子性      
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,//强引用+非原子性
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,//copy+非原子性
        OBJC_ASSOCIATION_RETAIN = 01401, //强引用+原子性      
        OBJC_ASSOCIATION_COPY = 01403  //copy+原子性 
    };
    

    缓存策略用于绑定对象value内存管理用的,跟常见的属性修饰符管理完全相同。

    2. 方法交换

    方法交换,核心就是交换方法的实现(IMP),有图为证


    屏幕快照 2019-01-10 18.33.39.png

    简单说一下核心方法实现:

    1. 根据旧类class和SEL,获取要被替换的Method,即originMethod
    2. 根据新类class和SEL获取新加的Method,即cusMethod
    3. 如果没有originMethod,直接向旧类添加新方法,添加成功,就替换老方法的实现(IMP)
    4. 直接交换方法实现
    void tt_swizzleMethodImplementation(Class originC,Class cusC ,SEL originSEL, SEL cusSEL) {
        Method originMethod = class_getInstanceMethod(originC, originSEL);
        Method cusMethod = class_getInstanceMethod(cusC, cusSEL);
        if (!originMethod) {
            BOOL added = class_addMethod(originC, cusSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
            if (added) {
                class_replaceMethod(originC, originSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
            }
        } else {
            method_exchangeImplementations(originMethod, cusMethod);
        }
    }
    

    这个我前一阵写过一个例子,就是通过方法交换来优化appDelegate内部的推送部分代码的,优化后你会发现,appDelegate推送部分代码非常简洁,里边写的比较详细,有需要的可以看下推送优化传送门,方法交换就此别过。

    3. 自定义对象归档返归档

    当存储自定义数据(model)时,需要用到归档反归档。
    系统对归档的内部实现猜测是以key-value键值对存储的。通过实现系统的两个协议来告知系统你要存储的数据。需要实现NSCoding协议内的两个方法

    - (void)encodeWithCoder:(NSCoder *)aCoder;
    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; 
    

    一般如果model属性少的话,手写还是可以的,但是属性过多还要手写? 通过runtime来动态遍历出model的属性key,然后来赋值或取值。MJExtension内部有实现,而且一个宏搞定。

    看下核心代码的实现:

    - (void)tt_decode:(NSCoder *)decoder {
        unsigned int count;
        objc_property_t *properties = class_copyPropertyList([self class], &count);
        for (unsigned int i = 0; i < count; i++) {
            objc_property_t property = properties[i];
            const char *name = property_getName(property);
            NSString *propertyName = [NSString stringWithUTF8String:name];
            NSString *propertyValue = [decoder decodeObjectForKey:propertyName];
            if (nil != propertyValue) {
                [self setValue:propertyValue forKey:propertyName];
            }
        }
        free(properties);
    }
    
    - (void)tt_encode:(NSCoder *)encoder {
        unsigned int count;
        objc_property_t *properties = class_copyPropertyList([self class], &count);
        for (unsigned int i = 0; i < count; i++) {
            objc_property_t property = properties[i];
            const char *name = property_getName(property);
            NSString *propertyName = [NSString stringWithUTF8String:name];
            id value = [self valueForKey:propertyName];
            if (nil != value) {
                [encoder encodeObject:value forKey:propertyName];
            }
        }
        free(properties);
    }
    

    这块不多说,需要注意的是最后需要释放objc_property_t指针,因为这是在OC环境,对C语言不自动管理内存。

    dic-model(字典转模型)

    字典转模型也不多说,看看常用的开源框架就行,核心是遍历出对象的属性列表然后把以属性名(可以自定义)为key从字典取出对应的value赋值给对象属性。

    KVO自定义实现

    当你了解了KVO的底层原理后,也可以尝试着使用runtime运行时来写一个自己的KVO,当然有可能会有bug,即便自己实现一个KVO也没必要用到项目中,毕竟系统的或RAC很成熟了,自己写的目的其实很简单,通过自己实现可以更熟悉底层的实现,从而给自己带来一些思考。前一阵自己实现过一个,也有demo,有需要的自己去看吧,这里不多说。

    面试题

    回过头来看看那个面试题,我们知道[self class] 等价于

    objc_msgSend(self, @selector(class));
    

    那么[supser class]等价于

        objc_msgSend(self, @selector(class));
        struct objc_super superClass = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self))
        };
        objc_msgSendSuper(&superClass, _cmd);
    
    

    通过上面转化后的代码我们就清楚了,消息的接收者都是self,super只是一个标识符而已。

    以上就是runtime运行时的一个概括汇总吧,只有当你真正理解了runtime,才会领悟这门语言动态的含义。

    奋然前行!

    点赞是你的态度!

    相关文章

      网友评论

        本文标题:iOS 你所了解的runtime

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