美文网首页
Runtime : 运行时详解

Runtime : 运行时详解

作者: lp_lp | 来源:发表于2019-09-28 22:11 被阅读0次
    image.png

    一、简介

    1.1 什么是Runtime
    Runtime是一套底层纯C语言API,OC代码最终都会被编译器转化为运行时代码,通过消息机制决定函数调用方式,这也是OC作为动态语言使用的基础。
    复制代码
    
    2.1 消息机制的基本原理

    在Object-C的语言中,对象方法调用都是类似[receiver selector] 的形式,其本质:就是让对象在运行时发送消息的过程。

    而方法调用[receiver selector] 分为两个过程:

    • 编译阶段

    [receiver selector] 方法被编译器转化,分为两种情况:

    1.不带参数的方法被编译为:objc_msgSend(receiver,selector)
    2.带参数的方法被编译为:objc_msgSend(recevier,selector,org1,org2,…)
    复制代码
    
    • 运行时阶段

    消息接收者recever寻找对应的selector,也分为两种情况:

    1.接收者能找到对应的selector,直接执行接收receiver对象的selector方法。
    2.接收者找不到对应的selector,消息被转发或者临时向接收者添加这个selector对应的实现内容,否则崩溃
    复制代码
    

    总而言之:

    OC调用方法[receiver selector],编译阶段确定了要向哪个接收者发送message消息,但是接收者如何响应决定于运行时的判断
    复制代码
    
    1.3 Runtime中的概念解析

    1.3.1 objc_msgSend

    所有 Objective-C 方法调用在编译时都会转化为对 C 函数 objc_msgSend 的调用。objc_msgSend(receiver,selector); 是 [receiver selector]; 对应的 C 函数
    复制代码
    

    1.3.2 Object(对象)

    objc/runtime.h 中Object(对象) 被定义为指向 objc_object 结构体 的指针,objc_object结构体 的数据结构如下:

    //runtime对objc_object结构体的定义
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
    //id是一个指向objc_object结构体的指针,即在Runtime中:
    typedef struct objc_object *id;
    
    //OC中的对象虽然没有明显的使用指针,但是在OC代码被编译转化为C之后,每个OC对象其实都是拥有一个isa的指针的
    复制代码
    

    1.3.2 Class(类)

    objc/runtime.h 中Class(类) 被定义为指向 objc_class 结构体 的指针,objc_class结构体 的数据结构如下:

    //runtime对objc_class结构体的定义
    struct objc_class {
        Class _Nonnull isa;                                          // objc_class 结构体的实例指针
    
    #if !__OBJC2__
        Class _Nullable super_class;                                 // 指向父类的指针
        const char * _Nonnull name;                                  // 类的名字
        long version;                                                // 类的版本信息,默认为 0
        long info;                                                   // 类的信息,供运行期使用的一些位标识
        long instance_size;                                          // 该类的实例变量大小;
        struct objc_ivar_list * _Nullable ivars;                     // 该类的实例变量列表
        struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定义的列表
        struct objc_cache * _Nonnull cache;                          // 方法缓存
        struct objc_protocol_list * _Nullable protocols;             // 遵守的协议列表
    #endif
    
    };
    
    //class是一个指向objc_class结构体的指针,即在Runtime中:
    typedef struct objc_class *Class; 
    复制代码
    

    1.3.3 SEL (方法选择器)

    objc/runtime.h 中SEL (方法选择器) 被定义为指向 objc_selector 结构体 的指针:

    typedef struct objc_selector *SEL;
    
    //Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL
    复制代码
    

    注意:

    1.不同类中相同名字的方法对应的方法选择器是相同的。
    2.即使是同一个类中,方法名相同而变量类型不同也会导致它们具有相同的方法选择器。
    复制代码
    

    通常获取SEL有三种方法:

    1.OC中,使用@selector(“方法名字符串”)
    2.OC中,使用NSSelectorFromString(“方法名字符串”)
    3.Runtime方法,使用sel_registerName(“方法名字符串”)
    复制代码
    

    1.3.4 Ivar

    objc/runtime.h 中Ivar 被定义为指向 objc_ivar 结构体 的指针,objc_ivar结构体 的数据结构如下:

    struct objc_ivar {
        char * Nullable ivar_name                               OBJC2UNAVAILABLE;
        char * Nullable ivar_type                               OBJC2UNAVAILABLE;
        int ivar_offset                                          OBJC2_UNAVAILABLE;
    #ifdef LP64
        int space                                                OBJC2_UNAVAILABLE;
    #endif
    } 
    
    //Ivar代表类中实例变量的类型,是一个指向ojbcet_ivar的结构体的指针
    typedef struct objc_ivar *Ivar;
    复制代码
    

    objc_class中看到的ivars成员列表,其中的元素就是Ivar,可以通过实例查找其在类中的名字,这个过程被称为反射,下面的class_copyIvarList获取的不仅有实例变量还有属性:

       Ivar *ivarList = class_copyIvarList([self class], &count);
        for (int i= 0; i<count; i++) {
            Ivar ivar = ivarList[i];
            const char *ivarName = ivar_getName(ivar);
            NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
        }
        free(ivarList);
    复制代码
    

    1.3.5 Method(方法)

    objc/runtime.h 中Method(方法) 被定义为指向 objc_method 结构体 的指针,在objct_class定义中看到methodLists,其中的元素就是Method,objc_method结构体 的数据结构如下:

    struct objc_method {
        SEL _Nonnull method_name;                    // 方法名
        char * _Nullable method_types;               // 方法类型
        IMP _Nonnull method_imp;                     // 方法实现
    };
    
    //Method表示某个方法的类型
    typedef struct objc_method *Method;
    
    复制代码
    

    二、和Runtime交互的三种方式

    2.1 OC源代码
    OC代码会在编译阶段被编译器转化。OC中的类、方法和协议等在Runtime中都由一些数据结构来定义。
    所以在日常的项目开发过程中,使用OC语言进行编码时,这已经是在和Runtime进行交互了,只是这个过程对于开发者而言是无感的
    复制代码
    
    2.2 NSObject方法
    Runtime的最大特征就是实现了OC语言的动态特性。
    复制代码
    

    作为大部分Objective-C类继承体系的根类的NSObject,其本身就具有了一些非常具有运行时动态特性的方法, 比如:

    1\. -respondsToSelector:方法可以检查在代码运行阶段当前对象是否能响应指定的消息
    
    2\. -description:返回当前类的描述信息 
    
    3\. -isKindOfClass: 和 -isMemberOfClass:  检查对象是否存在于指定的类的继承体系中
    
    4\. -conformsToProtocol:    检查对象是否实现了指定协议类的方法;
    
    5\. -methodForSelector:     返回指定方法实现的地址。
    复制代码
    
    2.3 使用Runtime函数
    Runtime系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。
    复制代码
    

    在项目工程代码里引用Runtime的头文件,同样能够实现类似OC代码的效果:

    //相当于:Class class = [UIView class];
    Class viewClass = objc_getClass("UIView");
    
    //相当于:UIView *view = [UIView alloc];
    UIView *view = ((id (*)(id, SEL))(void *)objc_msgSend)((id)viewClass, sel_registerName("alloc"));
    
    //相当于:UIView *view = [view init];
    ((id (*)(id, SEL))(void *)objc_msgSend)((id)view, sel_registerName("init"));
    复制代码
    

    三、Runtime消息转发

    3.1 动态方法解析与消息转发
    • 动态方法解析:动态添加方法

    Runtime足够强大,能够在运行时动态添加一个未实现的方法,这个功能主要有两个应用场景:

    1\. 动态添加未实现方法,解决代码中因为方法未找到而报错的问题;
    2\. 利用懒加载思路,若一个类有很多个方法,同时加载到内存中会耗费资源,可以使用动态解析添加方法
    复制代码
    

    方法动态解析主要用到的方法如下:

    //OC方法:
    //类方法未找到时调起,可于此添加类方法实现
    + (BOOL)resolveClassMethod:(SEL)sel
    
    //实例方法未找到时调起,可于此添加实例方法实现
    + (BOOL)resolveInstanceMethod:(SEL)sel
    
    //Runtime方法:
    /**
     运行时方法:向指定类中添加特定方法实现的操作
     @param cls 被添加方法的类
     @param name selector方法名
     @param imp 指向实现方法的函数指针
     @param types imp函数实现的返回值与参数类型
     @return 添加方法是否成功
     */
    BOOL class_addMethod(Class _Nullable cls,
                         SEL _Nonnull name,
                         IMP _Nonnull imp,
                         const char * _Nullable types)
    复制代码
    
    • 解决方法无响应崩溃问题

    执行OC方法其实就是一个发送消息的过程,若方法未实现,可以利用方法动态解析与消息转发来避免程序崩溃,这主要涉及下面一个处理未实现消息的过程:

    在这个过程中,可能还会使用到的方法有:

    例子:
    #import "ViewController.h"
    #import <objc/runtime.h>
    
    @interface ViewController ()
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 执行 fun 函数
        [self performSelector:@selector(fun)];
    }
    
    // 重写 resolveInstanceMethod: 添加对象方法实现
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(fun)) { // 如果是执行 fun 函数,就动态解析,指定新的 IMP
            class_addMethod([self class], sel, (IMP)funMethod, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    void funMethod(id obj, SEL _cmd) {
        NSLog(@"funMethod"); //新的 fun 函数
    }
    @end
    
    //日志输出:
    
    2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] funMethod
    复制代码
    

    从执行任务的输出日志中,可以看到:

    虽然没有实现 fun 方法,但是通过重写 resolveInstanceMethod: ,利用 class_addMethod 方法添加对象方法实现 funMethod 方法,并执行。从打印结果来看,成功调起了funMethod 方法。
    复制代码
    
    3.2 消息接收者重定向:

    如果上一步中 +resolveInstanceMethod:或者 +resolveClassMethod: 没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向。

    如果当前对象实现了 -forwardingTargetForSelector:Runtime 就会调用这个方法,允许将消息的接受者转发给其他对象,其主要方法如下:

    //重定向类方法的消息接收者,返回一个类
    - (id)forwardingTargetForSelector:(SEL)aSelector
    
    //重定向实例方法的消息接受者,返回一个实例对象
    - (id)forwardingTargetForSelector:(SEL)aSelector
    复制代码
    
    例子:
    #import "ViewController.h"
    #import <objc/runtime.h>
    
    @interface Person : NSObject
    - (void)fun;
    @end
    
    @implementation Person
    
    - (void)fun {
        NSLog(@"fun");
    }
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 执行 fun 方法
        [self performSelector:@selector(fun)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        return YES; // 为了进行下一步 消息接受者重定向
    }
    
    // 消息接受者重定向
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (aSelector == @selector(fun)) {
            return [[Person alloc] init];
            // 返回 Person 对象,让 Person 对象接收这个消息
        }
    
        return [super forwardingTargetForSelector:aSelector];
    }
    
    //日志输出:
    
    2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] fun
    复制代码
    

    从执行任务的输出日志中,可以看到:

    虽然当前 ViewController 没有实现 fun 方法,+resolveInstanceMethod: 也没有添加其他函数实现。
    但是我们通过 forwardingTargetForSelector 把当前 ViewController 的方法转发给了 Person 对象去执行了
    复制代码
    

    通过forwardingTargetForSelector 可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是 nil,也不是 self,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向流程

    3.3 消息重定向:

    如果经过消息动态解析、消息接受者重定向,Runtime 系统还是找不到相应的方法实现而无法响应消息,Runtime 系统会利用 -methodSignatureForSelector: 方法获取函数的参数和返回值类型。

    其过程:

    1\. 如果 -methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,
       并通过 -forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
    2\. 如果 -methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 -doesNotRecognizeSelector: 消息,程序也就崩溃了
    复制代码
    

    所以可以在-forwardInvocation:方法中对消息进行转发。

    其主要方法:

    // 消息重定向
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    
    // 获取函数的参数和返回值类型,返回签名
    - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;
    复制代码
    
    例子:
    #import "ViewController.h"
    #import <objc/runtime.h>
    
    @interface Person : NSObject
    - (void)fun;
    @end
    
    @implementation Person
    - (void)fun {
        NSLog(@"fun");
    }
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 执行 fun 函数
        [self performSelector:@selector(fun)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        return YES; // 为了进行下一步 消息接受者重定向
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        return nil; // 为了进行下一步 消息重定向
    }
    
    // 获取函数的参数和返回值类型,返回签名
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
    
        return [super methodSignatureForSelector:aSelector];
    }
    
    // 消息重定向
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        SEL sel = anInvocation.selector;   // 从 anInvocation 中获取消息
    
        Person *p = [[Person alloc] init];
    
        if([p respondsToSelector:sel]) {   // 判断 Person 对象方法是否可以响应 sel
            [anInvocation invokeWithTarget:p];  // 若可以响应,则将消息转发给其他对象处理
        } else {
            [self doesNotRecognizeSelector:sel];  // 若仍然无法响应,则报错:找不到响应方法
        }
    }
    @end
    
    //日志输出:
    2019-09-01 23:24:34.911774+0800 XKRuntimeKit[30032:8724248] fun
    复制代码
    

    从执行任务的输出日志中,可以看到:

    在 -forwardInvocation: 方法里面让 Person 对象去执行了 fun 函数
    复制代码
    

    既然 -forwardingTargetForSelector:-forwardInvocation: 都可以将消息转发给其他对象处理,那么两者的区别在哪?

    区别就在于 -forwardingTargetForSelector: 只能将消息转发给一个对象。而 -forwardInvocation: 可以将消息转发给多个对象。
    复制代码
    

    四、Runtime的应用

    4.1 动态方法交换

    实现动态方法交换(Method Swizzling )是Runtime中最具盛名的应用场景,其原理是:

    通过Runtime获取到方法实现的地址,进而动态交换两个方法的功能
    复制代码
    
    关键方法:
    //获取类方法的Mthod
    Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
    
    //获取实例对象方法的Mthod
    Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
    
    //交换两个方法的实现
    void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
    复制代码
    
    • 动态方法交换
    #import "RuntimeKit.h"
    #import <objc/runtime.h>
    
    @implementation RuntimeKit
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
    
            //交换方法的实现,并测试打印
            Method methodA = class_getInstanceMethod([self class], @selector(testA));
            Method methodB = class_getInstanceMethod([self class], @selector(testB));
            method_exchangeImplementations(methodA, methodB);
    
            [self testA];
            [self testB];
        }
        return self;
    }
    
    - (void)testA{
        NSLog(@"我是A方法");
    }
    
    - (void)testB{
        NSLog(@"我是B方法");
    }
    @end
    
    日志输出:
    2019-09-01 21:25:32.858860+0800 XKRuntimeKit[1662:280727] 我是B方法
    2019-09-01 21:25:32.859059+0800 XKRuntimeKit[1662:280727] 我是A方法
    复制代码
    
    • 拦截并替换系统方法
    #import "UIViewController+xk.h"
    #import <objc/runtime.h>
    
    @implementation UIViewController (xk)
    
    + (void)load{
    
        //获取系统方法地址
        Method sytemMethod = class_getInstanceMethod([self class], @selector(viewWillAppear:));
    
        //获取自定义方法地址
        Method customMethod = class_getInstanceMethod([self class], @selector(run_viewWillAppear:));
    
        //判断存在与否
        if (!class_addMethod([self class], @selector(viewWillAppear:), method_getImplementation(customMethod), method_getTypeEncoding(customMethod))) {
            method_exchangeImplementations(sytemMethod, customMethod);
        }
        else{
            class_replaceMethod([self class], @selector(run_viewWillAppear:), method_getImplementation(sytemMethod), method_getTypeEncoding(sytemMethod));
        }
    }
    
    - (void)run_viewWillAppear:(BOOL)animated{
        [self run_viewWillAppear:animated];
        NSLog(@"我是运行时替换的方法-viewWillAppear");
    }
    
    - (void)run_viewWillDisappear:(BOOL)animated{
        [self run_viewWillDisappear:animated];
        NSLog(@"我是运行时替换的方法-viewWillDisappear");
    }
    @end
    
    日志输出:
    
    2019-09-01 21:36:55.610385+0800 XKRuntimeKit[1921:310118] 我是运行时替换的方法-viewWillAppear
    复制代码
    

    将该分类引入,从执行结果可以看到,但系统的控制器执行viewWillAppear时,则会进入已经替换的方法run_viewWillAppear之中。

    4.2 类目添加新的属性

    在日常开发过程中,常常会使用类目Category为一些已有的类扩展功能。虽然继承也能够为已有类增加新的方法,而且相比类目更是具有增加属性的优势,但是继承毕竟是一个重量级的操作,添加不必要的继承关系无疑增加了代码的复杂度。

    遗憾的是,OC的类目并不支持直接添加属性
    复制代码
    

    为了实现给分类添加属性,还需借助 Runtime的关联对象(Associated Objects)特性,它能够帮助我们在运行阶段将任意的属性关联到一个对象上:

    /**
     1.给对象设置关联属性
     @param object 需要设置关联属性的对象,即给哪个对象关联属性
     @param key 关联属性对应的key,可通过key获取这个属性,
     @param value 给关联属性设置的值
     @param policy 关联属性的存储策略(对应Property属性中的assign,copy,retain等)
     OBJC_ASSOCIATION_ASSIGN             @property(assign)。
     OBJC_ASSOCIATION_RETAIN_NONATOMIC   @property(strong, nonatomic)。
     OBJC_ASSOCIATION_COPY_NONATOMIC     @property(copy, nonatomic)。
     OBJC_ASSOCIATION_RETAIN             @property(strong,atomic)。
     OBJC_ASSOCIATION_COPY               @property(copy, atomic)。
     */
    void objc_setAssociatedObject(id _Nonnull object,
                                  const void * _Nonnull key,
                                  id _Nullable value,
                                  objc_AssociationPolicy policy)
    
    /**
     2.通过key获取关联的属性
     @param object 从哪个对象中获取关联属性
     @param key 关联属性对应的key
     @return 返回关联属性的值
     */
    id _Nullable objc_getAssociatedObject(id _Nonnull object,
                                          const void * _Nonnull key)
    
    /**
     3.移除对象所关联的属性
     @param object 移除某个对象的所有关联属性
     */
    void objc_removeAssociatedObjects(id _Nonnull object)
    复制代码
    

    注意:

    key与关联属性一一对应,我们必须确保其全局唯一性,常用我们使用@selector(methodName)作为key
    复制代码
    
    例子:

    UIViewController+xk.h中新增一个name属性:

    @interface UIViewController (xk)
    
    //新增属性:名称
    @property(nonatomic,copy)NSString * name;
    
    - (void)clearAssociatedObjcet;
    @end
    复制代码
    

    UIViewController+xk.m中补充对应的实现:

    #import "UIViewController+xk.h"
    #import <objc/runtime.h>
    
    @implementation UIViewController (xk)
    
    //set方法
    - (void)setName:(NSString *)name{
        objc_setAssociatedObject(self,
                                 @selector(name),
                                 name,
                                 OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    //get方法
    - (NSString *)name{
        return objc_getAssociatedObject(self,
                                        @selector(name));
    }
    
    //添加一个自定义方法,用于清除所有关联属性
    - (void)clearAssociatedObjcet{
        objc_removeAssociatedObjects(self);
    }
    @end
    
    复制代码
    

    执行任务:

    
    ViewController * vc = [ViewController new];
    vc.name = @"我是根控制器";
    NSLog(@"获取关联属性name:%@",vc.name);
    
    [vc clearAssociatedObjcet];
    NSLog(@"获取关联属性name:%@",vc.name);
    
    日志输出:
    2019-09-01 21:50:05.162915+0800 XKRuntimeKit[2066:335327] 获取关联属性name:我是根控制器
    2019-09-01 21:50:05.163080+0800 XKRuntimeKit[2066:335327] 获取关联属性name:(null)
    复制代码
    

    同样的,使用运行时还可以为类目新增一些自身没有的方法,比如给UIView新增点击事件:

    #import <objc/runtime.h>
    
    static char onTapGestureKey;
    static char onTapGestureBlockKey;
    
    @implementation UIView (Gesture)
    
    //添加轻拍手势
    - (void)addTapGestureActionWithBlock:(onGestureActionBlock)block{
        UITapGestureRecognizer *gesture = objc_getAssociatedObject(self, &onTapGestureKey);
        self.userInteractionEnabled = YES;
        if (!gesture){
            gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(xk_handleActionForTapGesture:)];
            [self addGestureRecognizer:gesture];
            objc_setAssociatedObject(self, &onTapGestureKey, gesture, OBJC_ASSOCIATION_RETAIN);
        }
    
        //添加点击手势响应代码块属性
        objc_setAssociatedObject(self, &onTapGestureBlockKey, block, OBJC_ASSOCIATION_COPY);
    }
    
    //点击回调
    - (void)xk_handleActionForTapGesture:(UITapGestureRecognizer*)sender{
        onGestureActionBlock block = objc_getAssociatedObject(self, &onTapGestureBlockKey);
        if (block) block(sender);
    }
    @end
    复制代码
    

    但是使用运行时给类目新增代理属性时,需要注意循环应用问题,由于运行时执行添加的属性都是retain操作,所以往往在执行过程会导致对应的 delegate 得不到释放,因而会导致崩溃,对此,可以进行以下修改操作:

    场景: 给UIView新增emptyDataDelegate空页面代理,以处理一些异常情况的显示

    UIView+EmptyDataSet.h中新增一个emptyDataDelegate属性,:

    //页面无数据代理
    @protocol XKEmptyDataSetDelegate <NSObject>
    @optional
    //占位文字
    - (NSString*)placeholderForEmptyDataSet:(UIScrollView*)scrollView;
    @end
    
    //空页面设置
    @interface UIView (EmptyDataSet)
    @property (nonatomic,weak) id<XKEmptyDataSetDelegate>emptyDataDelegate;
    @end
    
    复制代码
    

    UIView+EmptyDataSet.m中借助XKEmptyDataWeakObjectContainer实现其方法:

    //弱引用代理
    @interface XKEmptyDataWeakObjectContainer : NSObject
    @property (nonatomic,weak,readonly)id weakObject;
    - (instancetype)initWithWeakObject:(id)object;
    @end
    
    @implementation XKEmptyDataWeakObjectContainer
    - (instancetype)initWithWeakObject:(id)object{
        self = [super init];
        if (self) {
            _weakObject = object;
        }
        return self;
    }
    @end
    
    static char xk_EmptyDataSetDelegateKey;
    
    //空视图设置
    @implementation UIView (EmptyDataSet)
    - (void)setEmptyDataDelegate:(id<XKEmptyDataSetDelegate>)emptyDataDelegate{
         objc_setAssociatedObject(self, &xk_EmptyDataSetDelegateKey, [[XKEmptyDataWeakObjectContainer alloc] initWithWeakObject:emptyDataDelegate], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (id<XKEmptyDataSetDelegate>)emptyDataDelegate{
        XKEmptyDataWeakObjectContainer * container = objc_getAssociatedObject(self, &xk_EmptyDataSetDelegateKey);
        return container.weakObject;
    }
    @end
    复制代码
    
    4.3 获取类详细属性
    • 获取属性列表
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i = 0; i<count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSLog(@"PropertyName(%d): %@",i,[NSString stringWithUTF8String:propertyName]);
    }
    free(propertyList);
    复制代码
    
    • 获取所有成员变量
    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (int i= 0; i<count; i++) {
        Ivar ivar = ivarList[i];
        const char *ivarName = ivar_getName(ivar);
        NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
    }
    free(ivarList);
    复制代码
    
    • 获取所有方法
    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL mthodName = method_getName(method);
        NSLog(@"MethodName(%d): %@",i,NSStringFromSelector(mthodName));
    }
    free(methodList);
    复制代码
    
    • 获取当前遵循的所有协议
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (int i=0; i<count; i++) {
        Protocol *protocal = protocolList[i];
        const char *protocolName = protocol_getName(protocal);
        NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
    }
    free(propertyList); //C语言中使用Copy操作的方法,要注意释放指针,防止内存泄漏
    复制代码
    
    4.4 解决同一方法高频率调用的效率问题

    Runtime源码中的IMP作为函数指针,指向方法的实现。通过它,可以绕开发送消息的过程来提高函数调用的效率。当需要持续大量重复调用某个方法的时候,会十分有用,如下:

    void (*setter)(id, SEL, BOOL);
    int i;
    
    setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
    for ( i = 0 ; i < 1000 ; i++ )
        setter(targetList[i], @selector(setFilled:), YES);
    复制代码
    
    4.5 动态操作属性
    • 修改私有属性
    场景:
    我们使用第三方框架里的Person类,在特殊需求下想要更改其私有属性nickName,这样的操作我们就可以使用Runtime可以动态修改对象属性。
    复制代码
    
    Person *ps = [[Person alloc] init];
    NSLog(@"nickName: %@",[ps valueForKey:@"nickName"]); //null
    
    //第一步:遍历对象的所有属性
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([ps class], &count);
    for (int i= 0; i<count; i++) {
        //第二步:获取每个属性名
        Ivar ivar = ivarList[i];
        const char *ivarName = ivar_getName(ivar);
        NSString *propertyName = [NSString stringWithUTF8String:ivarName];
        if ([propertyName isEqualToString:@"_nickName"]) {
            //第三步:匹配到对应的属性,然后修改;注意属性带有下划线
            object_setIvar(ps, ivar, @"allenlas");
        }
    }
    NSLog(@"nickName: %@",[ps valueForKey:@"nickName"]); //allenlas
    复制代码
    
    • 改进iOS归档和解档

    归档是一种常用的轻量型文件存储方式,但是它有个弊端:

    在归档过程中,若一个Model有多个属性,我们不得不对每个属性进行处理,非常繁琐
    复制代码
    

    归档操作主要涉及两个方法: encodeObjectdecodeObjectForKey ,对于这两个方法,可以利用Runtime 来进行改进:

    //原理:使用Runtime动态获取所有属性
    //解档操作
    - (instancetype)initWithCoder:(NSCoder *)aDecoder{
        self = [super init];
        if (self) {
            unsigned int count = 0;
    
            Ivar *ivarList = class_copyIvarList([self class], &count);
            for (int i = 0; i < count; i++) {
                Ivar ivar = ivarList[i];
                const char *ivarName = ivar_getName(ivar);
                NSString *key = [NSString stringWithUTF8String:ivarName];
                id value = [aDecoder decodeObjectForKey:key];
                [self setValue:value forKey:key];
            }
            free(ivarList); //释放指针
        }
        return self;
    }
    
    //归档操作
    - (void)encodeWithCoder:(NSCoder *)aCoder{
        unsigned int count = 0;
    
        Ivar *ivarList = class_copyIvarList([self class], &count);
        for (NSInteger i = 0; i < count; i++) {
            Ivar ivar = ivarList[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
    
            id value = [self valueForKey:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivarList); //释放指针
    }
    复制代码
    
    测试:
    //--测试归档
    Person *ps = [[Person alloc] init];
    ps.name = @"allenlas";
    ps.age  = 20;
    NSString *temp = NSTemporaryDirectory();
    NSString *fileTemp = [temp stringByAppendingString:@"person.archive"];
    [NSKeyedArchiver archiveRootObject:ps toFile:fileTemp];
    
    //--测试解档
    NSString *temp = NSTemporaryDirectory();
    NSString *fileTemp = [temp stringByAppendingString:@"person.henry"];
    Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:fileTemp];
    NSLog(@"person-name:%@,person-age:%ld",person.name,person.age); 
    //person-name:allenlas,person-age:20
    复制代码
    
    • 实现字典与模型的转换

    在日常项目开发中,经常会使用YYModelMJExtension等对接口返回的数据对象实现转模型操作。对于此,可以利用KVCRuntime 来进行类似的功能实现,在这个过程中需要解决的问题有:

    利用Runtime实现的思路大体如下:

    借助Runtime可以动态获取成员列表的特性,遍历模型中所有属性,然后以获取到的属性名为key,在JSON字典中寻找对应的值value;再将每一个对应Value赋值给模型,就完成了字典转模型的目的。
    复制代码
    
    json数据:
    {
        "id":"10089",
        "name": "Allen",
        "age":"20",
        "position":"iOS开发工程师",
        "address":{
                "country":"中国",
                "province": "广州"
                },
        "tasks":[{
                   "name":"Home",
                   "desc":"app首页开发"
        },{
                   "name":"Train",
                   "desc":"app培训模块开发"
        },{
                   "name":"Me",
                   "desc":"完成个人页面"
        }
        ]
    }
    复制代码
    
    1. 创建NSObject的类目 NSObject+model,用于实现字典转模型
    //在NSObject+model.h中
    
    NS_ASSUME_NONNULL_BEGIN
    
    //AAModel协议,协议方法可以返回一个字典,表明特殊字段的处理规则
    @protocol AAModel<NSObject>
    @optional
    + (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
    @end;
    
    @interface NSObject (model)
    + (instancetype)xk_modelWithDictionary:(NSDictionary *)dictionary;
    @end
    
    NS_ASSUME_NONNULL_END
    
    复制代码
    
    #import "NSObject+model.h"
    #import <objc/runtime.h>
    
    @implementation NSObject (model)
    + (instancetype)xk_modelWithDictionary:(NSDictionary *)dictionary{
    
        //创建当前模型对象
        id object = [[self alloc] init];
        //1.获取当前对象的成员变量列表
        unsigned int count = 0;
        Ivar *ivarList = class_copyIvarList([self class], &count);
    
        //2.遍历ivarList中所有成员变量,以其属性名为key,在字典中查找Value
        for (int i= 0; i<count; i++) {
            //2.1获取成员属性
            Ivar ivar = ivarList[i];
            NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)] ;
    
            //2.2截取成员变量名:去掉成员变量前面的"_"号
            NSString *propertyName = [ivarName substringFromIndex:1];
    
            //2.3以属性名为key,在字典中查找value
            id value = dictionary[propertyName];
    
            //3.获取成员变量类型, 因为ivar_getTypeEncoding获取的类型是"@\"NSString\""的形式
            //所以我们要做以下的替换
            NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];// 替换:
            //3.1去除转义字符:@\"name\" -> @"name"
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
            //3.2去除@符号
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
    
            //4.对特殊成员变量进行处理:
            //判断当前类是否实现了协议方法,获取协议方法中规定的特殊变量的处理方式
            NSDictionary *perpertyTypeDic;
            if([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]){
                perpertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass) withObject:nil];
            }
    
            //4.1处理:字典的key与模型属性不匹配的问题,如id->uid
            id anotherName = perpertyTypeDic[propertyName];
            if(anotherName && [anotherName isKindOfClass:[NSString class]]){
                value =  dictionary[anotherName];
            }
    
            //4.2.处理:模型嵌套模型
            if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
                Class modelClass = NSClassFromString(ivarType);
                if (modelClass != nil) {
                    //将被嵌套字典数据也转化成Model
                    value = [modelClass xk_modelWithDictionary:value];
                }
            }
    
            //4.3处理:模型嵌套模型数组
            //判断当前Vaue是一个数组,而且存在协议方法返回了perpertyTypeDic
            if ([value isKindOfClass:[NSArray class]] && perpertyTypeDic) {
                Class itemModelClass = perpertyTypeDic[propertyName];
                //封装数组:将每一个子数据转化为Model
                NSMutableArray *itemArray = @[].mutableCopy;
                for (NSDictionary *itemDic  in value) {
                    id model = [itemModelClass xk_modelWithDictionary:itemDic];
                    [itemArray addObject:model];
                }
                value = itemArray;
            }
    
            //5.使用KVC方法将Vlue更新到object中
            if (value != nil) {
                [object setValue:value forKey:propertyName];
            }
        }
        free(ivarList); //释放C指针
        return object;
    }
    @end
    复制代码
    
    1. 分别新建 UserModelAddressModelTasksModel对json处理进行处理:
    UserModel类
    #import "NSObject+model.h"
    #import "AddressModel.h"
    #import "TasksModel.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface UserModel : NSObject<AAModel>
    //普通属性
    @property (nonatomic, copy) NSString * uid;
    @property (nonatomic, copy) NSString * name;
    @property (nonatomic, copy) NSString * position;
    @property (nonatomic, assign) NSInteger age;
    
    //嵌套模型
    @property (nonatomic, strong) AddressModel *address;
    
    //嵌套模型数组
    @property (nonatomic, strong) NSArray *tasks;
    @end
    
    NS_ASSUME_NONNULL_END
    
    @implementation UserModel
    + (NSDictionary<NSString *,id> *)modelContainerPropertyGenericClass{
        //需要特别处理的属性
        return @{@"tasks" : [TasksModel class],@"uid":@"id"};
    }
    @end
    复制代码
    
    AddressModel类
    #import "NSObject+model.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface AddressModel : NSObject
    @property (nonatomic, copy) NSString * country;
    @property (nonatomic, copy) NSString * province;
    @end
    
    NS_ASSUME_NONNULL_END
    
    @implementation AddressModel
    @end
    复制代码
    
    TasksModel类
    #import "NSObject+model.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface TasksModel : NSObject
    @property (nonatomic, copy) NSString * name;
    @property (nonatomic, copy) NSString * desc;
    @end
    NS_ASSUME_NONNULL_END
    
    @implementation TasksModel
    @end
    复制代码
    
    1. 代码测试
    - (void)viewDidLoad {
        [super viewDidLoad];
        //读取JSON数据
        NSDictionary * jsonData = @{
                                    @"id":@"10089",
                                    @"name": @"Allen",
                                    @"age":@"20",
                                    @"position":@"iOS开发工程师",
                                    @"address":@{
                                            @"country":@"中国",
                                            @"province":@"广州"
                                            },
                                    @"tasks":@[@{
                                                   @"name":@"Home",
                                                   @"desc":@"app首页开发"
                                                   },@{
                                                   @"name":@"Train",
                                                   @"desc":@"app培训模块开发"
                                                   },@{
                                                   @"name":@"Me",
                                                   @"desc":@"完成个人页面"
                                                   }
                                               ]
                                    };
    
        //字典转模型
        UserModel * user = [UserModel xk_modelWithDictionary:jsonData];
        TasksModel * task = user.tasks[0];
    
        NSLog(@"%@",task.name);
    }
    复制代码
    

    其执行结果,数据结构如下:

    作者:ALLen、LAS
    链接:https://juejin.im/post/5d6bea9651882563e82aff2b

    相关文章

      网友评论

          本文标题:Runtime : 运行时详解

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