美文网首页OC
iOS runtime 详解和使用场景(最详细的使用干货)

iOS runtime 详解和使用场景(最详细的使用干货)

作者: 一不小心灬 | 来源:发表于2021-09-06 19:03 被阅读0次

    一、Runtime介绍

    OC是对C语言的扩展,加入了面向对象和消息发送机制,Runtime是OC的一个核心,是用C语言和汇编语言编写。OC是动态运行时语言,在运行时确定一个对象的类型、调用哪个对象的方法,因此需要Runtime来做类和对象的动态创建,消息传递和消息转发等。OC代码最终会转换成Runtime库中对应的函数结构体。任何语言最终都会被编译为汇编语言,再汇编为机器语言。 OC到可执行文件编译过程:

    OC->Runtime->C->汇编->可执行文件。
    

    Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。Objective-C 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码通过 Foundation 框架的NSObject类定义的方法通过对 runtime 函数的直接调用大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。

    image.png

    二、Runtime源码初探

    runtime 是 OC底层的一套C语言的API(引入 <objc/runtime.h><objc/message.h>),编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)。
    比如我们创建了一个对象 [[NSObject alloc]init],最终被转换为几万行代码,截取最关键的一句可以看到底层是通过runtime创建的对象

    image.png
    删除掉一些强制转换语句,可以看到调用方法本质就是发消息,[[NSObject alloc]init]语句发了两次消息,第一次发了alloc 消息,第二次发送init 消息。利用这个功能我们可以探究底层,比如block的实现原理。
    image.png

    三、Runtime功能介绍+使用场景

    • 动态添加属性
    • 动态添加方法
    • 方法交换
    • 归档接档
    • 字典转模型

    1.动态添加属性

    使用场景: 给系统的类添加属性的时候,可以使用runtime动态添加属性方法;

    @implementation NSObject (Property)
    
    - (void)setName:(NSString *)name
    {
        /*
         object:保存到哪个对象中
         key:用什么属性保存 属性名
         value:保存值
         policy:策略,strong,weak
         */
        objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (NSString *)name
    {
        return objc_getAssociatedObject(self, "name");
    }
    
    - (void)viewDidLoad {
    
      [super viewDidLoad];
    
       self.view.backgroundColor = [UIColor orangeColor];
    
      //给系统NSObject类动态添加属性name
    
        NSObject *objc = [[NSObject alloc] init];
    
        objc.name = @"石虎你是最棒的....";
    
        NSLog(@"objc.name = %@",objc.name);
    
    }
    

    2.动态添加方法

    开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。(例如:会员机制)

    • 添加无参数方法
    // 1.创建Person 对象
        Person *p = [[Person alloc] init];
     // 2.调用没有实现的eat方法   
        [p performSelector:@selector(eat)];
    // 3.在person.m文件中调用方法:
         // 作用:调用了一个未实现方法时一定会来到这里
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
       // 判断方法名是不是eat
        if (sel == NSSelectorFromString(@"eat")) {
           // 动态添加eat方法 
           /*
             第一个参数:给哪个类添加方法
             第二个参数:添加什么方法
              第三个参数IMP:方法实现,函数入口:函数名
              第四个参数:方法类型 
              v  没有返回值
              @ 对象 id
              :  方法
    */
            class_addMethod(self, @selector(eat), eat, "v@:");        
            return YES;
        }
       return [super resolveInstanceMethod:sel];
    }
    
    // 4.eat方法实现
          // self:方法调用者
          // _cmd:当前方法编号
        // 任何一个方法都能调用self,_cmd,其实任何一个方法都有这两个隐式参数
    void eat(id self, SEL _cmd)
    {
        NSLog(@"吃东西");
    }
    
    • 添加有参数方法
    // 2.调用没有实现的run方法   
       [p performSelector:@selector(run:) withObject:@10];
    // 3.在person.m文件中调用方法:
         // 作用:调用了一个未实现方法时一定会来到这里
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
       // 判断方法名是不是eat
        if (sel == NSSelectorFromString(@"run:")) {
           // 动态添加run方法 
           /*
             第一个参数:给哪个类添加方法
             第二个参数:添加什么方法
              第三个参数IMP:方法实现,函数入口:函数名
              第四个参数:方法类型 
              v  没有返回值
              @ 对象 id
              :  方法
    */
            class_addMethod(self, @selector(run:), run, "v@:@");        
            return YES;
        }
       return [super resolveInstanceMethod:sel];
    }
    
    // 4.run方法实现
          // self:方法调用者
          // _cmd:当前方法编号
        // 任何一个方法都能调用self,_cmd,其实任何一个方法都有这两个隐式参数
    void run(id self, SEL _cmd,  NSNumber *metre)
    {
        NSLog(@"跑了%@米",metre);
    }
    

    3.方法交换(Swizzle 黑魔法)

    平时我们app中用到的系统方法有很多,有时候我们需要对系统方法进行修改,已实现我们的需求和解决问题,我们不可能每个去改去处理.所以我们就要用到方法替换了.

    使用场景: array越界空等引起的崩溃, button重复点击, image空图片懒加载等很多功能.

    Method imageNameMethod = class_getClassMethod(self, @selector(imageNamed:));
    Method My_imageNameMethod = class_getClassMethod(self, @selector(My_imageNamed:));
    method_exchangeImplementations(imageNameMethod, My_imageNameMethod);
    
    // 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.
    
    + (instancetype)My_imageNamed:(NSString *)name
    {
        // 这里调用My_imageNamed,相当于调用imageNamed
        UIImage *image = [self My_imageNamed:name];
        
        if (image == nil) {
            NSLog(@"加载空的图片");
        }
        
        return image;
    }
    

    4.归档解档

    使用场景: 归档解档
    不用运行时的归档方法:(还好只有5个属性,如果20个,30个或者后台突然增加了属性,这么直接写死估计代码就不灵了)

    //  YYPerson.m
    
    #import "YYPerson.h"
    
    @implementation YYPerson
    
    // 当将一个自定义对象保存到文件的时候就会调用该方法
    // 在该方法中说明如何存储自定义对象的属性
    // 也就说在该方法中说清楚存储自定义对象的哪些属性
    - (void)encodeWithCoder:(NSCoder *)aCoder
    {
        NSLog(@"调用了encodeWithCoder:方法");
        [aCoder encodeObject:self.name forKey:@"name"];
        [aCoder encodeInteger:self.age forKey:@"age"];
        [aCoder encodeDouble:self.height forKey:@"height"];
    }
    
    // 当从文件中读取一个对象的时候就会调用该方法
    // 在该方法中说明如何读取保存在文件中的对象
    // 也就是说在该方法中说清楚怎么读取文件中的对象
    - (id)initWithCoder:(NSCoder *)aDecoder
    {
        NSLog(@"调用了initWithCoder:方法");
        //注意:在构造方法中需要先初始化父类的方法
        if (self=[super init]) {
            self.name=[aDecoder decodeObjectForKey:@"name"];
            self.age=[aDecoder decodeIntegerForKey:@"age"];
            self.height=[aDecoder decodeDoubleForKey:@"height"];
        }
        return self;
    }
    @end
    

    runtime 归档接档

    //
    //  Apply.m
    //  01-RuntimeSendMessage
    //
    //  Created by Mac on 2019/11/1.
    //  Copyright © 2019 Mac. All rights reserved.
    //
    
    #import "Apply.h"
    #import <objc/runtime.h>
    
    
    @implementation Apply
    // 归档的时候,系统会使用编码器把当前对象编码成二进制流
    - (void)encodeWithCoder:(NSCoder *)coder {
        unsigned int count = 0;
        // 获取所有实例变量
        Ivar *ivars = class_copyIvarList([self class], &count);
        // 遍历
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[I];
            const char *name = ivar_getName(ivar);
            NSString *key = [NSString stringWithUTF8String:name];
            // KVC
            id value = [self valueForKey:key];
            // 编码
            [coder encodeObject:value forKey:key];
        }
        
        // 因为是 C 语言的东西,不会自动释放,所以这里需要手动释放。
        free(ivars);
    }
    
    // 解档的时候,系统会把二进制流解码成对象
    - (instancetype)initWithCoder:(NSCoder *)coder {
        self = [super init];
        if (self) {
            unsigned int count = 0;
            // 获取所有实例变量
            Ivar *ivars = class_copyIvarList([self 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 = [coder decodeObjectOfClasses:[NSSet setWithObject:[self class]] forKey:key];
                // KVC
                [self setValue:value forKey:key];
            }
            
            free(ivars);
        }
        return self;
    }
    
    + (BOOL)supportsSecureCoding {
        return YES;
    }
    
    @end
    
    • 在使用的时候
    // 4.自动解归档
        Apply *apply = [Apply new];
        apply.name = @"张三";
        apply.age = @18;
        apply.nick = @"zhangsan";
        
        Apply *apply_2;
        NSString *fileName = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"archive.plist"];
        
        if (@available(iOS 11.0, *)) {
            NSData *data_1 = [NSKeyedArchiver archivedDataWithRootObject:apply requiringSecureCoding:YES error:nil];
            [data_1 writeToFile:fileName atomically:YES];
            
            NSData *data_2 = [[NSData alloc] initWithContentsOfFile:fileName];
            apply_2 = [NSKeyedUnarchiver unarchivedObjectOfClass:[Apply class] fromData:data_2 error:nil];
        } else {
            
            [NSKeyedArchiver archiveRootObject:apply toFile:fileName];
            
            apply_2 = [NSKeyedUnarchiver unarchiveObjectWithFile:fileName];
        }
        
        NSLog(@"name: %@, age: %@, nick: %@", apply_2.name, apply_2.age, apply_2.nick);
    

    查看原文链接

    5.字典转模型

    使用场景:字典转模型时,希望可以不用与字典中属性一一对应(案例:NSObject+JSONExtension.h)
    方法:可以使用runtime,遍历模型中有多少个属性,直接去字典中取出对应value,给模型赋值

    + (instancetype)modelWithDict:(NSDictionary *)dict
    {
        id objc = [[self alloc] init];
        
        int count = 0;
        
        // 成员变量数组 指向数组第0个元素
        Ivar *ivarList = class_copyIvarList(self, &count);
        
        // 遍历所有成员变量
        for (int i = 0; i < count; i++) {
            
            // 获取成员变量 user
            Ivar ivar = ivarList[i];
            // 获取成员变量名称
            NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // 获取成员变量类型
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            
            //  @"@\"User\"" -> @"User"
            type = [type stringByReplacingOccurrencesOfString:@"@\"" withString:@""];
            type = [type stringByReplacingOccurrencesOfString:@"\"" withString:@""];
            
            // 成员变量名称转换key
            NSString *key = [ivarName substringFromIndex:1];
            
            // 从字典中取出对应value dict[@"user"] -> 字典
            id value = dict[key];
            
            // 二级转换
            // 并且是自定义类型,才需要转换
            if ([value isKindOfClass:[NSDictionary class]] && ![type containsString:@"NS"]) { // 只有是字典才需要转换
                
                Class className = NSClassFromString(type);
                
                // 字典转模型
                value = [className modelWithDict:value];
            }
            
            // 给模型中属性赋值 key:user value:字典 -> 模型
            if (value) {
                [objc setValue:value forKey:key];
            }
            
        }
        
        return objc;
    }
    

    6.万能界面跳转方法

    使用场景: 消息接收后跳转

    利用runtime动态生成对象、属性、方法这特性,我们可以先跟服务端商量好,定义跳转规则,比如要跳转到A控制器,需要传属性idtype,那么服务端返回字典给我,里面有控制器名,两个属性名跟属性值,客户端就可以根据控制器名生成对象,再用kvc给对象赋值,这样就搞定了 ---O(∩_∩)O哈哈哈

    // 这个规则肯定事先跟服务端沟通好,跳转对应的界面需要对应的参数
    NSDictionary *userInfo = @{
                               @"class": @"HSFeedsViewController",
                               @"property": @{
                                            @"ID": @"123",
                                            @"type": @"12"
                                       }
                               };
    
    • 跳转界面
    - (void)push:(NSDictionary *)params
    {
        // 类名
        NSString *class =[NSString stringWithFormat:@"%@", params[@"class"]];
        const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
        // 从一个字串返回一个类
        Class newClass = objc_getClass(className);
        if (!newClass)
        {
            // 创建一个类
            Class superClass = [NSObject class];
            newClass = objc_allocateClassPair(superClass, className, 0);
            // 注册你创建的这个类
            objc_registerClassPair(newClass);
        }
        // 创建对象
        id instance = [[newClass alloc] init];
        // 对该对象赋值属性
        NSDictionary * propertys = params[@"property"];
        [propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
            // 检测这个对象是否存在该属性
            if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {
                // 利用kvc赋值
                [instance setValue:obj forKey:key];
            }
        }];
        // 获取导航控制器
        UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;
        UINavigationController *pushClassStance = (UINavigationController *)tabVC.viewControllers[tabVC.selectedIndex];
        // 跳转到对应的控制器
        [pushClassStance pushViewController:instance animated:YES];
    }
    
    • 检测对象是否存在该属性
    - (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName
    {
        unsigned int outCount, i;
        // 获取对象里的属性列表
        objc_property_t * properties = class_copyPropertyList([instance
                                                               class], &outCount);
        for (i = 0; i < outCount; i++) {
            objc_property_t property =properties[i];
            //  属性名转成字符串
            NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            // 判断该属性是否存在
            if ([propertyName isEqualToString:verifyPropertyName]) {
                free(properties);
                return YES;
            }
        }
        free(properties);
        return NO;
    }
    

    具体使用和代码: https://github.com/HHuiHao/Universal-Jump-ViewController

    相关文章

      网友评论

        本文标题:iOS runtime 详解和使用场景(最详细的使用干货)

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