Runtime基础
1.Runtime相关的头文件
学习Runtime第一手源码,看下面三个头文件基本就可以了。
#import <objc/runtime.h>
#import <objc/message.h>
#import <objc/objc.h>
2.Runtime几个重要的结构体
从结构体中看出,其实实例内部就只有一个isa指针,指向自己的类。
// 实例
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
下面是关于类的结构体的说明。
// 类
struct objc_class {
// 类的isa指针指向自己的元类
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
// super_class指向父类
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;
类的结构体中定义类相关的属性,下面着重说明实例变量数组和方法数组两个结构体。
objc_ivar_list
成员变量数组
// 成员变量
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
} OBJC2_UNAVAILABLE;
// 可以认为是一个数组,数组的元素就是objc_ivar
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
}
objc_method_list
方法数组
struct objc_method {
// 方法名
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
// 方法实际执行的函数
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
// 可以看成是存储方法的一个数组,每个元素objc_method
struct objc_method_list {
struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
3.相关函数
3.1获取类名
根据一个类动态的获取类名,但是class_getName
返回的是C
语言,需要通过NSString
转成OC
的字符串。
+ (NSString *)fetchClassName:(Class)class {
const char *className = class_getName(class);
return [NSString stringWithUTF8String:className];
}
3.2获取成员变量
下面的fetchIvarList
通过class_copyIvarList
可以获取到实例变量列表,然后通过ivar_getName
获取变量名,ivar_getTypeEncoding
获取变量的类型,再把变量名和类型通过一个字典保存到数组中。
+ (NSArray *)fetchIvarList:(Class)class {
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(class, &count);
NSMutableArray *mutableList = [NSMutableArray new];
for (int i = 0; i < count; i++) {
NSMutableDictionary *dictM = [NSMutableDictionary new];
const char *ivarName = ivar_getName(ivarList[i]);
const char *ivarType = ivar_getTypeEncoding(ivarList[i]);
dictM[@"type"] = [NSString stringWithUTF8String:ivarType];
dictM[@"ivarName"] = [NSString stringWithUTF8String:ivarName];
[mutableList addObject:dictM];
}
// 这里记得释放,要不然内存泄露
free(ivarList);
return [NSArray arrayWithArray:mutableList];
}
3.3获取属性
其实获取属性和获取成员变量的方式基本一致,只是调用的方法有点不太一样class_copyPropertyList
获取的是成员变量数组,property_getName
获取的是变量名,方法调用很类似。
+ (NSArray *)fetchPropertyList:(Class)class {
unsigned int count = 0;
objc_property_t *propertyList = class_copyPropertyList(class, &count);
NSMutableArray *mutableList = [NSMutableArray new];
for (int i = 0; i < count; i++) {
const char *propertyName = property_getName(propertyList[i]);
[mutableList addObject:[NSString stringWithUTF8String:propertyName]];
}
// 一样的也是需要释放
free(propertyList);
return [NSArray arrayWithArray:mutableList];
}
3.4获取类的实例方法
封装的fetchMethodList
获取类实例的方法列表,class_copyMethodList
的用法跟上面类似,但是要注意一点,获取的数组内部的元素其实是Method
,需要通过method_getName
获取方法的选择子SEL
,然后再通过NSStringFromSelector
获取方法名。
+ (NSArray *)fetchMethodList:(Class)class {
unsigned int count = 0;
Method *methodList = class_copyMethodList(class, &count);
NSMutableArray *mutalbelist = [NSMutableArray new];
for (int i = 0; i < count; i++) {
SEL methodSel = method_getName(methodList[i]);
NSString *methodName = NSStringFromSelector(methodSel);
[mutalbelist addObject:methodName];
}
free(methodList);
return [NSArray arrayWithArray:mutalbelist];
}
3.5动态添加方法
动态方法添加在实际开发中可能用的比较少,但是在消息转发和崩溃处理的时候可以动态的给对象添加方法,防止程序崩溃。下面是通过class_addMethod
给当前实例对象self
添加一个run
的方法,run
方法只是一个SEL
,对应实际执行的函数是myRun
;第一个参数是需要添加方法的类,第二个参数是添加的方法名,第三个参数是方法实际执行的函数,第四个是方法参数的@encode
指令,关于@encode
指令的介绍请参考这里,对于@encode
的说明和使用如果哪位大佬有对应的博客可以留言学习,对这一块也不是很了解,v@:
函数的类型,(返回值+参数类型) v:void
@
对象->self
:
表示SEL->_cmd
。
class_addMethod(object_getClass(self), @selector(run:), (IMP)myRun, "v@:");
[self performSelector:@selector(run:) withObject:@"哈哈"];
// IMP至少要带两个参数self和_cmd,其他的参数参数可以添加到后面用逗号隔开
void myRun(id self, SEL _cmd, id obj) {
NSLog(@"sel:%@, obj:%@", NSStringFromSelector(_cmd), obj);
}
// 打印的结果如下
sel:run:, obj:哈哈
3.6方法交换
方法交换在整个苹果的体系下应用非常广泛,在我们实际的开发中也经常用到,后续在Runtime
应用场景会介绍详细的用法。下面简单的介绍方法交换,前提是class
内部已经实现了firstSel
和secondSel
对应的实现,否则无法找到方法实现,会崩溃,class_getInstanceMethod
可以通过SEL
选择子在class
中找到对应的Method
,再调用method_exchangeImplementations
就可以实现两个方法的交换。
+ (void)methodSwap:(Class)class firstSel:(SEL)firstSel secondSel:(SEL)secondSel {
Method firstMethod = class_getInstanceMethod(class, firstSel);
Method secondMethod = class_getInstanceMethod(class, secondSel);
method_exchangeImplementations(firstMethod, secondMethod);
}
我们可以简单在ViewController
中测试一下,看看是不是真的互换了?在load
方法中把两个方法交换。从打印信息可以看到,两个方法的调用顺序其实已经反过来了,说明确实是调换了方法。
+(void)load {
[super load];
[self methodSwap:self firstSel: @selector(sayHello1) secondSel: @selector(sayHello2)];
}
- (void)viewDidLoad {
[self sayHello1];
[self sayHello2];
}
- (void)sayHello1 {
NSLog(@"sayHello1");
}
- (void)sayHello2 {
NSLog(@"sayHello2");
}
// 打印结果如下
2021-10-20 18:09:18.280476+0800 abffff[5660:200359] sayHello2
2021-10-20 18:09:18.280632+0800 abffff[5660:200359] sayHello1