/// 属性列表
@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];
}
Runtime介绍就到此完毕,后面有相关再补充,写文章不容易,还请点个**小星星**传送门