当前位置:   article > 正文

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(引入 <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动态添加属性方法;

  1. @implementation NSObject (Property)
  2. - (void)setName:(NSString *)name
  3. {
  4. /*
  5. object:保存到哪个对象中
  6. key:用什么属性保存 属性名
  7. value:保存值
  8. policy:策略,strong,weak
  9. */
  10. objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  11. }
  12. - (NSString *)name
  13. {
  14. return objc_getAssociatedObject(self, "name");
  15. }

  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. self.view.backgroundColor = [UIColor orangeColor];
  4. //给系统NSObject类动态添加属性name
  5. NSObject *objc = [[NSObject alloc] init];
  6. objc.name = @"石虎你是最棒的....";
  7. NSLog(@"objc.name = %@",objc.name);
  8. }

2.动态添加方法

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

  • 添加无参数方法

  1. // 1.创建Person 对象
  2. Person *p = [[Person alloc] init];
  3. // 2.调用没有实现的eat方法
  4. [p performSelector:@selector(eat)];
  5. // 3.在person.m文件中调用方法:
  6. // 作用:调用了一个未实现方法时一定会来到这里
  7. + (BOOL)resolveInstanceMethod:(SEL)sel
  8. {
  9. // 判断方法名是不是eat
  10. if (sel == NSSelectorFromString(@"eat")) {
  11. // 动态添加eat方法
  12. /*
  13. 第一个参数:给哪个类添加方法
  14. 第二个参数:添加什么方法
  15. 第三个参数IMP:方法实现,函数入口:函数名
  16. 第四个参数:方法类型
  17. v 没有返回值
  18. @ 对象 id
  19. : 方法
  20. */
  21. class_addMethod(self, @selector(eat), eat, "v@:");
  22. return YES;
  23. }
  24. return [super resolveInstanceMethod:sel];
  25. }
  26. // 4.eat方法实现
  27. // self:方法调用者
  28. // _cmd:当前方法编号
  29. // 任何一个方法都能调用self,_cmd,其实任何一个方法都有这两个隐式参数
  30. void eat(id self, SEL _cmd)
  31. {
  32. NSLog(@"吃东西");
  33. }
  • 添加有参数方法

  1. // 2.调用没有实现的run方法
  2. [p performSelector:@selector(run:) withObject:@10];
  3. // 3.在person.m文件中调用方法:
  4. // 作用:调用了一个未实现方法时一定会来到这里
  5. + (BOOL)resolveInstanceMethod:(SEL)sel
  6. {
  7. // 判断方法名是不是eat
  8. if (sel == NSSelectorFromString(@"run:")) {
  9. // 动态添加run方法
  10. /*
  11. 第一个参数:给哪个类添加方法
  12. 第二个参数:添加什么方法
  13. 第三个参数IMP:方法实现,函数入口:函数名
  14. 第四个参数:方法类型
  15. v 没有返回值
  16. @ 对象 id
  17. : 方法
  18. */
  19. class_addMethod(self, @selector(run:), run, "v@:@");
  20. return YES;
  21. }
  22. return [super resolveInstanceMethod:sel];
  23. }
  24. // 4.run方法实现
  25. // self:方法调用者
  26. // _cmd:当前方法编号
  27. // 任何一个方法都能调用self,_cmd,其实任何一个方法都有这两个隐式参数
  28. void run(id self, SEL _cmd, NSNumber *metre)
  29. {
  30. NSLog(@"跑了%@米",metre);
  31. }

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

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

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

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

4.归档解档

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

  1. // YYPerson.m
  2. #import "YYPerson.h"
  3. @implementation YYPerson
  4. // 当将一个自定义对象保存到文件的时候就会调用该方法
  5. // 在该方法中说明如何存储自定义对象的属性
  6. // 也就说在该方法中说清楚存储自定义对象的哪些属性
  7. - (void)encodeWithCoder:(NSCoder *)aCoder
  8. {
  9. NSLog(@"调用了encodeWithCoder:方法");
  10. [aCoder encodeObject:self.name forKey:@"name"];
  11. [aCoder encodeInteger:self.age forKey:@"age"];
  12. [aCoder encodeDouble:self.height forKey:@"height"];
  13. }
  14. // 当从文件中读取一个对象的时候就会调用该方法
  15. // 在该方法中说明如何读取保存在文件中的对象
  16. // 也就是说在该方法中说清楚怎么读取文件中的对象
  17. - (id)initWithCoder:(NSCoder *)aDecoder
  18. {
  19. NSLog(@"调用了initWithCoder:方法");
  20. //注意:在构造方法中需要先初始化父类的方法
  21. if (self=[super init]) {
  22. self.name=[aDecoder decodeObjectForKey:@"name"];
  23. self.age=[aDecoder decodeIntegerForKey:@"age"];
  24. self.height=[aDecoder decodeDoubleForKey:@"height"];
  25. }
  26. return self;
  27. }
  28. @end

runtime 归档接档

  1. //
  2. // Apply.m
  3. // 01-RuntimeSendMessage
  4. //
  5. // Created by Mac on 2019/11/1.
  6. // Copyright © 2019 Mac. All rights reserved.
  7. //
  8. #import "Apply.h"
  9. #import <objc/runtime.h>
  10. @implementation Apply
  11. // 归档的时候,系统会使用编码器把当前对象编码成二进制流
  12. - (void)encodeWithCoder:(NSCoder *)coder {
  13. unsigned int count = 0;
  14. // 获取所有实例变量
  15. Ivar *ivars = class_copyIvarList([self class], &count);
  16. // 遍历
  17. for (int i = 0; i < count; i++) {
  18. Ivar ivar = ivars[I];
  19. const char *name = ivar_getName(ivar);
  20. NSString *key = [NSString stringWithUTF8String:name];
  21. // KVC
  22. id value = [self valueForKey:key];
  23. // 编码
  24. [coder encodeObject:value forKey:key];
  25. }
  26. // 因为是 C 语言的东西,不会自动释放,所以这里需要手动释放。
  27. free(ivars);
  28. }
  29. // 解档的时候,系统会把二进制流解码成对象
  30. - (instancetype)initWithCoder:(NSCoder *)coder {
  31. self = [super init];
  32. if (self) {
  33. unsigned int count = 0;
  34. // 获取所有实例变量
  35. Ivar *ivars = class_copyIvarList([self class], &count);
  36. // 遍历
  37. for (int i = 0; i < count; i++) {
  38. Ivar ivar = ivars[I];
  39. const char *name = ivar_getName(ivar);
  40. NSString *key = [NSString stringWithUTF8String:name];
  41. id value = [coder decodeObjectOfClasses:[NSSet setWithObject:[self class]] forKey:key];
  42. // KVC
  43. [self setValue:value forKey:key];
  44. }
  45. free(ivars);
  46. }
  47. return self;
  48. }
  49. + (BOOL)supportsSecureCoding {
  50. return YES;
  51. }
  52. @end
  • 在使用的时候

  1. // 4.自动解归档
  2. Apply *apply = [Apply new];
  3. apply.name = @"张三";
  4. apply.age = @18;
  5. apply.nick = @"zhangsan";
  6. Apply *apply_2;
  7. NSString *fileName = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"archive.plist"];
  8. if (@available(iOS 11.0, *)) {
  9. NSData *data_1 = [NSKeyedArchiver archivedDataWithRootObject:apply requiringSecureCoding:YES error:nil];
  10. [data_1 writeToFile:fileName atomically:YES];
  11. NSData *data_2 = [[NSData alloc] initWithContentsOfFile:fileName];
  12. apply_2 = [NSKeyedUnarchiver unarchivedObjectOfClass:[Apply class] fromData:data_2 error:nil];
  13. } else {
  14. [NSKeyedArchiver archiveRootObject:apply toFile:fileName];
  15. apply_2 = [NSKeyedUnarchiver unarchiveObjectWithFile:fileName];
  16. }
  17. NSLog(@"name: %@, age: %@, nick: %@", apply_2.name, apply_2.age, apply_2.nick);

查看原文链接

5.字典转模型

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

  1. + (instancetype)modelWithDict:(NSDictionary *)dict
  2. {
  3. id objc = [[self alloc] init];
  4. int count = 0;
  5. // 成员变量数组 指向数组第0个元素
  6. Ivar *ivarList = class_copyIvarList(self, &count);
  7. // 遍历所有成员变量
  8. for (int i = 0; i < count; i++) {
  9. // 获取成员变量 user
  10. Ivar ivar = ivarList[i];
  11. // 获取成员变量名称
  12. NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
  13. // 获取成员变量类型
  14. NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
  15. // @"@\"User\"" -> @"User"
  16. type = [type stringByReplacingOccurrencesOfString:@"@\"" withString:@""];
  17. type = [type stringByReplacingOccurrencesOfString:@"\"" withString:@""];
  18. // 成员变量名称转换key
  19. NSString *key = [ivarName substringFromIndex:1];
  20. // 从字典中取出对应value dict[@"user"] -> 字典
  21. id value = dict[key];
  22. // 二级转换
  23. // 并且是自定义类型,才需要转换
  24. if ([value isKindOfClass:[NSDictionary class]] && ![type containsString:@"NS"]) { // 只有是字典才需要转换
  25. Class className = NSClassFromString(type);
  26. // 字典转模型
  27. value = [className modelWithDict:value];
  28. }
  29. // 给模型中属性赋值 key:user value:字典 -> 模型
  30. if (value) {
  31. [objc setValue:value forKey:key];
  32. }
  33. }
  34. return objc;
  35. }

6.万能界面跳转方法

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

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

  1. // 这个规则肯定事先跟服务端沟通好,跳转对应的界面需要对应的参数
  2. NSDictionary *userInfo = @{
  3. @"class": @"HSFeedsViewController",
  4. @"property": @{
  5. @"ID": @"123",
  6. @"type": @"12"
  7. }
  8. };
  • 跳转界面

  1. - (void)push:(NSDictionary *)params
  2. {
  3. // 类名
  4. NSString *class =[NSString stringWithFormat:@"%@", params[@"class"]];
  5. const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
  6. // 从一个字串返回一个类
  7. Class newClass = objc_getClass(className);
  8. if (!newClass)
  9. {
  10. // 创建一个类
  11. Class superClass = [NSObject class];
  12. newClass = objc_allocateClassPair(superClass, className, 0);
  13. // 注册你创建的这个类
  14. objc_registerClassPair(newClass);
  15. }
  16. // 创建对象
  17. id instance = [[newClass alloc] init];
  18. // 对该对象赋值属性
  19. NSDictionary * propertys = params[@"property"];
  20. [propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
  21. // 检测这个对象是否存在该属性
  22. if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {
  23. // 利用kvc赋值
  24. [instance setValue:obj forKey:key];
  25. }
  26. }];
  27. // 获取导航控制器
  28. UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;
  29. UINavigationController *pushClassStance = (UINavigationController *)tabVC.viewControllers[tabVC.selectedIndex];
  30. // 跳转到对应的控制器
  31. [pushClassStance pushViewController:instance animated:YES];
  32. }
  • 检测对象是否存在该属性

  1. - (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName
  2. {
  3. unsigned int outCount, i;
  4. // 获取对象里的属性列表
  5. objc_property_t * properties = class_copyPropertyList([instance
  6. class], &outCount);
  7. for (i = 0; i < outCount; i++) {
  8. objc_property_t property =properties[i];
  9. // 属性名转成字符串
  10. NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
  11. // 判断该属性是否存在
  12. if ([propertyName isEqualToString:verifyPropertyName]) {
  13. free(properties);
  14. return YES;
  15. }
  16. }
  17. free(properties);
  18. return NO;
  19. }

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/428259
推荐阅读
相关标签
  

闽ICP备14008679号