前言
- 熟练的运用Runtime相关的技术,能够更好的解决复杂问题和实现复杂需求
类在runtime中的表示
//类在runtime中的表示
struct objc_class {
// 实例的isa指向类对象,类对象的isa指向元类
Class 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;
复制代码
API介绍
objc系列函数
例如类与协议的空间分配、注册、注销等操作
函数 | 函数作用 |
---|---|
objc_getClass | 获取Class对象 |
objc_getMetaClass | 获取MetaClass对象 |
objc_allocateClassPair | 分配空间,创建类 |
objc_registerClassPair | 注册一个类 |
objc_disposeClassPair | 注销某个类 |
objc_allocateProtocol | 开辟空间创建协议 |
objc_registerProtocol | 注册一个协议 |
objc_setAssociatedObject | 为实例对象关联对象 |
objc_getAssociatedObject | 获取实例对象的关联对象 |
objc_removeAssociatedObjects | 清空实例对象的所有关联对象 |
objc_getProtocol | 获取某个协议 |
objc_copyProtocolList | 拷贝在运行时中注册过的协议列表 |
class系列函数
例如实例变量、方法、属性、协议等相关问题
函数 | 函数作用 |
---|---|
class_addIvar | 为类添加实例变量 |
class_addProperty | 为类添加属性 |
class_addMethod | 为类添加方法 |
class_addProtocol | 为类遵循协议 |
class_replaceMethod | 替换类某方法的实现 |
class_getName | 获取类名 |
class_isMetaClass | 判断是否为元类 |
class_getSuperclass | 获取某类的父类 |
class_setSuperclass | 设置某类的父类 |
class_getProperty | 获取某类的属性 |
class_getInstanceVariable | 获取实例变量 |
class_getClassVariable | 获取类变量 |
class_getInstanceMethod | 获取实例方法 |
class_getClassMethod | 获取类方法 |
class_getMethodImplementation | 获取方法的实现 |
class_getInstanceSize | 获取类的实例的大小 |
class_respondsToSelector | 判断类是否实现某方法 |
class_conformsToProtocol | 判断类是否遵循某协议 |
class_createInstance | 创建类的实例 |
class_copyIvarList | 拷贝类的实例变量列表 |
class_copyMethodList | 拷贝类的方法列表 |
class_copyProtocolList | 拷贝类遵循的协议列表 |
class_copyPropertyList | 拷贝类的属性列表 |
object系列函数
例如实例变量
函数 | 函数作用 |
---|---|
object_getClassName | 获取对象的类名 |
object_getClass | 获取对象的Class |
object_setClass | 设置对象的Class |
object_getIvar | 获取对象中实例变量的值 |
object_setIvar | 设置对象中实例变量的值 |
object_getInstanceVariable | 获取对象中实例变量的值 (ARC中无效,使用object_getIvar) |
object_setInstanceVariable | 设置对象中实例变量的值 (ARC中无效,使用object_setIvar) |
method系列函数
例如方法的参数及返回值类型和方法的实现
函数 | 函数作用 |
---|---|
method_getName | 获取方法名 |
method_getImplementation | 获取方法的实现 |
method_getTypeEncoding | 获取方法的类型编码 |
method_getNumberOfArguments | 获取方法的参数个数 |
method_copyReturnType | 拷贝方法的返回类型 |
method_getReturnType | 获取方法的返回类型 |
method_copyArgumentType | 拷贝方法的参数类型 |
method_getArgumentType | 获取方法的参数类型 |
method_getDescription | 获取方法的描述 |
method_setImplementation | 设置方法的实现 |
method_exchangeImplementations | 替换方法的实现 |
property系列函数
如属性名、属性的特性等
函数 | 函数作用 |
---|---|
property_getName | 获取属性名 |
property_getAttributes | 获取属性的特性列表 |
property_copyAttributeList | 拷贝属性的特性列表 |
property_copyAttributeValue | 拷贝属性中某特性的值 |
protocol系列函数
如获取协议名称、是否遵循协议等
函数 | 函数作用 |
---|---|
protocol_conformsToProtocol | 判断一个协议是否遵循另一个协议 |
protocol_isEqual | 判断两个协议是否一致 |
protocol_getName | 获取协议名称 |
protocol_copyPropertyList | 拷贝协议的属性列表 |
protocol_copyProtocolList | 拷贝某协议所遵循的协议列表 |
protocol_copyMethodDescriptionList | 拷贝协议的方法列表 |
protocol_addProtocol | 为一个协议遵循另一协议 |
protocol_addProperty | 为协议添加属性 |
protocol_getProperty | 获取协议中的某个属性 |
protocol_addMethodDescription | 为协议添加方法描述 |
protocol_getMethodDescription | 获取协议中某方法的描述 |
ivar系列函数
函数 | 函数作用 |
---|---|
ivar_getName | 获取Ivar名称 |
ivar_getTypeEncoding | 获取类型编码 |
ivar_getOffset | 获取偏移量 |
sel系列函数
函数 | 函数作用 |
---|---|
sel_getName | 获取名称 |
sel_getUid | 注册方法 |
sel_registerName | 注册方法 |
sel_isEqual | 判断方法是否相等 |
imp系列函数
函数 | 函数作用 |
---|---|
imp_implementationWithBlock | 通过代码块创建IMP |
imp_getBlock | 获取函数指针中的代码块 |
imp_removeBlock | 移除IMP中的代码块 |
Runtime实战使用
获取列表
/// 描述类中的一个方法
typedef struct objc_method *Method;
/// 实例变量
typedef struct objc_ivar *Ivar;
/// 类别Category
typedef struct objc_category *Category;
/// 类中声明的属性
typedef struct objc_property *objc_property_t;
复制代码
有时候会有这样的需求,我们需要知道当前类中每个属性的名字(比如字典转模型,字典的Key和模型对象的属性名字不匹配)
我们可以通过runtime的一系列方法获取类的一些信息
- 属性列表
- 方法列表
- 成员变量列表
- 遵循的协议列表
/// 属性列表
@dynamic propertyTemps;
- (NSArray<NSString*>*)propertyTemps{
NSMutableArray *temps = [NSMutableArray array];
unsigned int outCount, i;
Class targetClass = [self class];
while (targetClass != [NSObject class]) {
objc_property_t *properties = class_copyPropertyList(targetClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
const char *char_f = property_getName(property);
NSString *propertyName = [NSString stringWithUTF8String:char_f];
if (propertyName) [temps addObject:propertyName];
}
free(properties);
targetClass = [targetClass superclass];
}
return temps.mutableCopy;
}
/// 成员变量列表
@dynamic ivarTemps;
- (NSArray<NSString*>*)ivarTemps{
unsigned int count;
Ivar *ivar = class_copyIvarList([self class], &count);
NSMutableArray *temp = [NSMutableArray arrayWithCapacity:count];
for (int i = 0; i < count; i++) {
const char *char_f = ivar_getName(ivar[i]);
NSString *name = [NSString stringWithCString:char_f encoding:NSUTF8StringEncoding];
if (name) [temp addObject:name];
}
return temp.mutableCopy;
}
/// 方法列表
@dynamic methodTemps;
- (NSArray<NSString*>*)methodTemps{
unsigned int count;
Method *method = class_copyMethodList([self class], &count);
NSMutableArray *temp = [NSMutableArray arrayWithCapacity:count];
for (int i = 0; i < count; i++) {
NSString *name = NSStringFromSelector(method_getName(method[i]));
if (name) [temp addObject:name];
}
return temp.mutableCopy;
}
/// 遵循的协议列表
@dynamic protocolTemps;
- (NSArray<NSString*>*)protocolTemps{
unsigned int count;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
NSMutableArray *temp = [NSMutableArray arrayWithCapacity:count];
for (unsigned int i = 0; i<count; i++) {
const char *protocolName = protocol_getName(protocolList[i]);
NSString *name = [NSString stringWithCString:protocolName encoding:NSUTF8StringEncoding];
if (name) [temp addObject:name];
}
return temp.mutableCopy;
}
复制代码
实战示例:实现NSCoding的自动归档和解档
@implementation KJTestModel
- (void)encodeWithCoder:(NSCoder *)encoder{
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([KJTestModel 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 = [self valueForKey:key];
[encoder encodeObject:value forKey:key];
}
free(ivars);
}
- (id)initWithCoder:(NSCoder *)decoder{
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([KJTestModel 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 = [decoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
@end
复制代码
方法调用
- 如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)操作。
- 如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中操作。
- 对象调用方法经过三个阶段
消息发送:查询cache和方法列表,找到了直接调用,找不到方法会进入下个阶段
动态解析:调用实例方法resolveInstanceMethod
或类方法resolveClassMethod
里面可以有一次动态添加方法的机会
消息转发:首先会判断是否有其他对象可以处理方法forwardingTargetForSelector
返回一个新的对象,如果没有新的对象进行处理,会调用methodSignatureForSelector
方法返回方法签名,然后调用forwardInvocation
这里可以做一个简单的防止调用未实现方法崩溃处理:选择在消息转发的最后一步来做处理,methodSignatureForSelector:
消息获得函数的参数和返回值,然后[self respondsToSelector:aSelector]
判断是否有该方法,如果没有返回函数签名,创建一个NSInvocation对象并发送给forwardInvocation
@implementation NSObject (KJUnrecognizedSelectorException)
+ (void)kj_openUnrecognizedSelectorExchangeMethod{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
kExceptionMethodSwizzling(self, @selector(methodSignatureForSelector:), @selector(kj_methodSignatureForSelector:));
kExceptionMethodSwizzling(self, @selector(forwardInvocation:), @selector(kj_forwardInvocation:));
kExceptionClassMethodSwizzling(self, @selector(methodSignatureForSelector:), @selector(kj_methodSignatureForSelector:));
kExceptionClassMethodSwizzling(self, @selector(forwardInvocation:), @selector(kj_forwardInvocation:));
});
}
- (NSMethodSignature*)kj_methodSignatureForSelector:(SEL)aSelector{
if ([self respondsToSelector:aSelector]) {
return [self kj_methodSignatureForSelector:aSelector];
}
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)kj_forwardInvocation:(NSInvocation*)anInvocation{
NSString *string = [NSString stringWithFormat:@"🍉🍉 crash:%@ 类出现未找到实例方法",NSStringFromClass([self class])];
NSString *reason = [NSStringFromSelector(anInvocation.selector) stringByAppendingString:@" 🚗🚗实例方法未找到🚗🚗"];
NSException *exception = [NSException exceptionWithName:@"没找到方法" reason:reason userInfo:@{}];
[KJCrashManager kj_crashDealWithException:exception CrashTitle:string];
}
+ (NSMethodSignature*)kj_methodSignatureForSelector:(SEL)aSelector{
if ([self respondsToSelector:aSelector]) {
return [self kj_methodSignatureForSelector:aSelector];
}
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
+ (void)kj_forwardInvocation:(NSInvocation*)anInvocation{
NSString *string = [NSString stringWithFormat:@"🍉🍉 crash:%@ 类出现未找到类方法",NSStringFromClass([self class])];
NSString *reason = [NSStringFromSelector(anInvocation.selector) stringByAppendingString:@" 🚗🚗类方法未找到🚗🚗"];
NSException *exception = [NSException exceptionWithName:@"没找到方法" reason:reason userInfo:@{}];
[KJCrashManager kj_crashDealWithException:exception CrashTitle:string];
}
@end
复制代码
重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了。
如果想调用已经重写过的方法的父类的实现,只需使用super这个编译器标识,它会在运行时跳过在当前的类对象中寻找方法的过程
高频调用方法
Runtime源码中的IMP作为函数指针,指向方法的实现。通过它我们可以绕开发送消息的过程来提高函数调用的效率
void (*test)(id, SEL, BOOL);
test = (void(*)(id, SEL, BOOL))[target methodForSelector:@selector(xxx:)];
for (int i = 0; i < 100000; i++) {
test(targetList[i], @selector(xxx:), YES);
}
复制代码
拦截调用
在方法调用中说到了,如果没有找到方法就会转向拦截调用,那么什么是拦截调用呢?
拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理。
resolveClassMethod:
当你调用一个不存在的类方法的时候,会调用这个方法,默认返回NO,你可以加上自己的处理然后返回YES。resolveInstanceMethod:
和第一个方法相似,只不过处理的是实例方法。
后两个方法需要转发到其他的类处理
forwardingTargetForSelector:
将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。forwardInvocation:
将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法。
动态添加方法
重写了拦截调用的方法并且返回了YES,我们要怎么处理呢?
根据传进来的SEL
类型的selector
动态添加一个方法
// 隐式调用一个不存在的方法
[target performSelector:@selector(xxx:) withObject:@"test"];
复制代码
然后在target对象内部重写拦截调用的方法,动态添加方法。
void testAddMethod(id self, SEL _cmd, NSString *string){
NSLog(@"xxxx");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if ([NSStringFromSelector(sel) isEqualToString:@"xxx:"]) {
class_addMethod(self, sel, (IMP)testAddMethod, "v@:*");
}
return YES;
}
复制代码
其中class_addMethod
的四个参数分别是:
Class cls
给哪个类添加方法,本例中是selfSEL name
添加的方法,本例中是重写的拦截调用传进来的selector。IMP imp
是C的方法实现可以直接获得。OC获得方法的实现+ (IMP)instanceMethodForSelector:(SEL)aSelector
"v@:*"
方法的签名,代表有一个参数的方法
动态继承
动态继承修改NSBundle
对象的isa指针使其指向子类KJLanguageManager
,便可以调用子类的方法
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
object_setClass([NSBundle mainBundle], [KJLanguageManager class]);
});
}
复制代码
关联对象
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
现有这样一个需求:系统的类并不能满足你的需求,你需要额外添加一个属性,这种情况的一般解决办法就是继承。但是只增加一个属性,就去继承一个类,总是觉得太麻烦。 这时就可以使用runtime的关联对象来处理
// 全局变量 - 关联对象key
static char associatedObjectKey;
// 设置关联对象
objc_setAssociatedObject(target, &associatedObjectKey, @"关联测试", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 获取关联对象
NSString *string = objc_getAssociatedObject(target, &associatedObjectKey);
NSLog(@"----:%@", string);
复制代码
objc_setAssociatedObject
的四个参数:
id object
给谁设置关联对象。const void *key
关联对象唯一的key,获取时会用到。id value
关联对象。objc_AssociationPolicy
关联策略,有以下几种策略:
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
复制代码
其实,你还可以把添加和获取关联对象的方法写在类别中,方便使用。
// 获取关联对象
- (CGFloat)timeInterval{
return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
// 添加关联对象
- (void)setTimeInterval:(CGFloat)timeInterval{
objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_ASSIGN);
}
复制代码
注意:这里面我们把timeInterval
方法的地址作为唯一的key,_cmd
代表当前调用方法的地址。
方法交换
方法交换,顾名思义,就是将两个方法的实现交换
原理是:通过Runtime获取到方法实现的地址,进而动态交换两个方法的功能
交换实例方法
void kExceptionMethodSwizzling(Class clazz, SEL original, SEL swizzled){
Method originalMethod = class_getInstanceMethod(clazz, original);
Method swizzledMethod = class_getInstanceMethod(clazz, swizzled);
if (class_addMethod(clazz, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(clazz, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
复制代码
交换类方法
void kExceptionClassMethodSwizzling(Class clazz, SEL original, SEL swizzled){
Method originalMethod = class_getClassMethod(clazz, original);
Method swizzledMethod = class_getClassMethod(clazz, swizzled);
Class metaclass = objc_getMetaClass(NSStringFromClass(clazz).UTF8String);
if (class_addMethod(metaclass, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(metaclass, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
复制代码
方法交换对于我来说更像是实现一种思想的最佳技术:AOP面向切面编程
交换完再调回自己,要保证只交换一次,否则会乱套
例如,将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之调用B方法时候执行A方法
下面是一个数组越界的runtime实现:
// 调用原方法以及新方法进行交换,处理崩溃问题。
+ (void)load {
// 利用GCD只执行一次,防止多线程问题
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 获得不可变数组objectAtIndex的selector
SEL A_sel = @selector(objectAtIndex:);
// 自己实现的将要被交换的方法的selector
SEL B_sel = @selector(kj_objectAtIndex:);
// 两个方法的Method
Method A_Method = class_getInstanceMethod(objc_getClass("__NSArrayI"), A_sel);
// 自己实现的将要被交换的方法的selector
Method B_Method = class_getInstanceMethod(objc_getClass("__NSArrayI"), B_sel);
// 首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
BOOL isAdd = class_addMethod(self, A_sel, method_getImplementation(B_Method), method_getTypeEncoding(B_Method));
if (isAdd) {
// 如果成功,说明类中不存在这个方法的实现
// 将被交换方法的实现替换到这个并不存在的实现
class_replaceMethod(self, B_sel, method_getImplementation(A_Method), method_getTypeEncoding(A_Method));
}else{
// 否则,交换两个方法的实现
method_exchangeImplementations(A_Method, B_Method);
}
});
}
- (instancetype)kj_objectAtIndex:(NSUInteger)index{
NSArray *temp = nil;
@try {
temp = [self kj_objectAtIndex:index];
}@catch (NSException *exception) {
NSString *string = @"🍉🍉 crash:";
if (self.count == 0) {
string = [string stringByAppendingString:@"数组个数为零"];
}else if (self.count <= index) {
string = [string stringByAppendingString:@"数组索引越界"];
}
[KJCrashManager kj_crashDealWithException:exception CrashTitle:string];
}@finally {
return temp;
}
}
复制代码
备注:这里内部调用了temp = [self kj_objectAtIndex:index];
,看上去有点像递归死循环,其实不是,这里正因为交换了方法,其实是调用的原始方法objectAtIndex:
再举个例子:在CollectionView上面移动Item并且不影响正常CollectionView的左右滑动,这时就可以交换获取到Touch事件,然后以回调的方式传递出来,那么换做是你,你会采取怎么样的方式来处理呢?
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self kj_swizzleMethod:@selector(touchesBegan:withEvent:) Method:@selector(kj_touchesBegan:withEvent:)];
[self kj_swizzleMethod:@selector(touchesMoved:withEvent:) Method:@selector(kj_touchesMoved:withEvent:)];
[self kj_swizzleMethod:@selector(touchesEnded:withEvent:) Method:@selector(kj_touchesEnded:withEvent:)];
[self kj_swizzleMethod:@selector(touchesCancelled:withEvent:) Method:@selector(kj_touchesCancelled:withEvent:)];
});
}
- (void)kj_touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event{
if (self.kOpenExchange && self.moveblock) {
CGPoint point = [[touches anyObject] locationInView:self];
self.moveblock(KJMoveStateTypeBegin,point);
}
[self kj_touchesBegan:touches withEvent:event];
}
- (void)kj_touchesMoved:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event{
if (self.kOpenExchange && self.moveblock) {
CGPoint point = [[touches anyObject] locationInView:self];
self.moveblock(KJMoveStateTypeMove,point);
}
[self kj_touchesMoved:touches withEvent:event];
}
- (void)kj_touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event{
if (self.kOpenExchange && self.moveblock) {
CGPoint point = [[touches anyObject] locationInView:self];
self.moveblock(KJMoveStateTypeEnd,point);
}
[self kj_touchesEnded:touches withEvent:event];
}
- (void)kj_touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event{
if (self.kOpenExchange && self.moveblock) {
CGPoint point = [[touches anyObject] locationInView:self];
self.moveblock(KJMoveStateTypeCancelled,point);
}
[self kj_touchesEnded:touches withEvent:event];
}
复制代码