iOS Runtime详细介绍及实战使用(二)

简介: iOS 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 给哪个类添加方法,本例中是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获取到方法实现的地址,进而动态交换两个方法的功能

1.png


交换实例方法


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

相关文章
|
27天前
|
监控 算法 iOS开发
深入探索iOS函数调用栈:符号化与性能调优实战
在iOS开发中,理解函数调用栈对于性能调优和问题排查至关重要。函数调用栈记录了程序执行过程中的函数调用顺序,通过分析调用栈,我们可以识别性能瓶颈和潜在的代码问题。本文将分享iOS函数调用栈的基本概念、符号化过程以及如何利用调用栈进行性能调优。
35 2
|
4月前
|
JSON 搜索推荐 定位技术
打造个性化天气应用:iOS开发实战
【8月更文挑战第31天】在这篇文章中,我们将一起探索如何从零开始构建一个iOS天气应用。通过简单易懂的步骤,你将学习到如何使用Swift编程语言和苹果的开发工具Xcode来实现这个目标。我们会涉及到用户界面设计、网络编程以及数据解析等关键技能,确保你能够顺利地完成这个项目。无论你是初学者还是有一定经验的开发者,这篇文章都会带给你新的启发和收获。
|
iOS开发
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(五)(3)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(五)
160 0
|
前端开发 数据处理 iOS开发
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(五)(2)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(五)
91 0
|
iOS开发 Kotlin 容器
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(五)(1)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(五)
125 0
|
存储 缓存 前端开发
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(四)(2)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(四)
128 0
|
前端开发 Swift iOS开发
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(四)(1)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(四)
205 0
|
存储 PHP Swift
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(三)(2)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(三)
167 0
|
前端开发 iOS开发
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(三)(1)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(三)
153 0
|
iOS开发 开发者 容器
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(二)(2)
实战编程·使用SwiftUI从0到1完成一款iOS笔记App(二)
141 0