runtime知识点及项目中的应用

概论:Runtime是Objective-C中底层的一套C语言API,是一个将C语言转化为面向对象语言的拓展。OC是一种面向对象的动态语言,动态语言就是在运行时执行静态语言的编译连接的工作。OC编写的程序不能直接编译为及其读懂的机器语言,在程序运行时,须通过Runtime来转换。

而在某些在特定的场景下,运用runtime的知识,往往可以大大减少我们的工作量。并能使代码更加统一、简洁。

本文将以一下几个方面逐一讲解runtime:

目录:
一、系统方法中的应用
  1、消息机制
  2、KVO的底层实现
二、runtime常见作用
  1、动态获取成员属性
  2、动态获得成员变量
  3、动态获得实例方法
  4、动态添加方法实现
  5、方法交换
三、实际开发中的应用
  1、给控件添加block事件回调方法
  2、自定义类型自动归档、解归档
  3、数组越界crash处理

首先定义一个Dog类,本篇文章的讲解将围绕这个类展开。

@interface Dog : NSObject
{
    float _weight;
}
@property(nonatomic,copy) NSString *name;
- (void)run:(NSInteger)runLong street:(NSString *)streetName;
@end

@interface Dog ()
{
    NSInteger  _age;
}
@property(nonatomic,copy) NSString *address;
@end

@implementation Dog
- (void)run:(NSInteger)runLong street:(NSString *)streetName{
     NSLog(@"在%@跑了%li米",streetName,runLong);
}
- (void)_sleep{
     NSLog(@"%s",__func__);
}
@end

一、系统方法运用runtime

1、发送消息

在OC中,调用一个对象的方法,实际上是给对象发了一条消息,在编译Objective-C函数调用的语法时,会被翻译成一个C的函数调用:objc_msgSend()
当我们dog,run的方法时:
[dog run:100 street:@"华尔街"];
实际上会转换为:
objc_msgSend(dog, @selector(run:street:),100,@"华尔街");
注:由于xcode5.0开始苹果不建议我们使用底层的代码,所以我们需要在target->build setting->搜索msg->将YES改为NO之后import <objc/message.h>才能使用objc_msgSend方法。

2、KVO的底层实现

概述:KVO是通过"isa-swizzling"技术来实现的,当一个对象注册观察者时,这个对象的isa指针被修改指向一个中间类。
当观察A类型的对象时,在运行时会创建了一个继成自A类的NSKVONotifying_A类,且为NSKVONotifying_A重写观察属性的setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察者属性值的更改情况。
因为本文主要讲解runtime的作用以及项目中的应用,所以这里就不做过多的陈述,关于KVO和isa指针不了解的同学可以移步我的另一篇Objective-C isa指针及KVO实现原理进行学习。

二、runtime常见作用

1、动态获取成员属性
通过class_copyPropertyList获取属性列表,此方法会获得所有通过@property展开的成员属性,包括私有属性。

unsigned int count = 0;
NSMutableArray *nameArray = [NSMutableArray array];
/**
     @param class 你想要检测的类  Class类型
     @param count 数组的个数
     @return 返回c语言数组  数组里元素是  objc_property_t 类型
     */
objc_property_t *properList =  class_copyPropertyList([Dog class],&count);
for (int i = 0; i<count; i++) {
    const char *name =  property_getName(properList[i]);
    NSString *ocName = [NSString stringWithUTF8String:name];
    [nameArray addObject:ocName];
}
    NSLog(@"属性名:%@",nameArray);

2、动态获得成员变量
通过class_copyIvarList获取成员变量列表,此方法会获得所有通过@property展开的成员属性以及声明的成员变量,包括私有成员变量。

 unsigned int count = 0;
    NSMutableArray *nameArray = [NSMutableArray array];
    NSMutableArray *typeArray = [NSMutableArray array];
     /**
     @param class 你想要检测的类  Class类型
     @param count 数组的个数
     @return 返回c语言数组  数组里元素是  Ivar 类型
     */
    Ivar *vars = class_copyIvarList([Dog class], &count);
    for (int i = 0; i<count; i++) {
        
        const char *name = ivar_getName(vars[i]);
        const char *type = ivar_getTypeEncoding(vars[i]);
        NSString *ocName = [NSString stringWithUTF8String:name];
        NSString *ocType = [NSString stringWithUTF8String:type];
        [nameArray addObject:ocName];
        [typeArray addObject:ocType];
        
        /*
         类型说明:基本类型 q:NSInteger   d:double   f:float  i:int  c:BOOL
         引用类型:以字符串显示  如:NSString:"NSString"  NSArray:"NSArray"
         不同类型编码请参考:
         https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
         */
        
    }
    NSLog(@"变量名:%@-----变量类型:%@",nameArray,typeArray);

3、动态获得实例方法
通过class_copyMethodList获得实例方法列表,通过method_copyReturnType获得返回类型,通过method_copyArgumentType获得各个参数类型,代码如下

 unsigned int count = 0;
    NSMutableArray *methodArray = [NSMutableArray array];
    Method *methodList = class_copyMethodList([Dog class],&count);
    for (int i = 0 ; i<count; i++) {
        NSMutableDictionary *methodDic = [NSMutableDictionary dictionary];
        // 方法名
        SEL method = method_getName(methodList[i]);
        // 返回类型
        const char *returnTypeName = method_copyReturnType(methodList[i]);
        // 参数个数  默认会多两个  第一个接受消息的对象  第二个selector  原因:objc_msgSend(id, SEL)前两个参数
        int argumentCount = method_getNumberOfArguments(methodList[i]);
        
        methodDic[@"methodName"] = NSStringFromSelector(method);
        methodDic[@"returnType"] = [NSString stringWithUTF8String:returnTypeName];
        for (int j = 0; j<argumentCount; j++) {
            NSString *key = [NSString stringWithFormat:@"param%i",j+1];
            methodDic[key] = [NSString stringWithUTF8String:method_copyArgumentType(methodList[i], j)];
        }
        [methodArray addObject:methodDic];
        
    }
    //  @对象类型  v为void   :为selector
    NSLog(@"%@",methodArray);

4、动态添加方法实现
通过class_addMethod动态添加方法:通过[dog performSelector:@selector(haha)];执行haha的方法,因为Dog类中并没有haha方法,所以执行时会crash,我们可以通过如下代码,动态添加方法实现:

// class: 给哪个类添加方法
// SEL: 方法选择器,即调用时候的名称(只是一个编号)
// IMP: 方法的实现(函数指针)
// type: 方法类型,(返回值+隐式参数) v:void @对象->self :表示SEL->_cmd
class_addMethod([Dog class], @selector(haha),(IMP)abc, "v@:");
// 任何方法默认都有两个隐式参数,self,_cmd
void abc(id self, SEL _cmd) {
    NSLog(@"哈哈大笑");
}

5、方法交换
通过method_exchangeImplementations实现方法的交换:在Dog类的load方法里添加如下方法,实现方法的交换。(写在load方法中的原因:当类被加载到内存中是会调用,执行比较早,并且不会多次调用)

+(void)load{
     Method orginMethod = class_getInstanceMethod([Dog class], @selector(run:street:));
     Method myMethod = class_getInstanceMethod([Dog class], @selector(myRun:street:));
    method_exchangeImplementations(orginMethod, myMethod);
}

- (NSString *)myRun:(NSInteger)runLong street:(NSString *)streetName{
    NSLog(@"交换方法了");
    return nil;
}

三、实际开发中的应用

说明:runtime在开发中的应用有很多,但知识点都是大致相同,为了控制篇幅这里只给出了某个知识点的一种应用,其他的应用可自己尝试实现,如果想看其他实现的代码可以给我留言。

1、给控件添加block回调方法
知识点: 动态关联两个对象
需求:给UIButton添加点击点击事件回调的block方法
思路:给UIButton添加一个扩展方法,传入一个block,再在button的点击事件里执行block;
还可以做:给类的catagory添加属性
实现代码:

@interface UIButton (Blocks)
- (void)handleControlEvent:(UIControlEvents)event withBlock:(void (^)(UIButton *btn))block;
@end
@implementation UIButton (Blocks)
static char overviewKey;
- (void)handleControlEvent:(UIControlEvents)event withBlock:(void (^)(UIButton *btn))block {
// 关联对象方法
// object:给哪个对象添加关联
// key:被关联者的索引key
// value:被关联者
// policy:关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
    objc_setAssociatedObject(self, &overviewKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self addTarget:self action:@selector(callActionBlock:) forControlEvents:event];
}
- (void)callActionBlock:(id)sender {
// 通过key 获取关联对象
    void (^block)(UIButton *btn) = objc_getAssociatedObject(self, &overviewKey);
    if (block) {
        block(sender);
    }
}
@end

2、自定义类型自动归档、解归档
概述: 归档,持久化保存数据的一种方法。系统的类如果遵循了NSCoding协议都可以直接进行归档(如:NSString,NSArray等),但是自定义对象如果要进行归档,需要:
1、遵循NSCoding协议
2、实现解档方法:-(id)initWithCoder:(NSCoder *)aDecoder和归档方法:-(void)encodeWithCoder:(NSCoder *)aCoder

知识点: 获取一个类的成员变量
需求:一劳永逸的给自定义类型实现自动归档
思路:获取自定义类的成员变量,获取成员变量字符串,再通过KVC取值,实现归/解档。
还可以做:字典转模型、获取系统类的私有属性,进行操作。如 UITextField占位文字的颜色 [self setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
实现代码:

// 归档
- (void)encodeWithCoder:(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)];
            // 通过KVC取值
            id value = [self valueForKeyPath:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
        c = [c superclass];
    }


}

// 解档
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
    if (self =  [super init]) {
        // 遍历,对父类的属性执行解档方法
        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)];
                id value = [aDecoder decodeObjectForKey:key];
                [self setValue:value forKey:key];
            }
            free(ivars);
            c = [c superclass];
        }

    }
    return self;
}

3、数组越界crash处理
知识点:获取某个类的方法、方法交换
需求:如果数组越界,避免程序闪退
思路:首先看一下数组越界后所报的原因

数组越界

报错原因提到了"__NSArrayI"而不是"NSArray"这是因为:NSArray这个类在设计的时候采用了“抽象工厂”模式,内部是个类簇,真正起作用的是内部的:"__NSArray0"(空数组)、"__NSSingleObjectArrayI"(个数为1的数组),"__NSArrayI"(不可变数组)以及"__NSArrayM"(可变数组)。所以我们只需要给NSArray添加个category替换这几个类的objectAtIndex:方法即可。
还可以做:截获系统方法,替换成自己的方法。如:字典添加nil的crash,按钮暴力点击重复响应,字体大小适配等
代码:

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        @autoreleasepool {
            [self exchangeMethodWithClass:objc_getClass("__NSArray0") orginSelector:@selector(objectAtIndex:) customSelector:@selector(emptyObjectIndex:)];
            [self exchangeMethodWithClass:objc_getClass("__NSSingleObjectArrayI") orginSelector:@selector(objectAtIndex:) customSelector:@selector(singleArrObjectIndex:)];
            [self exchangeMethodWithClass:objc_getClass("__NSArrayI") orginSelector:@selector(objectAtIndex:) customSelector:@selector(arrObjectIndex:)];
            [self exchangeMethodWithClass:objc_getClass("__NSArrayM") orginSelector:@selector(objectAtIndex:) customSelector:@selector(mutableObjectIndex:)];
            [self exchangeMethodWithClass:objc_getClass("__NSArrayM") orginSelector:@selector(insertObject:atIndex:) customSelector:@selector(mutableInsertObject:atIndex:)];
        }
    });
}

+ (void)exchangeMethodWithClass:(Class)class orginSelector:(SEL)orginS customSelector:(SEL)customS{
    Method orginMethod = class_getInstanceMethod(class, orginS);
    Method customMethod = class_getInstanceMethod(class, customS);
    method_exchangeImplementations(orginMethod, customMethod);
}

- (id)emptyObjectIndex:(NSInteger)index{
    return nil;
}

- (id)singleArrObjectIndex:(NSInteger)index{
    @autoreleasepool {
        if (index >= self.count || index < 0) {
            return nil;
        }
        return [self singleArrObjectIndex:index];
    }
}

- (id)arrObjectIndex:(NSInteger)index{
    @autoreleasepool {
        if (index >= self.count || index < 0) {
            return nil;
        }
        return [self arrObjectIndex:index];
    }
}

- (id)mutableObjectIndex:(NSInteger)index{
    @autoreleasepool {
        if (index >= self.count || index < 0) {
            return nil;
        }
        return [self mutableObjectIndex:index];
    }
}

- (void)mutableInsertObject:(id)object atIndex:(NSUInteger)index{
    @autoreleasepool {
        if (object) {
            [self mutableInsertObject:object atIndex:index];
        }
    }
}
- (id)myObjectAtIndex:(NSUInteger)index
{
    @autoreleasepool {
        if (index < self.count) {
            return [self myObjectAtIndex:index];
        } else {
            return nil;
        }
    }
}

到这里本篇文章基本结束,文中所涉及代码都在这里
最后:喜欢我文章的可以多多点赞和关注,您的鼓励是我写作的动力。O(∩_∩)O~

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

推荐阅读更多精彩内容