iOS Runtime笔记
![](https://img.haomeiwen.com/i846340/e84930972290032a.jpeg)
相信大家对上图应该不陌生,图中说明了OC中对象的本质以及对象、类与元类的关系,这个也是OC的基础,属于runtime的内容,那么什么是runtime呢?它有什么作用呢?现用的那些东西可以涉及到runtime呢?下面一一讲解。
1.runtime是什么?
说的简单点,runtime就是一套C语言的函数和结构体,OC的代码在编译后就会转化为对应的C代码(个人理解,可能存在误差啊!😂),OC被称之为运行时语言就是依赖于这个运行时系统,举个最简单的例子:
[p eat];
这句代码很常见吧!p对象执行eat方法,那么这句代码在编译后会被转化为什么呢?
objc_msgSend(p, sel_registerName("eat"));(精简版)
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("eat"));(原版)
其实OC的方法执行本质上是发送消息(即消息发送机制)
那么常见的runtime函数还有哪些呢?
- 消息发送(
在<objc/message.h>中,直接使用#import导入即可
)
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...) 给对象发送消息
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...) 给对象父类发送消息(我觉得理解成给对象发送父类的消息可能好点)
- Class(
在<objc/runtime.h>中
)
说到Class就不得不提文章开头的图片了,用于解释对象、类和元类的经典图,继承于NSObject的类对象
都有一个isa
指针成员变量指向它所属的类,而类
本身呢!其实也是一个Class结构体指针变量,结构体中也有一个名为isa
的Class指针变量,这个isa
就指向了该类所属的元类
,我们常用的类方法就属于元类的。
NSObject的isa成员变量
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
Class是一个指向结构体objc_class的指针:
typedef struct objc_class *Class;
objc_class结构体
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE; 父类
const char * _Nonnull name OBJC2_UNAVAILABLE;类名
long version OBJC2_UNAVAILABLE;版本
long info OBJC2_UNAVAILABLE;详情
long instance_size OBJC2_UNAVAILABLE;类的实例对象的大小
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;类的成员变量列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;类的实例方法列表 是指向 ·objc_method_list 指针的指针,也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是 Category 实现的原理)
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;Runtime 系统会把被调用的方法存到 cache 中下次查找的时候效率更高
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;类的协议列表
#endif
} OBJC2_UNAVAILABLE;
常见函数,更多函数前往<objc/runtime.h>
查看
OBJC_EXPORT Class _Nullable object_getClass(id _Nullable obj) 获取对象的Class
OBJC_EXPORT Class _Nullable object_setClass(id _Nullable obj, Class _Nonnull cls) 设置对象所属的Class
OBJC_EXPORT Class _Nullable objc_getClass(const char * _Nonnull name) 根据C字符获取一个Class
OBJC_EXPORT Class _Nullable objc_getMetaClass(const char * _Nonnull name) 获取C字符对应的Class的元类
BOOL class_respondsToSelector(Class _Nullable cls, SEL _Nonnull sel) 类的对象是否响应方法(估计就是方法- (BOOL)respondsToSelector:(SEL)aSelector的底层实现)
OBJC_EXPORT Class _Nullable objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes) 创建一个新类
OBJC_EXPORT void objc_registerClassPair(Class _Nonnull cls) 注册一个新类(和上面的函数一起使用的)
OBJC_EXPORT void objc_disposeClassPair(Class _Nonnull cls) 注销一个类(慎用!!!)
- Method(
在<objc/runtime.h>和<objc/objc.h>中
)
直接看Method的结构体
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
显然方法中主要就两个东西SEL(方法选择器)
和IMP
(method_types用于用于表示方法的返回值类型和参数类型),其实可以这么理解类中有一个方法分发表存放着SEL
和IMP
的映射关系,SEL
就是一个C字符串,通过它找到的IMP
是一个函数指针,指向方法的具体实现。
typedef struct objc_selector *SEL
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
常见的函数,更多函数前往<objc/runtime.h>和<objc/objc.h>
查看
OBJC_EXPORT const char * _Nonnull sel_getName(SEL _Nonnull sel) 获取SEL对应的C字符串
OBJC_EXPORT SEL _Nonnull sel_registerName(const char * _Nonnull str)根据C字符串注册一个SEL
OBJC_EXPORT void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 替换两个方法的IMP
OBJC_EXPORT IMP _Nonnull method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) 设置方法的IMP
OBJC_EXPORT SEL _Nonnull method_getName(Method _Nonnull m) 获取方法的SEL
OBJC_EXPORT IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 替换一个类的方法的IMP
OBJC_EXPORT BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 为类添加一个方法
- Ivar(成员变量)和Property(属性)
这两个东西很多人弄混了,其实这是两个不一样的东西,简单理解的话就是属性=成员变量+成员变量的set方法+成员变量的get方法
,我之前针对这两个东西写过一篇文章《iOS-属性与实例变量(成员变量)》。
先看看Ivar指向的结构体,结构体中的变量很简单
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
常用的函数,更多函数前往<objc/runtime.h>
查看
OBJC_EXPORT id _Nullable object_getIvar(id _Nullable obj, Ivar _Nonnull ivar) 获取成员变量的值
OBJC_EXPORT void object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value) 设置成员变量的值
OBJC_EXPORT Ivar _Nonnull * _Nullable class_copyIvarList(Class _Nullable cls, unsigned int * _Nullable outCount) 拷贝类的所有成员变量
OBJC_EXPORT const char * _Nullable ivar_getName(Ivar _Nonnull v) 获取成员变量的名称
OBJC_EXPORT objc_property_t _Nonnull * _Nullable class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount) 获取属性列表
OBJC_EXPORT objc_property_t _Nullable class_getProperty(Class _Nullable cls, const char * _Nonnull name) 根据C字符串获取属性
OBJC_EXPORT const char * _Nonnull property_getName(objc_property_t _Nonnull property) 获取属性名
- Category(类别)
类别好像没有多少函数(<objc/runtime.h>
里面基本没有),主要是类别的结构体
typedef struct objc_category *Category
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
}
- Protocol(协议)
协议好像没有对应的结构体,<objc/runtime.h>
是这样声明的,相关函数用的少,后期再补上(🤔🤔🤔)。
@class Protocol;
2.runtime有什么作用?
runtime堪称黑魔法,他可以新建类,为类添加属性、成员变量和方法,甚至更改一个方法的实现,反正很多一开始以为不可能的事都可以通过runtime实现,下面说说具体的使用场景:
-
类别添加属性
一看到这个大家会想到面试中经常问的问题类别可以添加属性吗?类别可以添加成员变量吗?我的理解是类别可以添加属性但是不可以添加成员变量,至于原因呢?我觉得是结构体的objc_class中的成员列表变量ivars是一个指针,再把OC代码编译成C代码后,类在内存上的布局就改变不了。
那么为什么可以添加属性呢?首先类的方法是可以添加的,因为objc_class中的实例方法列表变量methodLists是一个指向objc_method_list指针的指针,指针在内存中的布局是一样的,所以可以更改,然后再使用runtime
的关联对象
来实现为类别添加属性,详细的可参考《iOS runtime 关联对象》。 -
方法交换
相信大家在开发过程中经常遇到这样的问题:一开始使用系统方法很好很方便,可以慢慢迭代到后期发现系统方法不能满足要求,需要做额外操作,这时候咋办呢?最简单的就是集成然后重写方法再替换,但是这样需要改掉之前所有的,麻烦!直接快捷键查找再替换,Low!!而且不保险改到一半出问题了就尴尬了!再说以后再改还要再替换,工程越大替换成本越大。那可以添加分类,在分类中重写方法吗?不推荐!!原因简单的说它的初衷并不是让你去改变一个类。那咋办?这时候可以使用runtime
,直接上代码:
#import "NSURL+testCategory.h"
#import <objc/runtime.h>
@implementation NSURL (testCategory)
+ (void)load
{
Method oldMethod = class_getClassMethod(self, sel_registerName("URLWithString:"));
Method newMethod = class_getClassMethod(self, sel_registerName("customURLWithString:"));
method_exchangeImplementations(oldMethod, newMethod);
}
/**
* @author liyong
*
* 网址字符串转为可加载的url(主要针对有汉字的网址字符串)
*
* @param URLString 网址字符串
*
* @return
*/
+ (nullable instancetype)customURLWithString:(nonnull NSString *)URLString
{
if ([URLString length] > 0)
{
NSString *encodingURLString = [URLString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
//注意!这地方不可以使用URLWithString会死循环,因为URLWithString的实现已经变成customURLWithString,而customURLWithString已经变成URLWithString
NSURL *url = [NSURL customURLWithString:encodingURLString];
if (!url)
{
NSLog(@"nil空了!!!");
return nil;
}
return url;
}
NSLog(@"nil空了!!!");
return nil;
}
@end
代码中使用customURLWithString替换了系统的URLWithString,注意点代码中有。
这里额外说一下load
方法,我的理解比较简单,就是是app的可执行文件从存储空间加载到内存后,最先被CPU读取的代码或者说是指令,用于给开发者做预处理任务,而且这个方法在类别重写后不影响原类的执行。后来查资料发现load方法貌似比我想的要复杂的多!(😂说的简单点好理解)
但是这种方法交换也存在一种问题就是如果过度滥用方法交换的话,会导致动态能力过强,代码的可读性和维护性会下降,所以大家要注意点,然后就是多添加注释!不然会坑自己!
- 模型的序列化与反序列化/JSON数据转成模型
模型缓存在本地的业务相信大家都处理过,一般使用归档的方式将模型或者装有模型的容器类缓存到本地,而且模型的类需要遵循NSCoding
协议,实现两个协议方法。
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
常见的实现方法如下:
@interface LYDog : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *color;
@end
@implementation LYDog
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeObject:self.color forKey:@"color"];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init])
{
self.name = [aDecoder decodeObjectForKey:@"name"];
self.color = [aDecoder decodeObjectForKey:@"color"];
}
return self;
}
@end
很正常的写法,但是当这个模型类的属性变多的时候呢?复制粘贴一个个修改?确实可以这么写,但是可以利用runtime+KVO实现另外一种方式:
@interface LYDog : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *color;
@end
@implementation LYDog
- (void)encodeWithCoder:(NSCoder *)aCoder
{
unsigned int propertyCount = 0;
objc_property_t *propertyList = class_copyPropertyList([self class], &propertyCount);
for (int index = 0; index < propertyCount; index++)
{
objc_property_t property = propertyList[index];
const char * propertyNameChar = property_getName(property);
NSString *propertyName = [NSString stringWithUTF8String:propertyNameChar];
[aCoder encodeObject:[self valueForKey:propertyName] forKey:propertyName];
}
//必须free掉
free(propertyList);
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init])
{
unsigned int propertyCount = 0;
objc_property_t *propertyList = class_copyPropertyList([self class], &propertyCount);
for (int index = 0; index < propertyCount; index++)
{
objc_property_t property = propertyList[index];
const char *propertyNameChar = property_getName(property);
NSString *propertyName = [NSString stringWithUTF8String:propertyNameChar];
[self setValue:[aDecoder decodeObjectForKey:propertyName] forKey:propertyName];
}
//必须free掉
free(propertyList);
}
return self;
}
@end
使用runtime获取类的属性列表循环列表根据属性名利用KVO为属性赋值或者序列化,代码固定,后期属性变化了也基本不需要更改代码。其实这个思想也可以用于把JSON字符串转模型,第三方类库JSONModel
中好像就用到了,下次细细研读一下JSONModel
的源码。
- 动态方法解析+消息转发/消息重定向
对一个对象发送了未实现的消息(即对象使用了一个未实现的方法)时,后果很简单,导致崩溃,而且控制台还会打印日志:
unrecognized selector sent to instance 0x**********
那么我们可不可以在程序闪退之前做一些什么呢?好点的能不能阻止闪退呢?利用runtime的动态消息解析就可以做到。
首先我们要知道向对象发送消息后的流程:
- 对象是否为nil对象,因为向nil对象发消息是不会crash的,而是被忽略。
2.根据SEL查找方法的IMP,首先从cache中查找,找到了就可以利用IMP找到对应的实现函数并执行。
3.若cache中没找到就在类的方法分发表中使用SEL查找IMP,找不到就去父类的方法分发表找,直到找到NSObject。
如果上面三步之后还找不到IMP的话,就会进入消息转发,转发流程大致如下:
![](https://img.haomeiwen.com/i846340/0c5d4e7896c3dde7.png)
图中涉及到如下方法:
1.下面两个方法用于判断是否需要利用class_addMethod函数向对象动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel 针对对象方法
+ (BOOL)resolveClassMethod:(SEL)sel 针对类方法
2.若上面的方法返回NO就会进入下面的方法,该方法是runtime告诉我们是否需要将消息重定向给另外一个对象以避免crash
- (id)forwardingTargetForSelector:(SEL)aSelector
3.若上面的方法返回nil,就会进入下面的方法,用于返回方法编码(编码包含方法的返回值类型和参数类型等),若返回nil表示不处理,否则进入下一步
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
4.下面的方法和1中可操作的内容差不多,添加方法或者修改方法的实现,但是这个方法弊端是消耗内存较大,要想达到相同的目的建议在1中操作。
- (void)forwardInvocation:(NSInvocation *)anInvocation
以上就是几个较大的runtime的使用场景,那么系统中又利用runtime做了哪些功能呢?
3.利用runtime实现的系统功能的解析
工作过程中有没有想过系统的哪些功能是使用runtime实现的呢?苹果开发出这么厉害的动态系统不可能不用吧?下面我们来分析几个
- KVO
KVO一部分实现原理就用到了runtime,当使用下面的方法为对象添加观察者,那么这个方法里面做了什么操作呢?方法中其实利用runtime为对象创建了一个子类,并且为子类重写了被观察属性的set方法。
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
光说不行,我们可以测试一下,代码很简单为p对象的firstName属性添加观察者:
self.p = [[LYPersonModel alloc] init];
NSLog(@"修改前类:%@", NSStringFromClass(object_getClass(self.p)));
[self.p addObserver:self forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"修改后类:%@", NSStringFromClass(object_getClass(self.p)));
结果
![](https://img.haomeiwen.com/i846340/6bb7908d23a6cbb7.jpeg)
![](https://img.haomeiwen.com/i846340/d01d81938f72b1b4.jpeg)
注意!差看不到p的isa指针的话可以先退出Xcode,再进一次,这可能是Xcode的bug
知道这一部分原理了,那么自己自定义一个简单的KVO应该不是难事了吧?大家可以自己试试,里面还有一些细节,下次补上demo