runtime的一些小妙用

用法一: Unrecognized Selector类型crash防护(Unrecognized Selector)

先介绍下class_addMethod这个方法:
/**
     Class _Nullable cls: 你要添加方法的那个类;
     SEL _Nonnull name:name都说可以随便取, 但有些场景随便取会有问题(如下例:),一些取添加或替换方法的名称SEL;
     IMP _Nonnull imp:新方法的 IMP;
     const char * _Nullable types:新方法的返回值及参数
     */
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

开发中经常会遇到这个问题------“unrecognized selector sent to instance 0x7faa2a132c0” 而导致cash. 为了防止crash我们可以用class_addMethod给找不到对应方法即将crash的消息添加一个与之对应的方法来防止其因为找不到相应方法而crash.

在OC中找不到对相应的实现方法时, 有补救机制 即 会先调用动态决议方法 该方法解决不了问题 再调用重定向方法; 若都解决不了再 cash.

动态决议方法:(这是给类利用class_addMethod添加函数的机会...)

  • (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
  • (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

重定向方法:

  • (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
  • (void)forwardInvocation:(NSInvocation *)anInvocation;
补救机制(拦截调用)的整个流程即Objective——C的消息转发机制。其具体流程如下图:
image.png

下面上代码: 给一个类的对象调用一个未实现的方法 然后用runtime 在动态决议方法中为其添加实现, 防止crash

/* Person 类 */
#import "Person.h"
#import <objc/runtime.h>

@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(sel));   //  打印: eat

    Method addMethod = class_getInstanceMethod([self class], @selector(addMethod));
    // 这里 SEL _Nonnull name 必须写 sel, 否则还是会cash...
    class_addMethod([self class], sel, method_getImplementation(addMethod), method_getTypeEncoding(addMethod));
    
    return true;
}
/*  
*  这个方法也可以解决cash问题, 意思是转给新的对象去执行这个方法...
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector: %@",NSStringFromSelector(aSelector));
    
    if (aSelector == @selector(eat)) {
        // 创建新的对象
        StubProxyObject * stub = [[StubProxyObject alloc] init];
        return stub;
    }
    
    return [super forwardingTargetForSelector:@selector(addMethod)];
}
*/
- (void) addMethod {
    NSLog(@"addMethod");
}
@end
// 调用 Person未实现方法eat
Person * person = [[Person alloc] init];
[person performSelector:@selector(eat)];

添加后就不会cash;

用法二: 替换系统的方法:

代码示例: 创建 UIViewController 分类, 然后替换 - (void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion;

#import "UIViewController+Extention.h"
#import <objc/runtime.h>

@implementation UIViewController (Extention)

+ (void)load {
    Class class = [self class];
    
    // 保证方法替换只执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originSelector = @selector(dismissViewControllerAnimated:completion:);
        SEL swizzledSelector = @selector(customDismissViewController);
        
        Method oringinMethod = class_getInstanceMethod(class, originSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        IMP swizzledIMP = method_getImplementation(swizzledMethod);
        const char * type = method_getTypeEncoding(swizzledMethod);
        
        BOOL didAddMethod = class_addMethod(class, originSelector, swizzledIMP, type);
        
        if (didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(oringinMethod), method_getTypeEncoding(oringinMethod));
        } else {
            method_exchangeImplementations(oringinMethod, swizzledMethod);
        }
    });
}
- (void) customDismissViewController {
    NSLog(@"customDismissViewController");
    //  这里调用不会有 死循环....
    [self customDismissViewController];
}

还可以写为:

+ (void)load {
    Class class = [self class];

    SEL originSelector = @selector(dismissViewControllerAnimated:completion:);
    SEL swizzledSelector = @selector(customDismissViewController);

    Method originMethod = class_getInstanceMethod(class, originSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

    // 确保两个方法都得获取到
    if (!originMethod || !swizzledMethod) {
        return;
    }
        // 交换跟上面情况一样
//    method_exchangeImplementations(originMethod, swizzledMethod);
    
    //  这种只是作替换,既自定义方法里 不能再调用  [self customDismissViewController];
    class_replaceMethod(class, originSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
//    class_replaceMethod(class, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}
- (void) customDismissViewController {
    NSLog(@"customDismissViewController");
   // 这里不能调, 不然会产生死循环...
//    [self customDismissViewController];
}

应用场景: 如何通过不去手动修改每个UIImage的imageNamed:方法就可以实现为该方法中加入版本判断语句? 可以为 UIImage建一个分类(UIImage+Category), 替换系统的 imageNamed: 方法; 注意: 替换方法后,最后要调用一下自己定义替换的方法, 让其有加载图片的功能...

用法三: 在不同类之间实现Method Swizzling

示例: Person类有一个实例方法 - (void)run:(CGFloat)speed, 目前需要Hook该方法对速度大于20才执行 run, 利用另一个类的方法交换来实现:

Person类

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface Person : NSObject

- (void) run: (CGFloat)speed;

@end

#import "Person.h"
#import <objc/runtime.h>

@implementation Person

- (void) run: (CGFloat)speed {
    NSLog(@"person --- %f",speed);
}

@end

StubProxyObject 类

#import "StubProxyObject.h"
#import <UIKit/UIKit.h>
#import <objc/runtime.h>

@implementation StubProxyObject

+(void)load {
    Class originClass = NSClassFromString(@"Person");
    Class swizzledClass = [self class];
    
    SEL originSelector = NSSelectorFromString(@"run:");
    SEL swizzledSelector = @selector(stub_run:);
    
    Method originMethod = class_getInstanceMethod(originClass, originSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
    
    // 向Person类中新添加 stub_run: 方法
    BOOL addMethod = class_addMethod(originClass, swizzledSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
   
    if (!addMethod) {
        return;
    }
    
    // 获取当前 Person 中新添加的方法 stub_run:的Method指针
    Method newSwizzledMethod = class_getInstanceMethod(originClass, swizzledSelector);
    if (!newSwizzledMethod) {
        return;
    }
    
    BOOL didAddMethod = class_addMethod(originClass, originSelector, method_getImplementation(newSwizzledMethod), method_getTypeEncoding(newSwizzledMethod));
   
    if (didAddMethod) {
        class_replaceMethod(originClass, swizzledSelector , method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    } else {
        method_exchangeImplementations(originMethod, newSwizzledMethod);
    }
}

- (void) stub_run: (CGFloat) merter {
    NSLog(@"StubProxyObject --- stub_run");
    if (merter > 20) {
        [self stub_run:merter];
    }
}

@end

然后在其它地方 Person 对象调用 run: 方法时:

Person * person = [Person new];
[person run:30];

控制台会打印:
StubProxyObject --- stub_run
person --- 30.000000

用法四: 给分类添加属性:

我们知道, 分类中是无法设置属性的,如果在分类的声明中写@property, 能为其生成get 和 set 方法的声明,但无法生成成员变量,就是说虽然点语法能调用出来,但程序执行后会crash...

示例: 给NSObject添加分类(NSObject+Category)设置属性

#import <Foundation/Foundation.h>

@interface NSObject (Extention)

@property (nonatomic, strong) NSString * name;

@end

#import "NSObject+Extention.h"
#import <objc/runtime.h>

static char * const nameKey = "nameKey";

@implementation NSObject (Extention)

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, nameKey);
}
@end

用法五: 利用runtime进行解归档操作

首先得知道三个方法:

  • 获得某个类的所有成员变量(outCount 会返回成员变量的总数)
Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
  • 获得成员变量的名字
const char *ivar_getName(Ivar v)
  • 获得成员变量的类型
const char *ivar_getTypeEndcoding(Ivar v)
利用runtime 获取所有属性来重写归档解档方法(对于类中属性比较多时, 用runtime来解归档比较方便)
// 设置不需要归解档的属性
- (NSArray *)ignoredNames {
    return @[@"_aaa",@"_bbb",@"_ccc"];
}
// 归档调用方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
     // 获取所有成员变量
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i];
        // 获得成员变量的名字
        const char * name = ivar_getName(ivar);
        // 将每个成员变量名转换为NSString对象类型
        NSString *key = [NSString stringWithUTF8String:name];
        
        // 忽略不需要归档的属性
        if ([[self ignoredNames] containsObject:key]) {
            continue;
        }
        
        // 通过成员变量名,取出成员变量的值
        id value = [self valueForKeyPath:key];
        // 再将值归档
        [aCoder encodeObject:value forKey:key];
        // 这两步就相当于 [aCoder encodeObject:@(self.age) forKey:@"_age"];
    }
    free(ivars);
}
// 解档方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super initWithCoder:aDecoder]) {
        // 获取所有成员变量
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            // 获得成员变量的名字
            const char * name = ivar_getName(ivar);
            // 将每个成员变量名转换为NSString对象类型
            NSString *key = [NSString stringWithUTF8String:name];
            
            // 忽略不需要解档的属性
            if ([[self ignoredNames] containsObject:key]) {
                continue;
            }
            
            // 根据变量名解档取值,无论是什么类型
            id value = [aDecoder decodeObjectForKey:key];
            // 取出的值再设置给属性
            [self setValue:value forKey:key];
            // 这两步就相当于以前的 self.age = [aDecoder decodeObjectForKey:@"_age"];
        }
        free(ivars);
    }
    return self;
}

若项目中解归档的类比较多时, 就可以考虑 NSObject 分类来写上述逻辑了....

NSObject+Extension.h

#import <Foundation/Foundation.h>

@interface NSObject (Extention)

@property (nonatomic, strong) NSString * name;

- (NSArray *)ignoredNames;
- (void)encode:(NSCoder *)aCoder;
- (void)decode:(NSCoder *)aDecoder;

@end

NSObject+Extention.m

#import "NSObject+Extension.h"
#import <objc/runtime.h>

@implementation NSObject (Extension)

// 归档调用方法
- (void)encode:(NSCoder *)aCoder {
    // 一层层父类往上查找,对父类的属性执行归解档方法
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // 如果有实现该方法再去调用
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) continue;
            }
            
            id value = [self valueForKeyPath:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
        c = [c superclass];
    }
}

//  解档方法
- (void)decode:(NSCoder *)aDecoder {
    // 一层层父类往上查找,对父类的属性执行归解档方法
    Class c = [self class];
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(c, &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // 如果有实现该方法再去调用
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) continue;
            }
            
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivars);
        c = [c superclass];
    }
    
}
@end

在需要归解档的对象中实现下面方法即可:

// 设置需要忽略的属性
- (NSArray *)ignoredNames {
    return @[@"_aaa"];
}

// 在系统方法内来调用我们的方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        [self decode:aDecoder];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [self encode:aCoder];
}

然而多个类需要解归档时, 上面的代码还是重复的, 所以我们直接可以定义个宏, 在需要的类里直接一句宏就搞定了,这也是MJExtention里一句宏搞定解归档的实现原理;

在 NSObject+Extension.h 里, 我们定义一个宏:

#define YQCodingImplementation \
-(void)encodeWithCoder:(NSCoder *)aCoder\
{\
[self encode:aCoder];\
}\
-(instancetype)initWithCoder:(NSCoder *)aDecoder\
{\
if (self = [super init]) {\
[self decode:aDecoder];\
}return self; \
}

然后在需要的类里:

#import "Person.h"
#import "NSObject+HZCoding.h"

@implementation Person

// 归档  解档 , 一句宏就可以了...
YQCodingImplementation

@end

用法六: 利用runtime 获取所有属性来进行字典转模型

可以看参考文章, 里面写的比较详细了....
参考文章: https://www.jianshu.com/p/ab966e8a82e2
http://www.cocoachina.com/ios/20161102/17920.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,692评论 0 9
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,349评论 8 265
  • 引导 对于从事 iOS 开发人员来说,所有的人都会答出「 Runtime 是运行时 」,什么情况下用 Runtim...
    Winny_园球阅读 4,199评论 3 75
  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,700评论 7 64
  • 亲爱的妞,今天的你好累好难过的样子,4:45下课回来就一副疲惫不堪的神情,见到妈妈就说喉咙不舒服,妈妈看着好心疼。...
    LianaLL阅读 186评论 0 0