一、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 系统自动在幕后辛勤劳作着。
二、Runtime源码初探
runtime 是 OC底层的一套C语言的API(引入
或
),编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m
可以看到编译后的xxx.cpp
(C++文件)。
比如我们创建了一个对象 [[NSObject alloc]init]
,最终被转换为几万行代码,截取最关键的一句可以看到底层是通过runtime创建的对象
删除掉一些强制转换语句,可以看到调用方法本质就是发消息,
[[NSObject alloc]init]
语句发了两次消息,第一次发了alloc
消息,第二次发送init
消息。利用这个功能我们可以探究底层,比如block的实现原理。
三、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 < 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控制器,需要传属性id
、type
,那么服务端返回字典给我,里面有控制器名,两个属性名跟属性值,客户端就可以根据控制器名生成对象,再用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 配置图文教程及坑总结