关于Runtime的用法和详解,一直想研究了解一下,但是一直没有时间进行彻底的学习(说白了就是懒,也就是现在人们普遍存在的拖延症
),最近几天公司项目不是很紧,于是抽出几天的时间来学习了一下Runtime。
废话不多说,先上源码和官方API:
在介绍Runtime之前,首先简单的列一下简单的目录,以方便了解本文的主要内容。
- Runtime的介绍
- Runtime中的数据结构
- Runtime中的消息机制
- 与Runtime的交互
- Runtime的应用
Runtime介绍
对于Runtime,Apple官方给出的解释是:
The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps.
我们都知道 Objective-C是一门动态语言,这意味着它不仅需要编译器,它还需要运行时系统来进行代码编译。而Runtime就执行了这一运行时系统代码编译工作。
Runtime直译就是运行时,是Apple用C和汇编语言编写的一套C语言的API,它正是 Objective-C这门动态语言的核心。我们知道计算机唯一能识别的语言是机器语言,高级编程语言不能被直接识别,需要先编译为汇编语言,再由汇编语言编译为机器语言才能被计算机识别。而 Objective-C语言不能被直接编译为汇编语言,它必须先编译为C语言,然后再编译为汇编语言。而Runtime正是编译器将我们写的 Objective-C代码编译为C语言时用到的核心库。
Apple和GNU各自维护一个开源的runtime版本,你可以到这里下载Apple维护的开源代码,而且这两个版本之间都在努力保持一致。
Runtime中的数据结构
研究Runtime的数据结构我们就要查看它的源码,从Runtime的源码中我们可以清晰地看到它的所有方法的实现和所有的结构体定义。
1.id的定义
在 objc.h 中我们可以找到id
的定义,代码如下:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
从上面的代码中我们可以看到,id
是一个指向结构体objc_object
的指针,而结构体objc_object
中只声明了一个Class
类型的指针,正因为这样的定义,我们的id
可以表示为任意类型。
2.Class的定义
在 objc.h 中我们也可以找到Class
的定义,代码如下:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
他是一个指向结构体objc_class
的指针。而结构体objc_class
的定义我们需要到 runtime.h 中去找,代码如下:
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;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
下面我们解释一下里面的参数:
isa指针:
在Runtime中Objc
类本身也是一个对象,Runtime
把类对象所属类型叫做元类(Meta Class)。元类(Meta Class)用于描述类对象本身所具有的特征,最常见的类方法就被定义与此。所以objc_class
中的isa指针指向的是元类。而元类的isa
指针则直接指向的是根元类,并不是父类的元类。每个类仅有一个对象,而每个类对象仅有一个与之相关的元类。在objc_object
结构体中也同样有一个isa指针,它指向的是类对象(即objc_class
)。
super_class指针:
super_class
指针指向的是object_class
类所继承的父类对象,如果当前类是最顶层的类(如:NSObject),则super_class
指针为nil。
name:
name指得就是当前类的类名。
version
version指的就是当前类的版本信息,默认为0
info
info:是类信息,提供运行期是用的一些标识
instance_size
instance_size指的是类的实例变量的大小
ivars
ivars用于存放所有的成员变量和属性信息
methodLists
methodLists用于存放对象的所有成员方法
cache
cache方法的缓存,为了优化性能和缩短方法的查找时间,objc_class
的cache结构体中存储的是每一次类或者实例对象调用的方法名。这样当类或者实例对象调用方法时,会优先在cache中寻找相应的方法,如果找不到再去methodLists
中去遍历查。
protocols
protocols用于存放对象的所有协议
上面说到指针的指向,下面我们通过一张图来解释一下:
通过上图我们可以看出,子类实例对象的isa
指针指向的是子类对象,子类对象的isa
指针指向的是子类元类,子类对象的super_class
指针指向的是父类对象,父类对象的isa
指针指向的是根类对象,根类对象的isa
指针指向nil
。子类元类的isa
指针则直接指向的根元类,根元类的isa指针则指向自己。
3.Method
在runtime.h
中我们找到了Method的定义
/// An opaque type that represents a method in a class definition.
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;
} OBJC2_UNAVAILABLE;
在objc_class
的methodLists
里面的元素就是Method。
method_name:方法名
method_types: 方法类型
method_imp:方法的实现
在这个结构体中听我们可以看到SEL(method_name)与IMP(method_name)形成了一个映射,通过SEL,我们可以很方便的找到方法的实现IMP。
4.SEL
在objc.h
中我们找到了的SEL的定义
///A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.
@property SEL selector;
/// An opaque type that represents a method selector.表示方法选择器的不透明类型。
typedef struct objc_selector *SEL;
SEL是一个指向objc_selector
结构体的指针。它是selector
在 Objective-C中标识类型(Swift中是Selector类)。SEL是方法选择器,用于表示运行时方法的名字。
selector
是SEL的一个实例,其实它就是个映射到方法的C字符串,我们可以用 Objective-C的编译器命令@selector()
或者Runtime系统的sel_registerName
函数来获取一个SEL类型的方法选择器。
我们在Runtime中并没有找到objc_selector
的定义,但是selector既然是一个字符串,我觉得应该是类似className+methodName
的组合,所以其命名规则大概是:
- 同一个类,selector不能重复
- 不同的类,selector可以重复
这也就可以解释为什么在 Objective-C中没有函数重载,因为它只记录了方法名,并没有记录参数,所以没法区分不同参数类型的方法。
5.IMP
在objc.h
中我们找到了的IMP的定义
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
由于我们现在用的都是 Objective-C2.0版本,所以else我们可以自动忽略。
IMP函数指针指向了方法实现的首地址,当 Objective-C发起消息时,最终执行的方法有IMP决定。
Runtime中的消息机制
在上面一章中我们介绍了Runtime的数据结构,看完以后我们可以看一下消息机制是怎样来调用的。下面我们来说一下Runtime中的消息机制的工作原理。
消息机制包括消息的发送和消息的转发,这两个我们下面分开讲。
消息的发送
当一个对象调用一个方法时[obj mothod]
,其实Runtime在底层就执行了一次消息发送,它的执行流程是这样的:
1.通过对象obj
的isa
指针找到对象obj
的Class
类;
2.在Class
的objc_cache
中找method
方法,找到后就去执行method
的IMP实现;找不到就执行下面第3步
3.在Class
中的method_list
中找method
方法,找到后就执行method
的实现,并且把method
的method_name
作为key
,method
的IMP作为value
保存到objc_cache
中,这样当再次执行method
方法是直接去objc_cache
寻找就行,节省了查找的时间(上一章介绍cache的时候说过);如果method_list
中也找不到该方法,那么就去obj
的superClass
中的method_list
中去找,以此类推,直到找到为止。
4.如果最终没有找到,Runtime提供了三种方法来处理:动态方法解析、消息接受者重定向、消息重定向,这三种方法后面讲。
以上就是Runtime的消息发送,由于Runtime是在编译时才调用方法的实现,所以如果我们只在.h声明了方法而没有在.m中实现,在编译时期是不会报错的,只有在运行时调用方法的实现时才会报错。
消息的转发
上文中说到当消息发送失败后Runtime提供了三种处理方法:动态方法解析、消息接受者重定向、消息重定向,下面我们来看一下这三个方法:
动态方法解析
动态解析,当没有找到方法时,Runtime为我们提供了一次动态添加方法实现的机会,让你去提供一个函数的实现。如果你添加了方法,并返回YES
,那么Runtime就会重新启动一次消息发送过程。
具体实现方法如下:
//添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
//添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//Runtime方法:
/**
运行时方法:向指定类中添加特定方法实现的操作
@param cls 被添加方法的类
@param name selector方法名
@param imp 指向实现方法的函数指针
@param types imp函数实现的返回值与参数类型
@return 添加方法是否成功
*/
BOOL class_addMethod(Class _Nullable cls,
SEL _Nonnull name,
IMP _Nonnull imp,
const char * _Nullable types)
下面举例来说名动态解析,看一下Runtime是如何调用的。
我们新建一个Animal的类,在它的.h
文件中声明方法,但并不实现,当我们通过Animal对象调用eat方法的时候我们通过Runtime的动态方法解析为他添加一个新的方法实现,具体代码如下:
///Animal.h文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Animal : NSObject
- (void)eat;
@end
NS_ASSUME_NONNULL_END
///Animal.m文件
#import "Animal.h"
#import <objc/runtime.h>
@implementation Animal
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"消息转发了,执行了resolveInstanceMethod方法");
if (sel == @selector(eat)) {
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(newEat)), "V@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
- (void)newEat {
NSLog(@"添加了新的吃饭的方法");
}
@end
下面我们创建一个Animal的对象来调用 - (void)eat;
方法
Animal *animal = [[Animal alloc] init];
[animal eat];
执行上述代码后,程序并没有Crash,我们可以看一下控制台的输出:
通过控制台的输出我们可以看到,程序执行了
+ (BOOL)resolveInstanceMethod:(SEL)sel
方法,在+ (BOOL)resolveInstanceMethod:(SEL)sel
方法中我们重新添加了新的实现,程序也执行了我们添加的新实现。注:
1.动态方法解析成功的前提是我们必须存在可以处理消息的方法:
newEat
,如果没有的话,动态解析依然失败。2.我们注意到上面
class_addMethod
方法的一个参数"v@:"
,这里的v是返回值类型为void
的方法,没有参数传入。详情请转到这
消息接收者重复定向
如果在消息动态解析时没有找到你传给它的实现(或者你根本没有实现+ (BOOL)resolveInstanceMethod:(SEL)sel
方法),消息发送机制会检查你是否实现了- (id)forwardingTargetForSelector:(SEL)aSelector
方法,如果你实现了这个方法并返回了一个重定向对象,那么就会把消息转发给你给它的对象,继而保证程序的继续执行。
我们创建一个AnimalForwarding
类,进行测试:
///AnimalForwarding.h文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AnimalForwarding : NSObject
@end
NS_ASSUME_NONNULL_END
///AnimalForwarding.m文件
#import "AnimalForwarding.h"
@implementation AnimalForwarding
- (void)eat {
NSLog(@"转发给了AnimalForwarding对象,并执行了eat方法");
}
@end
我们把Animal.m中的newEat
方法注释掉,执行测试。这是程序没有Crash,而是正常执行,我们可以看一下控制台输出:
通过控制台输出我们可以看到,我们的程序执行了
AnimalForwarding
类中的eat
方法,这就说明了我们通过- (id)forwardingTargetForSelector:(SEL)aSelector
方法,将Anima
的方法转发给了AnimalForwarding
。在控制台输出中我们可以看到程序执行了两次
+ (BOOL)resolveInstanceMethod:(SEL)sel
方法,当我们使用Animal对象调用eat
方法时,Runtime没有找到实现,执行了动态解析方法,这时候我们传给他一个方法名,当Runtime拿着我们给他的方法名继续执行时发现又没有这个实现,因此有调用了一次动态解析,此时我们对newEat
没有做处理,所以程序直接执行了- (id)forwardingTargetForSelector:(SEL)aSelector
进行消息接收者重定向,此时,我们拿到的aSelector
依然是eat
,而不是newEat
。
消息重定向
如果上述两种方法都没有生效,那么这个对象会因为找不到相应的方法实现而无法响应消息,此时Runtime唯一能做的就是启用完整的消息转发机制。首先Runtime会发送- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
消息获得函数的参数和返回值类型,如果- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
返回为nil,Runtime则会发出doesNotRecognizeSelector
消息,控制台输出unrecognized selector sent to instance
程序直接Crash掉。如果返回了一个方法签名,Runtime就会创建一个 NSInvocation
对象并发送给- (void)forwardInvocation:(NSInvocation *)anInvocation
方法通知该对象,给予此次消息发送的最后一次寻找IMP的机会。
具体代码如下:
///Animal.m
#import "Animal.h"
#import <objc/runtime.h>
#import "ViewController.h"
#import "AnimalForwarding.h"
@implementation Animal
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"消息转发了,执行了resolveInstanceMethod方法");
if (sel == @selector(eat)) {
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(newEat)), "V@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"执行了forwardingTargetForSelector方法进行消息接收者重定向");
return nil;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"执行了forwardInvocation方法,进行最后一次IMP寻找");
SEL sel = anInvocation.selector;
AnimalForwarding *aForwarding = [AnimalForwarding new];
if ([aForwarding respondsToSelector:sel]) {
[anInvocation invokeWithTarget:aForwarding];
} else {
[self doesNotRecognizeSelector:sel];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"执行了methodSignatureForSelector方法,返回方法签名");
if (aSelector == @selector(eat)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
@end
下面我们看一下控制台的输出:
通过控制台输出我们可以看出,Runtime先执行
+ (BOOL)resolveInstanceMethod:(SEL)sel
,然后执行- (id)forwardingTargetForSelector:(SEL)aSelector
,在两个方法执行都无效后执行了- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
获取方法签名,最后执行了- (void)forwardInvocation:(NSInvocation *)anInvocation
,通过- (void)forwardInvocation:(NSInvocation *)anInvocation
方法找到了AnimalForwarding
类中的eat
方法执行了消息转发,如果AnimalForwarding
类中没有eat
方法的实现,那么程序就会Crash掉。以上就是Runtime的三次转发流程。虽然理论上我们可以重载
- (void)doesNotRecognizeSelector:(SEL)aSelector
方法来保证不抛出异常避免程序Crash掉,但是Apple文档着重提出“一定不能让这个函数就这么结束掉(不调用super),必须抛出异常”。
Runtime的交互
Objective-C程序有三种途径和Runtime进行交互:通过 Objective-C源码、通过Foundation框架中NSObject类的方法、直接调用运行时系统函数。
通过 Objective-C源码
大部分情况下,运行时系统在后台自动运行,您只需编写和编译 Objective-C 源代码。
当您编译Objective-C类和方法时,编译器为实现语言动态特性将自动创建一些数据结构和函数。这些数据 结构包含类定义和协议类定义中的信息,如在Objective-C 2.0 程序设计语言中定义类和协议类一节所讨论 的类的对象和协议类的对象,方法选标,实例变量模板,以及其它来自于源代码的信息。因为这一部分是编译器自己在后台运行的,所以这个过程对我们来说是无感的。
通过Foundation框架中NSObject类的方法
Cocoa程序中绝大部分类都是NSObject类的子类,所以大部分都继承了NSObject类的方法,因而继承 了NSObject的行为。(NSProxy类是个例外)然而,某些情况下, NSObject类仅仅定义了完成某件事情的模板,而没有提供所有需要的代码。
某些 NSObject 的方法只是简单地从运行时系统中获得信息,从而允许对象进行一定程度的自我检查。 例如,class 返回对象的类;isKindOfClass:和 isMemberOfClass:则检查对象是否在指定的 类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol: 检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。
直接调用运行时系统函数
Runtime系统是一个有公开接口的动态库,由一些数据结构和函数的集合组成,这些数据结构和函数的声明 头文件在/usr/include/objc
中。这些函数支持用纯C的函数来实现和Objective-C同样的功能。代码示例:
//相当于:Animal *animal = [Animal alloc];
Animal *animal = ((id (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Animal"), sel_registerName("alloc"));
//相当于:Animal *animal = [Animal init];
((id (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("init"));
[animal eat];
Runtime的应用
- 分类增加属性
- 获取类属性方法列表
- 方法添加和替换
- 修改私有属性
- 实现NSCoding的自动归档和自动解档
- 实现字典和模型的自动转换
Runtime功能很强大,根据本文讲解做的Demo在这,有兴趣的可以下载看一下。
分类增加属性
我们知道分类是不能添加属性和变量的,Runtime的关联对象特性可以帮助我们在运行阶段的将任意属性和变量关联到一个对象上。下面是相关的方法:
/**
//给对象设置关联属性
@param object 需要设置关联属性的对象,即给哪个对象关联属性
@param key 关联属性对应的key,可通过key获取这个属性,
@param value 给关联属性设置的值
@param policy 关联属性的存储策略(对应Property属性中的assign,copy,retain等)
*/
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
/**
//通过key获取关联的属性
@param object 从哪个对象中获取关联属性
@param key 关联属性对应的key
@return 返回关联属性的值
*/
id objc_getAssociatedObject(id object, const void *key)
/**
//移除对象所关联的属性
@param object 移除某个对象的所有关联属性
*/
void objc_removeAssociatedObjects(id object)
内存管理的策略
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
///@property(assign)
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
///@property(strong, nonatomic)
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
///@property(copy, nonatomic)
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
///@property(strong, atomic)
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
///@property(copy, atomic)
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
注意:在使用上述方法时,key与关联属性一一对应,我们必须确保其全局唯一性,一般我们使用@selector(属性名)作为key。
下面我们来演示一个实例:
///Cat+CatCategory.h
#import "Cat.h"
@interface Cat (CatCategory)
@property (nonatomic, copy) NSString *color;
- (void)clearAssociateObject;
@end
///Cat+CatCategory.m
#import "Cat+CatCategory.h"
#import <objc/runtime.h>
@implementation Cat (CatCategory)
- (void)setColor:(NSString *)color {
objc_setAssociatedObject(self, @selector(color), color, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)color {
return objc_getAssociatedObject(self, @selector(color));
}
///清除关联属性
- (void)clearAssociateObject {
objc_removeAssociatedObjects(self);
}
@end
//给分类添加属性
Cat *cat = [[Cat alloc] init];
cat.color = @"白色";
NSLog(@"cat的颜色是%@",cat.color);
[cat clearAssociateObject];
NSLog(@"cat的颜色是%@",cat.color);
///打印
cat的颜色是白色
cat的颜色是(null)
获取类属性方法列表
获取类的实例方法
//获取类的实例方法
unsigned int count = 0;
Method *allMothods = class_copyMethodList([Cat class], &count);
for (int i = 0; i < count; i++) {
//Method,为runtime声明的一个宏,表示对一个方法的描述
Method md = allMothods[i];
//获取SEL:SEL类型,即获取方法选择器@selector()
SEL sel = method_getName(md);
//得到sel的方法名:以字符串格式获取sel的name,也即@selector()中的方法名称
const char *methodName = sel_getName(sel);
NSLog(@"(Method:%s)",methodName);
}
free(allMothods);
获取类的类方法
//获取类的类方法
Class metaClass = object_getClass([Cat class]);
unsigned int count = 0;
Method *allMothods = class_copyMethodList(metaClass, &count);
for (int i = 0; i < count; i++) {
Method md = allMothods[i];
SEL sel = method_getName(md);
//得到sel的方法名:以字符串格式获取sel的name,也即@selector()中的方法名称
const char *methodName = sel_getName(sel);
NSLog(@"(Method:%s)",methodName);
}
free(allMothods);
获取类的属性
//获取类的属性列表
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([Cat class], &count);
for (unsigned int i = 0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"PropertyName(%d): %@",i,[NSString stringWithUTF8String:propertyName]);
}
free(propertyList);
获取类的成员变量
unsigned int count = 0;
Ivar *allVariables = class_copyIvarList([Cat class], &count);
for (int i = 0; i<count; i++) {
Ivar ivar = allVariables[i];
const char *VariableName = ivar_getName(ivar);//获取成员变量名称
const char *VariableType = ivar_getTypeEncoding(ivar);//获取成员变量类型
NSLog(@"(Name:%s) ---(Type:%s)",VariableName,VariableType);
}
free(allVariables);
获取类所遵守的协议
//获取类所遵守的所有协议
unsigned int count;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([Cat class], &count);
for (int i=0; i< count; i++) {
Protocol *protocal = protocolList[i];
const char *protocolName = protocol_getName(protocal);
NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
}
free(protocolList);
方法添加和替换
方法的添加
方法的添加我们在说消息转发的时候已经提到了:
/**
cls 被添加方法的类
name 添加的方法的名称
imp 方法的实现,提倡用class_getMethodImplementation 调用
types 上面提到了,返回值类型和参数
*/
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) ;
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(newEat)), "V@:");
方法的替换
上面我们获取到了所有的方法,我们可以根据需要来替换掉里面的一些方法。关于方法的替换我们在这交换的是自己写的Demo的方法,并没有交换系统的方法,具体示例如下:
///Cat.h
#import "Animal.h"
@interface Cat : Animal
- (void)sleep;
@end
///Cat.m
#import "Cat.h"
@interface Cat ()
@property (nonatomic, copy) NSString *name;
@end
@implementation Cat
- (void)sleep {
NSLog(@"cat睡觉了");
}
- (void)sleepAndDream {
NSLog(@"cat睡觉并且做梦了");
}
@end
Cat *cat = [[Cat alloc] init];
[cat sleep];
SEL originalSleep = @selector(sleep);
SEL swizzledSleep = @selector(sleepAndDream);
Method originalMethod = class_getInstanceMethod([cat class],originalSleep);
Method swizzledMethod = class_getInstanceMethod([cat class],swizzledSleep);
//判断名为swizzledMethod的方法是否存在存在,返回NO表示存在,YES表示不存在。
BOOL didAddMethod = class_addMethod([cat class], originalSleep, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
//如果swizzledSleep不存在,我们就用它替换掉我们要交换的方法
class_replaceMethod([cat class], swizzledSleep, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
//如果swizzledSleep存在,交换他们两个的实现
method_exchangeImplementations(originalMethod, swizzledMethod);
}
[cat sleep];
在上面的代码中我们执行了两个方法:class_replaceMethod
和method_exchangeImplementations
,我们在进行方法替换之前先判断我们替换的方法的实现在类中是否存在,如果存在的话我们执行method_exchangeImplementations
交换两个方法的实现,如果不存在我们直接执行class_replaceMethod
替换掉旧的方法就可以。
修改私有属性
///Cat.m
#import "Cat.h"
@interface Cat ()
@property (nonatomic, copy) NSString *name;
@end
@implementation Cat
- (instancetype)init
{
self = [super init];
if (self) {
self.name = @"小小酥";
}
return self;
}
+ (NSInteger)getCatAge {
return 8;
}
- (void)sleep {
NSLog(@"cat睡觉了");
}
- (void)sleepAndDream {
NSLog(@"cat睡觉并且做梦了");
}
@end
Cat *cat = [[Cat alloc] init];
NSLog(@"cat的name: %@",[cat valueForKey:@"name"]); //null
//第一步:遍历对象的所有属性
unsigned int count;
Ivar *ivarList = class_copyIvarList([cat class], &count);
for (int i= 0; i<count; i++) {
//第二步:获取每个属性名
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSString *propertyName = [NSString stringWithUTF8String:ivarName];
if ([propertyName isEqualToString:@"_name"]) {
//第三步:匹配到对应的属性,然后修改;注意属性带有下划线
object_setIvar(cat, ivar, @"hello小小酥");
}
}
NSLog(@"cat的name: %@",[cat valueForKey:@"name"]);
实现NSCoding的自动归档和自动解档
归档是一种常用的轻量型文件存储方式,但是它有个弊端:在归档过程中,若一个Model有多个属性,我们不得不对每个属性进行处理,非常繁琐。
归档操作主要涉及两个方法:encodeObject 和 decodeObjectForKey,现在,我们可以利用Runtime来改进它们,其原理就是使用Runtime动态获取所有属性,关键的代码示例如下:
///解档操作
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self) {
Class c = self.class;
while (c && c != [NSObject class]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(c, &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id value = [self valueForKey:key];
[self setValue:value forKey:key];
}
c = [c superclass];
free(ivars);
}
}
return self;
}
///归档操作
- (void)encodeWithCoder:(NSCoder *)coder
{
Class c = self.class;
while (c && c != [NSObject class]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(c, &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id value = [self valueForKey:key];
[coder encodeObject:value forKey:key];
}
c = [c superclass];
free(ivars);
}
}
实现字典和模型的自动转换
将字典转成模型,我们可以参考MJExtension,这里就不在一一赘述了,核心代码如下(将代码添加到NSObject的分类中):
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [self init]) {
//(1)获取类的属性及属性对应的类型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//通过property_getName函数获得属性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通过property_getAttributes函数可以获得属性的名字和@encode编码
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
//立即释放properties指向的内存
free(properties);
//(2)根据类型给属性赋值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;
}
以上就是Runtime的用法,其实Runtime还可以根据消息转发实现热更新,但是由于Apple拒绝审核热修复的问题,所以不再此进行介绍,有兴趣的同学可以去看一下JSPatch源码。
结束
以上就是Runtime的详细介绍和在实际开发中使用,Runtime作为Objective-C的核心,学习和深入地了解它有助于我们进行更好的开发。我们在学习中也可以去尝试了解编程语言的底层实现,这样更有利于我们进行深度学习。
文章若有不足之处还请不吝赐教,大家互相学习。如果您觉得我的文章有用,点一下喜欢就可以了哦。
参考文章:
1.Runtime-iOS运行时基础篇
2.Runtime-iOS运行时应用篇
3.iOS Runtime详解