网络知识 娱乐 iOS runtime 详解和使用场景(最详细的使用教程)

iOS runtime 详解和使用场景(最详细的使用教程)

 一、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(引入  或),编译器最终都会将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 


@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  @"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

  • 作者开发经验总结的文章推荐,持续更新学习心得笔记
    Runtime 10种用法(没有比这更全的了)
    成为iOS顶尖高手,你必须来这里(这里有最好的开源项目和文章)
    iOS逆向Reveal查看任意app 的界面
    JSPatch (实时修复App Store bug)学习(一)
    iOS 高级工程师是怎么进阶的(补充版20+点)
    扩大按钮(UIButton)点击范围(随意方向扩展哦)
    最简单的免证书真机调试(原创)
    通过分析微信app,学学如何使用@2x,@3x图片
    TableView之MVVM与MVC之对比
    使用MVVM减少控制器代码实战(减少56%)
    ReactiveCocoa添加cocoapods 配置图文教程及