
iOS Runtime理解和应用场景
第一个参数是需要添加方法的类,第二个参数是一个selector,也就是实例方法的名字,第三个参数是一个IMP类型的变量也就是函数实现,需要传入一个C函数,这个函数至少有两个参数,一个是id self一个是SEL _cmd,第四个参数是函数类型。3)完整的消息转发:当-(void)forwardInvocation:(NSInvocation*)anInvocation 方法方法nil的时候则进入消
一、Runtime的动态性
iOS runtime 底层详解、内部原理、场景应用-CSDN博客
OC的运行时系统(Runtime System)提供了丰富的动态特性,包括类与对象的创建、消息发送与转发、方法的动态添加与替换、属性的动态合成等。通过使用运行时库提供的API,可以在运行时获取和操作类与对象的信息,实现各种动态性的功能。
我对 Runtime 的理解是,它是 Objective-C 语言的核心之一,为我们提供了一种在程序运行时动态操作类和对象的能力。通过 Runtime,我们可以在不修改源代码的情况下,实现诸如动态创建类、添加成员变量、调用方法、交换方法实现等功能,从而实现更灵活、更动态的编程方式。
Runtime是纯C的API,我们编写的OC代码最终都会转化成Runtime的C代码执行。举个简单的例子[target doSomething];,这是我们平常最常用的调用方法的形式,实际上最终会转化为如下C的代码objc_msSend(target,@selector(doSomething))(也就是我们所说的消息机制)。
我们知道,OC是面向对象的语言,在OC中可以说一切皆对象。类通过实例化就得到了对象,实际上类本身也是一个对象,在runtime中用结构体表示:
//类在runtime中的表示
struct objc_class {
Class isa;//指针,顾名思义,表示是一个什么,
//实例的isa指向类对象,类对象的isa指向元类
#if !__OBJC2__
Class super_class; //指向父类
const char *name; //类名
long version;
long info;
long instance_size
struct objc_ivar_list *ivars //成员变量列表
struct objc_method_list **methodLists; //方法列表
struct objc_cache *cache;//缓存
//一种优化,调用过的方法存入缓存列表,下次调用先找缓存
struct objc_protocol_list *protocols //协议列表
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
其中下面几个属性,使我们运用runtime时可能会用到的:
/// 描述类中的一个方法
typedef struct objc_method *Method;
/// 成员变量
typedef struct objc_ivar *Ivar;
/// 类别Category
typedef struct objc_category *Category;
/// 类中声明的属性
typedef struct objc_property *objc_property_t;
原文链接:https://blog.csdn.net/qq_34101611/article/details/51607816
二、基础
iOS 的运行时(Runtime)是 Objective-C 和 Swift 编程的重要组成部分,它使得许多高级功能和动态特性成为可能。理解 iOS 运行时可以帮助开发者在高级应用开发中更灵活地操控对象、方法和类。以下是对 iOS 运行时的详细解释和一些常见应用。
运行时交互
在Objective-C
中与运行时系统有三个层次的交互:
- 通过
Objective-C
源码 - 通过
Foundation
中的NSObject
定义的方法 - 通过直接调用运行时函数
Objective-C 源代码
顾名思义,只要编写和编译 Objective-C
代码即可使用它。编译包含Objective-C
类和方法的代码时,编译器会创建实现语言动态特性的数据结构和函数。
NSObject 方法
在Cocoa
中,大多数对象都是NSObject
的子类,因此都继承了他定义的方法。(NSProxy
是个特例) NSObject
中有一些方法可以简单地向运行时系统查询信息。支持对象执行自省。例如class方法,有isKindOfClass:
和 isMemberOfClass:
, 检查对象在继承层级结构的位置是否正确; conformsToProtocol:
,对象是否要实现特定协议中定义的方法;respondsToSelector:
,表示对象是否可以接受特定消息;
// 检查对象在继承层级结构的位置;
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;
// 指示对象是否声称要实现特定协议中定义的方法
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
// 表示对象是否可以接受特定消息 (检查是否实现某个方法)
- (BOOL)respondsToSelector:(SEL)aSelector;
// 方法实现的地址
- (IMP)methodForSelector:(SEL)aSelector;
运行时函数
运行时系统是一个共享的动态库,公共的接口在目录下/usr/include/objc
。其中包含了一些函数和数据结构,许多函数可以使用纯C来复制编译器在编写 Objective-C 代码时所做的事情。这其实也就是我们常常看到的,使用某些运行时函数可以达到可以NSObject方法一样的效果,其实也正是这些底层的函数构成了NSObject的基础功能。这里是官方文档Objective-C 运行时参考。
现在,我们知道了运行时交互有哪些,那么接下来,我们再看看Objective-C
中的一些基本概念:类、对象、Method、SEL、IMP。熟悉这些概念之后,会更加理解运行时做了哪些事。
类、对象、Method、SEL、IMP
类
类对象(Class
)是由程序设置后在运行时由编译器创建的,当一个对象的实例方法被调用时,会通过isa
找到这个类,然后在该类中方法列表中查找。
// Class 定义
typedef struct objc_class *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;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
结构体里有指向父类的指针、类名、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等。
那疑问来了,请问类方法是存在哪里的?我们在调用类方法的时候,我们如何去找呢?这里就引入一个概念:元类(meta-class)。(想要深入了解元类可以查看这篇文章 What is a meta-class in Objective-C?)
元类就是类对象的isa指向的类,也可以说是类对象的类
对象
实例对象(Object
)是我们对类对象alloc或者new操作时所创建的,在这个过程中会拷贝实例所属的类的成员变量,但并不拷贝类定义的方法。调用实例方法时,系统会根据实例的isa指针去类的方法列表及父类的方法列表中寻找与消息对应的selector指向的方法
// 对象结构体
struct objc_object {
Class _Nonnull isa; OBJC_ISA_AVAILABILITY;
};
由此,我们得出了一个结论,类对象和实例对象的查找机制是一样的:
- 对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。
- 类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。
对应关系如下图,描述了对象,类,元类之间的关系:
图中实线是 super_class指针,虚线是isa指针。
- Root class (class)就是NSObject,NSObject没有超类,所以Root class(class)的superclass指向nil。
- 每个Class都有一个isa指向唯一的Meta class
- Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
- 每个Meta class的isa都指向Root class (meta)。
Method
Method
就是我们平时所说的函数,它表示的是能够独立完成一个功能的一段代码;Method
通过selector
和IMP
两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp
SEL
SEL是方法选择器, 常见的写法有:@selector()
/// 代表一个方法的不透明类型
typedef struct objc_selector *SEL;
声明方式:
SEL s1 = @selector(test1);
SEL s2 = NSSelectorFromString(@"test2");
IMP
IMP
是指向最终实现程序的内存地址的指针,下面是它的定义:
声明方式:
SEL s1 = @selector(test1);
SEL s2 = NSSelectorFromString(@"test2");
理解了前面的这些概念之后,接下来我们进入正题。
在 Objective-C 中,消息直到运行时才绑定到方法实现。编译器转换消息表达式,调用objc_msgSend方法。
消息发送
Objective-C
中所有方法的调用/类的生成都在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法,也可以替换某个类的方法为新的实现,理论上你可以在运行时通过类名/方法名调用到任何 Objective-C
方法,替换任何类的实现以及新增任意类。
比方说我们写一个调用方法[receiver message]
,那这个方法会被编译器转化成:
// 第一个参数类型是发送者, 第二个参数类型是SEL。SEL在OC中是selector方法选择器
id objc_msgSend ( id _Nullable self, SEL op, ... );
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。由于这点特性,也导致了Objective-C
不支持函数重载。
实际上,我们在调用的方法的过程,其实在Runtime中就是消息发送。
objc_msgSend
的实现是由汇编语言
实现,根据CPU架构实现的过程各不相同,如果想阅读相关的代码要有一定的汇编基础;
objc_msgSend
会做以下几件事情:
- 检测这个selector是不是要忽略
- 检查target是不是为nil
- 如果这里有相应的nil的处理函数,就跳转到相应的函数中
- 如果没有处理nil的函数,就自动清理并返回。这一点就是为何在
Objective-C
中给nil发送消息不会崩溃的原因。
- 确定不是给nil发消息之后,在该对象的类(Class)的缓存中查找方法对应的IMP;(俗称快查)
- 如果找到,就跳转进去执行;
- 如果没有找到,执行下一步;
- 在方法列表中继续查找,一直找到NSObject为止;(俗称慢找)
- 如果还没有找到,那就需要开始消息转发阶段了。至此,发送消息Messaging阶段完成。这一阶段主要完成的是通过select()快速查找IMP的过程
Notice: 为了加快消息传递过程,运行时系统会在使用方法时缓存方法的选择器和地址。每个类都有一个单独的缓存,它可以包含继承方法以及类中定义的方法的选择器。在消息传递过程中,会首先检查接收对象类的缓存。
消息传递框架:
objc_msgSend函数会依据接收者与选择器的类型来调用适当的方法。为了完成此操作, 该方法需要在接收者所属的类中搜寻其“方法列表”( list of methods),如果能找到与选择器 名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”( message forwarding)操作。
消息转发
转发阶段,会调用_objc_msgForward(id self, SEL _cmd,...)
方法
_objc_msgForward(id _Nonnull receiver, SEL _Nonnull sel, ...)
_objc_msgForward
会做以下几件事情:
- 先调用备用接收者方法
forwardingTargetForSelector
获取新的 target 作为 receiver 重新执行 selector
- 如果返回的内容合法, 跳转去执行
- 如果返回的内容不合法(为 nil 或者跟旧 receiver 一样),继续执行后续方法。
- 检查有没有实现方法签名
methodSignatureForSelector
- 对象实现了该方法,则调用
methodSignatureForSelector
获取方法签名后,判断返回类型信息是否正确,再调用forwardInvocation
执行NSInvocation
对象,并将结果返回。 - 如果对象没实现
methodSignatureForSelector
方法,继续执行后面方法。
- 调用
doesNotRecognizeSelector
方法,抛出异常。
作者:新生代农民工No1
链接:https://juejin.cn/post/7063376892178464799
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
三、概述
1. iOS 运行时简介
iOS 运行时主要是指 Objective-C 运行时库,它是一个动态库,负责在程序运行时动态地执行一些操作,如方法调用、消息传递、方法交换等。
动态特性
- 消息传递:在 Objective-C 中,对象通过消息传递来调用方法。这与其他语言的函数调用有所不同。
- 动态类型检查:在运行时可以检查对象的类型。
- 动态方法解析:可以在运行时添加新的方法或替换现有的方法。
2. 运行时基础操作
消息传递
在 Objective-C 中,方法调用实际上是通过消息传递实现的。
// 等价于 [object method];
objc_msgSend(object, @selector(method));
动态类型检查
可以在运行时检查对象是否响应某个方法。
if ([object respondsToSelector:@selector(method)]) {
[object method];
}
动态方法解析
可以在运行时添加新的方法或替换现有的方法。
#import <objc/runtime.h>
void newMethod(id self, SEL _cmd) {
NSLog(@"New method added at runtime!");
}
int main() {
Class MyClass = objc_allocateClassPair([NSObject class], "MyClass", 0);
class_addMethod(MyClass, @selector(newMethod), (IMP)newMethod, "v@:");
objc_registerClassPair(MyClass);
id myObject = [[MyClass alloc] init];
[myObject performSelector:@selector(newMethod)];
return 0;
}
3. 常见应用
方法交换(Method Swizzling)
方法交换是一个强大的工具,允许在运行时交换两个方法的实现。这在调试和为现有类添加新功能时特别有用。
#import <objc/runtime.h>
@implementation UIViewController (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzled_viewWillAppear:));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
- (void)swizzled_viewWillAppear:(BOOL)animated {
NSLog(@"View will appear: %@", self);
[self swizzled_viewWillAppear:animated]; // This actually calls the original viewWillAppear: due to the method swizzling
}
@end
动态添加属性
运行时也允许在类的实例上动态添加属性。
#import <objc/runtime.h>
@interface NSObject (DynamicProperty)
@property (nonatomic, strong) id dynamicProperty;
@end
@implementation NSObject (DynamicProperty)
- (void)setDynamicProperty:(id)dynamicProperty {
objc_setAssociatedObject(self, @selector(dynamicProperty), dynamicProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)dynamicProperty {
return objc_getAssociatedObject(self, @selector(dynamicProperty));
}
@end
动态创建类
可以在运行时动态创建类,并为其添加方法和属性。
#import <objc/runtime.h>
int main() {
Class MyClass = objc_allocateClassPair([NSObject class], "MyClass", 0);
class_addIvar(MyClass, "dynamicIvar", sizeof(id), log2(sizeof(id)), @encode(id));
objc_registerClassPair(MyClass);
id myObject = [[MyClass alloc] init];
[myObject setValue:@"Hello, World!" forKey:@"dynamicIvar"];
NSLog(@"%@", [myObject valueForKey:@"dynamicIvar"]);
return 0;
}
4. 运行时函数
以下是一些常用的运行时函数:
-
类相关函数
objc_allocateClassPair
: 动态创建类。objc_registerClassPair
: 注册动态创建的类。objc_getClass
: 获取类。objc_getMetaClass
: 获取元类。
-
方法相关函数
class_addMethod
: 为类添加方法。class_getInstanceMethod
: 获取实例方法。class_getClassMethod
: 获取类方法。method_exchangeImplementations
: 交换方法实现。
-
属性和变量相关函数
class_addIvar
: 添加实例变量。class_copyIvarList
: 获取实例变量列表。class_addProperty
: 添加属性。class_copyPropertyList
: 获取属性列表。
-
关联对象
objc_setAssociatedObject
: 设置关联对象。objc_getAssociatedObject
: 获取关联对象。objc_removeAssociatedObjects
: 移除所有关联对象。
5. 运行时的实际应用场景
1. 调试和测试
可以通过方法交换拦截和记录方法调用,帮助调试和测试。
#import <objc/runtime.h>
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
2. 为现有类添加功能
可以通过类别(Category)和方法交换,为现有类添加新功能。
#import <objc/runtime.h>
@implementation UIViewController (Logging)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
swizzleMethod([self class], @selector(viewDidLoad), @selector(logging_viewDidLoad));
});
}
- (void)logging_viewDidLoad {
NSLog(@"View did load: %@", self);
[self logging_viewDidLoad];
}
@end
3. 动态接口实现
通过运行时,可以在运行时实现接口,适用于插件系统或动态模块加载。
总结
iOS 的运行时(Runtime)是一个强大的工具,允许开发者在程序运行时动态地操控对象、方法和类。理解和应用运行时技术,可以实现许多高级功能,如方法交换、动态添加属性、动态创建类等。这些技术在调试、测试、动态接口实现等方面具有重要应用。通过合理使用运行时,可以打造更灵活和强大的 iOS 应用。
四、应用场景
1、实现Runtime在分类创建属性(添加关联对象)
当使用 Objective-C 的分类来添加属性时,通常情况下是无法直接在分类中声明实例变量的,因为分类不允许添加实例变量。但是,我们可以利用 Runtime 来实现在分类中添加属性的功能,具体步骤如下:
定义关联的键值:为了关联属性和其对应的存取方法,需要定义一个全局唯一的键值。
实现属性的 getter 和 setter 方法:在分类中实现属性的 getter 和 setter 方法,这些方法将通过 Runtime 添加到分类中。
使用 Runtime 添加属性:在分类的实现文件中,通过 Runtime 的函数来动态地为类添加属性。
下面是一个示例代码,演示了如何使用 Runtime 在分类中添加属性:
#import <objc/runtime.h>
// 定义关联的键值
static char kAssociatedObjectKey;
@interface NSObject (MyCategory)
// 声明属性
@property (nonatomic, strong) NSString *myProperty;
@end
@implementation NSObject (MyCategory)
// 实现属性的 getter 方法
- (NSString *)myProperty {
// 使用关联对象获取属性值
return objc_getAssociatedObject(self, &kAssociatedObjectKey);
}
// 实现属性的 setter 方法
- (void)setMyProperty:(NSString *)myProperty {
// 使用关联对象设置属性值
objc_setAssociatedObject(self, &kAssociatedObjectKey, myProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
2--黑魔法:交换方法用于统一处理某个方法
管理类方法.h
#import <Foundation/Foundation.h>
@interface AARunTimeUtility : NSObject
/**
交换实例方法
@param cls 当前class
@param originalSelector originalSelector description
@param swizzledSelector swizzledSelector description
@return 返回
*/
+ (BOOL)swizzlingInstanceMethodInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;
/**
交换类方法
@param cls 当前class
@param originalSelector originalSelector description
@param swizzledSelector swizzledSelector description
@return 成
*/
+ (BOOL)swizzlingClassMethodInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;
@end
.m文件
#import "AARunTimeUtility.h"
#import <objc/runtime.h>
@implementation AARunTimeUtility
+ (BOOL)swizzlingInstanceMethodInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
Class class = cls;
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod)
{
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else
{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
return didAddMethod;
}
+ (BOOL)swizzlingClassMethodInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
Class class = cls;
Method originalMethod = class_getClassMethod(class, originalSelector);
Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod)
{
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else
{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
return didAddMethod;
}
@end
(一)使用情况大量需要替换的情况
iOS新发布的时候在Scrollview的头部会系统默认多出一段空白;
解决方法是设置其contentInsetAdjustmentBehavior属性为UIScrollViewContentInsetAdjustmentNever。但对于现存的项目来说挨个修改工作量无疑是巨大的,也容易出问题。这时候就用到Runtime了,用runtime来交换其初始化方法来统一设置这个属性就可以得到解决。34e
#import <UIKit/UIKit.h>
@interface UIScrollView (Inset)
@end
#import "UIScrollView+Inset.h"
#import "AARunTimeUtility.h"
@implementation UIScrollView (Inset)
+(void)load {
[AARunTimeUtility swizzlingInstanceMethodInClass:[self class] originalSelector:@selector(initWithFrame:) swizzledSelector:@selector(m_initWithFrame:)];
}
- (instancetype)m_initWithFrame:(CGRect)frame {
UIScrollView *scrollV = [self m_initWithFrame:frame];
if (@available(iOS 11.0, *)) {
scrollV.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
return scrollV;
}
@end
(二)防止数组奔溃交换方式
#import "AARunTimeUtility.h"
@implementation NSArray (SafeAccess)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// NSArray的类簇实际类型为__NSArrayI
Class arrayClass = NSClassFromString(@"__NSArrayI");
[AARunTimeUtility swizzlingInstanceMethodInClass:arrayClass originalSelector:@selector(objectAtIndex:) swizzledSelector:@selector(safe_objectAtIndex:)];
// NSMutableArray的类簇实际类型为__NSArrayM
Class mutableArrayClass = NSClassFromString(@"__NSArrayM");
[AARunTimeUtility swizzlingInstanceMethodInClass:mutableArrayClass originalSelector:@selector(objectAtIndex:) swizzledSelector:@selector(safe_mutableObjectAtIndex:)];
[AARunTimeUtility swizzlingInstanceMethodInClass:mutableArrayClass originalSelector:@selector(addObject:) swizzledSelector:@selector(safe_addObject:)];
[AARunTimeUtility swizzlingInstanceMethodInClass:mutableArrayClass originalSelector:@selector(removeObjectAtIndex:) swizzledSelector:@selector(safe_removeObjectAtIndex:)];
});
}
- (id)safe_objectAtIndex:(NSUInteger)index {
if (index >= self.count) {
// 处理越界情况,这里可以选择返回 nil 或者其他默认值,或者抛出异常
// 例如:
// @throw [NSException exceptionWithName:NSRangeException reason:@"Index out of bounds" userInfo:nil];
return nil;
} else {
// 调用原始方法
return [self safe_objectAtIndex:index];
}
}
@end
@implementation NSMutableArray (SafeAccess)
- (id)safe_mutableObjectAtIndex:(NSUInteger)index {
if (index >= self.count) {
// 处理越界情况,这里可以选择返回 nil 或者其他默认值,或者抛出异常
// 例如:
// @throw [NSException exceptionWithName:NSRangeException reason:@"Index out of bounds" userInfo:nil];
return nil;
} else {
// 调用原始方法
return [self safe_mutableObjectAtIndex:index];
}
}
- (void)safe_addObject:(id)anObject {
if (anObject) {
[self safe_addObject:anObject];
}
}
- (void)safe_removeObjectAtIndex:(NSUInteger)index {
if (index < self.count) {
[self safe_removeObjectAtIndex:index];
}
}
@end
3--遍历类属性--映射解析
类属性
开发日常中我们对Model数据进行解析是必然的操作,包括很多三方解析框架都是通过runtime来获取相关属性进行映射解析的。getObjcPropertyWithClass: 方法用于获取对象的所有属性名。它使用 Runtime 函数 class_copyPropertyList 来获取对象的属性列表,并遍历列表获取每个属性的名字。最终返回属性名的数组。
#import <Foundation/Foundation.h>
@interface NSObject (MutableCopy)
- (id)getMutableCopy;
@end
@implementation NSObject (MutableCopy)
- (id)getMutableCopy {
// 获取类的属性名数组
NSArray<NSString *> *keys = [self getObjcPropertyWithClass:[self class]];
// 创建新对象
id newObj = [[[self class] alloc] init];
// 遍历属性名数组,将属性的值赋给新对象
for (NSString *key in keys) {
id value = [self valueForKey:key];
if (value) {
[newObj setValue:value forKey:key];
}
}
return newObj;
}
- (NSArray<NSString *> *)getObjcPropertyWithClass:(Class)cls {
NSMutableArray<NSString *> *keys = [NSMutableArray array];
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(cls, &outCount);
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
[keys addObject:propertyName];
}
free(properties);
while (cls != [NSObject class]) {
cls = [cls superclass];
properties = class_copyPropertyList(cls, &outCount);
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
[keys addObject:propertyName];
}
free(properties);
}
// 去重并返回
return [keys valueForKeyPath:@"@distinctUnionOfObjects.self"];
}
@end
获取列表:
有时我们会有获取类中属性名字的需求,例如在字典转模型时,我们就需要知道当前类中每个属性的名字。我们可以通过runtime的一系列方法获取类的一些信息(包括属性列表,方法列表,成员变量列表,和遵循的协议列表)。
//1.获取成员变量列表
unsigned int count1 = 0;
Ivar *ivarList = class_copyIvarList([Student class], &count1);
for (int i = 0; i < count1; i++) {
const char *ivar = ivar_getName(ivarList[i]);
NSString *ivarName = [NSString stringWithUTF8String:ivar];
// NSLog(@"%@",ivarName);
}
//2.获取属性列表
unsigned int count2 = 0;
objc_property_t *propertyList = class_copyPropertyList([Student class], &count2);
for (int i = 0; i < count2; i++) {
const char *property = property_getName(propertyList[i]);
NSString *propertyName = [NSString stringWithUTF8String:property];
// NSLog(@"--->%@",propertyName);
}
//3.获取方法列表
unsigned int count3 = 0;
Method *methodList = class_copyMethodList([Student class], &count3);
for (int i = 0; i < count3; i++) {
SEL sel = method_getName(methodList[i]);
NSString *selName = NSStringFromSelector(sel);
// NSLog(@"+++++%@",selName);
}
//4.获取协议列表
unsigned int count4 = 0;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([Student class], &count4);
for (int i = 0; i < count4; i++) {
const char *pro = property_getName((__bridge objc_property_t)(protocolList[i]));
NSString *protocolName = [NSString stringWithUTF8String:pro];
NSLog(@"%@",protocolName);
}
4--修改isa指针,实现KVO
@interface Person : NSObject
{
@public
NSString * _name;
}
@property (nonatomic,copy) NSString *name;
@end
@interface NSObject (KVO)
- (void)cyx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
NSString * const cyx_key = @"observer";
@implementation NSObject (KVO)
-(void)cyx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
objc_setAssociatedObject(self, (__bridge const void *)(cyx_key), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//修改isa 指针
object_setClass(self, [SonPerson class]);
}
这里利用runtime修改isa指针,修改调用方法时寻找方法的类。这里我们修改到SonPerson类。并在SonPerson类里面实现监听方法
extern NSString * const cyx_key;
@implementation SonPerson
-(void)setName:(NSString *)name{
[super setName:name];
NSObject * observer = objc_getAssociatedObject(self, cyx_key);
[observer observeValueForKeyPath:@"name" ofObject:self change:nil context:nil];
}
这里也用到了runtime 里面 objc_getAssociatedObject 和objc_setAssociatedObject动态存储方法
#import "ViewController.h"
#import "Person.h"
#import "NSObject+KVO.h"
@interface ViewController ()
@property (nonatomic,strong) Person *p;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person * p = [[Person alloc] init];
[p cyx_addObserver:self forKeyPath:@"name" options:0 context:nil];
_p = p;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",_p.name);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
static int i=0;
i++;
_p.name = [NSString stringWithFormat:@"%d",i];
//_p -> _name = [NSString stringWithFormat:@"%d",i];
}
输出:
1
2
3
4
5
6
7
5--利用runtime实现消息转发机制的三次补救
众所周知OC的一个对象在发送消息的时候首先在cache里找,如果找不到就在该类的struct objc_method_list列表中去搜索,如果找到则直接调用相关方法的实现,如果没有找到就会通过super_class指针沿着继承树向上去搜索,如果找到就跳转,如果到了继承树的根部(通常为NSObject)还没有找到。那就会调用NSObjec的一个方法doesNotRecognizeSelector:,这样就会报unrecognized selector 错误。其实在调用doesNotRecognizeSelector:方法之前还会进行消息转发---还有三次机会来补救。也就是常说的OC消息转发的三次补救措施。
1)先在本类中搜索改方法的实现,如果有则直接调用若果没有则去父类中搜索直到NSObject,如果NSObject没有则进入消息转发(类的动态方法解析、备用接受者对象、完整的消息转发)。
2)类的动态方法解析:
首先创建SonPerson类,在ViewController 里面写
id person = [[SonPerson alloc]init];
[person appendString:@""];
注意这里要用id 不然编译报错。
在该类和父类中没有找到该方法的实现则会执行 +(BOOL)resolveClassMethod:(SEL)sel 或+(BOOL)resolveInstanceMethod:(SEL)sel 方法。在+(BOOL)resolveClassMethod:(SEL)sel 或+(BOOL)resolveInstanceMethod:(SEL)sel 方法 中利用黑魔法runtime 动态添加方法实现。
void dynamicAdditionMethodIMP(id self,SEL _cmd){
NSLog(@"dynamicAdditionMethodIMP");
}
+(BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(sel));
if(sel ==@selector(appendString:)) {
class_addMethod([selfclass], sel, (IMP)dynamicAdditionMethodIMP,"v@:");
returnYES;
}
return[superresolveClassMethod:sel];
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(sel));
if(sel ==@selector(appendString:)) {
class_addMethod([selfclass], sel, (IMP)dynamicAdditionMethodIMP,"v@:");
returnYES;
}
return[super resolveInstanceMethod:sel];
}
BOOL class_addMethod(Class cls, SEL name, IMP imp,constchar*types);
第一个参数是需要添加方法的类,第二个参数是一个selector,也就是实例方法的名字,第三个参数是一个IMP类型的变量也就是函数实现,需要传入一个C函数,这个函数至少有两个参数,一个是id self一个是SEL _cmd,第四个参数是函数类型。具体设置方法可以看注释。
控制台输出:
resolveInstanceMethod: appendString:
dynamicAdditionMethodIMP
2)备用接受者: 在+(BOOL)resolveClassMethod:(SEL)sel 或+(BOOL)resolveInstanceMethod:(SEL)sel 方法返回NO的时候进入备用接受者阶段 。
创建一个备用接受者类ForwardPerson 实现appendString:方法
-(void)appendString:(NSString*)str{
NSLog(@"%@===%@",NSStringFromClass([self class]),NSStringFromSelector(_cmd));
}
在SonPerson类中实现- (id)forwardingTargetForSelector:(SEL)aSelector 方法 并返回一个备用接受者对象
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"forwardingTargetForSelector");
return [ForwardPerson new];
}
控制台输出:
forwardingTargetForSelector
ForwardPerson===appendString:
3)完整的消息转发:当-(void)forwardInvocation:(NSInvocation*)anInvocation 方法方法nil的时候则进入消息转发的最后阶段,完整的消息转发。也需要创建一个转发对象ForwardInvocation
#import "ForwardInvocation.h"
@implementationForwardInvocation
-(void)appendString:(NSString*)str{
NSLog(@"%@===%@",NSStringFromClass([self class]),NSStringFromSelector(_cmd));
}
@end
在SonPerson中实现-(void)forwardInvocation:(NSInvocation*)anInvocation和- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector方法
-(void)forwardInvocation:(NSInvocation*)anInvocation{
NSLog(@"forwardInvocation");
if ([ForwardInvocation instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:self.invocation];
}
}
/*必须重新这个方法,消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象 返回nil上面方法不执行*/
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature*signature = [super methodSignatureForSelector:aSelector];
if(!signature){
if ([ForwardInvocation instancesRespondToSelector:aSelector]){
signature = [ForwardInvocation instanceMethodSignatureForSelector:aSelector];
}
}
returnsignature;
}
控制台输出:
forwardInvocation
ForwardInvocation===appendString
6. runtime在快速归档中的应用
#import "Student.h"
@interface Student()<NSCoding>
@end
@implementation Student
/** 归档 */
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeObject:@(self.age) forKey:@"age"];
[aCoder encodeObject:@(self.height) forKey:@"height"];
}
/** 解档 */
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
self.age = [[aDecoder decodeObjectForKey:@"age"] integerValue];
self.height = [[aDecoder decodeObjectForKey:@"height"] floatValue];
}
return self;
}
@end
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc] init];
student.name = @"刘亦菲";
student.age = 30;
student.height = 170.0;
/** 归档 */
// [NSKeyedArchiver archiveRootObject:student toFile:@"/Users/liuliuhaiqiang/Desktop/student.data"];
/** 解档 */
Student *newStudent = [NSKeyedUnarchiver unarchiveObjectWithData:[NSData dataWithContentsOfFile:@"/Users/liuliuhaiqiang/Desktop/student.data"]];
NSLog(@"姓名:%@,年龄:%ld,身高:%f",newStudent.name,(long)newStudent.age,newStudent.height);
}
@end
按照这样普通的方法,确实也能够归档。但是如果出现以下两种情况,用这种常规的方法进行归档就显得效率较低。一是类的属性较多是,二是类的属性发生改变时。这时使用runtime进行快速归档就能很好的解决以上问题。
/** 归档 */
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int count = 0;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (int i = 0; i < count; i++) {
const char *property = property_getName(propertyList[i]);
NSString *propertyName = [NSString stringWithUTF8String:property];
//先取值再归档
id value = [self valueForKey:propertyName];
[aCoder encodeObject:value forKey:propertyName];
}
}
/** 接档 */
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
unsigned int count = 0;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (int i = 0; i < count; i++) {
const char *property = property_getName(propertyList[i]);
NSString *propertyName = [NSString stringWithUTF8String:property];
//先解档在赋值
id value = [aDecoder decodeObjectForKey:propertyName];
[self setValue:value forKey:propertyName];
}
}
return self;
}
使用runtime后无论属性有多少,增加多少属性,均无需修改代码!
7. 什么是 Method Swizzling?它有哪些应用场景?
Method Swizzling 是一种改变方法实现的技术,主要用于修改现有方法的行为,通常在 +load
或 +initialize
方法中实现。常见的应用场景包括:
- 日志记录:在方法调用前后插入日志记录。
- 调试:为调试目的修改方法实现。
- 功能增强:为现有类添加新的功能,而无需修改源代码。
- Bug 修复:在不修改原始代码的情况下修复现有方法的 Bug。
8. Catogory和Extension
9.消息转发;
10.动态绑定:代理、懒加载;
11.动态加载:Plugin
12.动态创建类、实例变量、添加方法等;
系统中的应用
weak;dealloc;
内存;
缓存方案;
iOS runtime 详解和使用场景(最详细的使用教程)-CSDN博客
原文链接:https://blog.csdn.net/qq_34101611/article/details/51607816
原文链接:https://blog.csdn.net/C_philadd/article/details/136709732
参考:
更多推荐
所有评论(0)