/// 属性列表 @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
给哪个类添加方法,本例中是self
SEL 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]; }