iOS runtime的使用场景-实战篇

173709-2be9edadc94d5cbb.png
本文参考pingpong的iOS runtime 的使用场景--实战篇非常感谢该作者
1.背景知识
1.1 OC的方法调用流程

下面以实例对象调用方法[blackDog walk]为例描述方法调用的流程:

1、编译器会把`[blackDog walk]`转化为`objc_msgSend(blackDog,SEL)`,SEL为@selector(walk)。

2、Runtime会在blackDog对象所对应的Dog类的方法缓存列表里查找方法的SEL

3、如果没有找到,则在Dog类的方法分发表查找方法的SEL。(类由对象isa指针指向,方法分发表即methodList)

4、如果没有找到,则在其父类(设Dog类的父类为Animal类)的方法分发表里查找方法的SEL(父类由类的superClass指向)

5、如果没有找到,则沿继承体系继续下去,最终到达NSObject类。

6、如果在234的其中一步中找到,则定位了方法实现的入口,执行具体实现

7、如果最后还是没有找到,会面临两种情况:``(1) 如果是使用`[blackDog walk]`的方式调用方法````(2) 使用`[blackDog performSelector:@selector(walk)]`的方式调用方法
1.2 消息转发流程
1、动态方法解析
接收到未知消息时(假设blackDog的walk方法尚未实现),runtime会调用+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)

2、备用接收者
如果以上方法没有做处理,runtime会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法。
如果该方法返回了一个非nil(也不能是self)的对象,而且该对象实现了这个方法,那么这个对象就成了消息的接收者,消息就被分发到该对象。
适用情况:通常在对象内部使用,让内部的另外一个对象处理消息,在外面看起来就像是该对象处理了消息。
比如:blackDog让女朋友whiteDog来接收这个消息

3、完整消息转发
在- (void)forwardInvocation:(NSInvocation *)anInvocation方法中选择转发消息的对象,其中anInvocation对象封装了未知消息的所有细节,并保留调用结果发送到原始调用者。
比如:blackDog将消息完整转发給主人dogOwner来处理
173709-7bcc4302c515f1e0.png
2.成员变量和属性
2.1 json->model
  • 原理描述:用runtime提供的函数遍历Model自身所有属性,如果属性在json中有对应的值,则将其赋值。
  • 核心方法:在NSObject的分类中添加方法
- (instancetype)initWithDict:(NSDictionary *)dict {

    if (self = [self init]) {
        //(1)获取类的属性及属性对应的类型
        NSMutableArray * keys = [NSMutableArray array];
        NSMutableArray * attributes = [NSMutableArray array];
        /*
         * 例子
         * name = value3 attribute = T@"NSString",C,N,V_value3
         * name = value4 attribute = T^i,N,V_value4
         */
        unsigned int outCount;
        objc_property_t * properties = class_copyPropertyList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            objc_property_t property = properties[i];
            //通过property_getName函数获得属性的名字
            NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            //通过property_getAttributes函数可以获得属性的名字和@encode编码
            NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
            [attributes addObject:propertyAttribute];
        }
        //立即释放properties指向的内存
        free(properties);

        //(2)根据类型给属性赋值
        for (NSString * key in keys) {
            if ([dict valueForKey:key] == nil) continue;
            [self setValue:[dict valueForKey:key] forKey:key];
        }
    }
    return self;
}
2.2 一键序列化
  • 原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。
  • 核心方法:在Model的基类中重写方法:
- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        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)];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    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)];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
}
2.3 访问私有变量

我们知道,OC中没有真正意义上的私有变量和方法,要让成员变量私有,要放在m文件中声明,不对外暴露。如果我们知道这个成员变量的名称,可以通过runtime获取成员变量,再通过getIvar来获取它的值。方法:

Ivar ivar = class_getInstanceVariable([Model class], "_str1");
NSString * str1 = object_getIvar(model, ivar);
3. 关联对象

如何給NSArray添加一个属性(不能使用继承)
iOS runtime实战应用:关联对象

  • 问题
    OC的分类允许给分类添加属性,但不会自动生成getter、setter方法
    所以常规的仅仅添加之后,调用的话会crash
  • runtime如何关联对象
//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)
  • 应用,如何关联:
- (void)setCustomTabbar:(UIView *)customTabbar {
    //这里使用方法的指针地址作为唯一的key
    objc_setAssociatedObject(self, @selector(customTabbar), customTabbar, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIView *)customTabbar {
    return objc_getAssociatedObject(self, @selector(customTabbar));
}
4.Method Swizzling
  • Method Swizzling原理
    每个类都维护一个方法(Method)列表,Method则包含SEL和其对应IMP的信息,方法交换做的事情就是把SEL和IMP的对应关系断开,并和新的IMP生成对应关系
+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        Class selfClass = object_getClass([self class]);

        SEL oriSEL = @selector(imageNamed:);
        Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);

        SEL cusSEL = @selector(myImageNamed:);
        Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);

        BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
        if (addSucc) {
            class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else {
            method_exchangeImplementations(oriMethod, cusMethod);
        }

    });
}
  • 自己使用过例子:
    使用场景,在iOS7中如果viewdidappear还没有完成,就立刻执行push或者pop操作会crash。
  • 解决方案
    就是利用method swizzing, 将系统的viewdidappear替换为自己重写的sofaViewDidAppear
@implementation UIViewController (APSafeTransition)

+ (void)load
{
    Method m1;
    Method m2;

    m1 = class_getInstanceMethod(self, @selector(sofaViewDidAppear:));
    m2 = class_getInstanceMethod(self, @selector(viewDidAppear:));
    method_exchangeImplementations(m1, m2);
}
5.查看私有成员变量
// 查看私有成员变量
- (void)checkPrivityVariable {
    unsigned int count;
    Ivar *ivars = class_copyIvarList([UITextField class], &count);
    for (int i = 0; i < count; i++) {
        // 取出i位置的成员变量
        Ivar ivar = ivars[i];
        NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
    }
    free(ivars);
}

运行结果

image.png
5.1 设置UITextField占位文字的颜色
// 设置TextField占位符文字颜色
- (void)setTextFieldPlaceholderTextColor {
    UITextField *textF = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 200, 25)];
    textF.layer.borderWidth = 1;
    textF.layer.borderColor = [UIColor blackColor].CGColor;
    textF.center = CGPointMake(self.view.bounds.size.width * 0.5, 150);
    textF.placeholder = @"请输入用户名";
    [self.view addSubview:textF];
    
    // 法一
//    [textF setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
    
    // 法二
//    UILabel *placeholderLbe = [textF valueForKeyPath:@"_placeholderLabel"];
//    placeholderLbe.textColor = [UIColor redColor];
    
    // 法三
    NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
    attrs[NSForegroundColorAttributeName] = [UIColor redColor];
    textF.attributedPlaceholder =  [[NSMutableAttributedString alloc] initWithString:@"请输入用户名" attributes:attrs];
}

运行结果

image.png
6 字典转模型
// 建立NSObject的一个分类
#import <Foundation/Foundation.h>

@interface NSObject (Json)
+ (instancetype)cs_objectWithJson:(NSDictionary *)json;
@end
#import "NSObject+Json.h"
#import <objc/runtime.h>

@implementation NSObject (Json)

+ (instancetype)cs_objectWithJson:(NSDictionary *)json {
    id obj = [[self alloc] init];
    
    unsigned int count;
    Ivar *ivars = class_copyIvarList(self, &count);
    for (int i = 0; i < count; i++) {
        // 取出位置的成员变量
        Ivar ivar = ivars[i];
        NSMutableString *name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];
        [name deleteCharactersInRange:NSMakeRange(0, 1)];
        
        // 设值
        id value = json[name];
        if ([name isEqualToString:@"ID"]) {
            value = json[@"id"];
        }
        [obj setValue:value forKey:name];
    }
    free(ivars);
    return obj;
}

@end

字典转模型调用

#import <Foundation/Foundation.h>

@interface CSPersion : NSObject

@property (assign, nonatomic) int ID;
@property (assign, nonatomic) int weight;
@property (assign, nonatomic) int age;
@property (copy, nonatomic) NSString *name;

@end
// 字典转模型
- (void)jsonToModel {
    // 字典转模型
    NSDictionary *json = @{
                           @"id" : @20,
                           @"age" : @20,
                           @"weight" : @60,
                           @"name" : @"Jack",
                           @"no" : @30
                           };
    CSPersion *persion = [CSPersion cs_objectWithJson:json];
    NSLog(@"id = %d, age = %d, weight = %d, name = %@",persion.ID,persion.age,persion.weight,persion.name);
}

运行结果

image.png
7 替换方法实现

CSCar.h和CSCar.m文件如下

#import <Foundation/Foundation.h>
@interface CSCar : NSObject
- (void)run;
- (void)test;
@end
#import "CSCar.h"
@implementation CSCar
- (void)run {
    NSLog(@"%s", __func__);
}
- (void)test {
    NSLog(@"%s", __func__);
}
@end
6.3.1 法一
// 该方法要写在@@implementation实现外面
void myRun() {
    NSLog(@"---myRun---");
}

// 替换方法的实现
- (void)replaceMethod {
    // 方法一
    CSCar *car = [[CSCar alloc] init];
    class_replaceMethod([CSCar class], @selector(run), (IMP)myRun, "V");

    [car run];
}

运行结果

image.png
6.3.2 法二
// 替换方法的实现
- (void)replaceMethod {
    CSCar *car = [[CSCar alloc] init];
    
    // 方法二
    class_replaceMethod([CSCar class], @selector(run), imp_implementationWithBlock(^{
        NSLog(@"123123");
    }), "v");
    
    [car run];
}

运行结果

image.png
6.3.3 法三
// 替换方法的实现
- (void)replaceMethod {
    CSCar *car = [[CSCar alloc] init];

    // 方法三
    Method runMethod = class_getInstanceMethod([CSCar class], @selector(run));
    Method testMethod = class_getInstanceMethod([CSCar class], @selector(test));
    method_exchangeImplementations(runMethod, testMethod);
    
    [car run];
}

运行结果

image.png
7.让你快速上手一个项目
  • 对于一个大项目而言,最烦恼的就是在众多界面难以找到对应的viewController,要改个东西都要花好长的时间去找对应的类。

  • 特别是当你接手一个大项目的时候,对整体的业务逻辑不熟悉,整体的架构体系不熟悉,让你修复某个页面的BUG,估计你找这个页面所对应的viewController都要找好久。

  • 思考
    能否有一种方式可以快速让你上手一个大项目?快速找到某个页面所对应的viewController ?

  • 思路
    在每一个页面出现的时候,都打印出哪个类即将出现,如下图所示

解决方案
  • 方案1

整个项目中建立一个基类的viewController,然后将项目中所有的viewController都继承于基类的viewController,然后重写基类中的viewWillAppear方法。

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSString *className = NSStringFromClass([self class]);
    NSLog(@"%@ will appear", className);
}
  • 方案2
#import "UIViewController+Swizzling.h"

#import @implementation UIViewController (Swizzling)

+ (void)load {

    //我们只有在开发的时候才需要查看哪个viewController将出现
    //所以在release模式下就没必要进行方法的交换
#ifdef DEBUG

    //原本的viewWillAppear方法
    Method viewWillAppear = class_getInstanceMethod(self, @selector(viewWillAppear:));

    //需要替换成 能够输出日志的viewWillAppear
    Method logViewWillAppear = class_getInstanceMethod(self, @selector(logViewWillAppear:));

    //两方法进行交换
    method_exchangeImplementations(viewWillAppear, logViewWillAppear);

#endif

}

- (void)logViewWillAppear:(BOOL)animated {

    NSString *className = NSStringFromClass([self class]);

    //在这里,你可以进行过滤操作,指定哪些viewController需要打印,哪些不需要打印
    if ([className hasPrefix:@"UI"] == NO) {
        NSLog(@"%@ will appear",className);
    }

    //下面方法的调用,其实是调用viewWillAppear
    [self logViewWillAppear:animated];
}

@end
优缺点分析
  • 方案1 适用于一个新项目,从零开始搭建的项目,建立一个基类controller,这种编程思想非常可取。但对于一个已经成型的项目,则方案一行不通,你总不能建议一个基类,让后将所有的controller继承的类都改成基类吧?这工程量太大,太麻烦。

  • 方案2 不论是从零开始搭建的项目,还是已经成型的项目,方案2都适用。


项目连接地址


更多有关runtime文章请看下面
iOS-runtime-API详解+使用
iOS - runtime -详解
iOS Runtime原理及使用
iOS - runtime如何通过selector找到对应的 IMP地址(分别考虑类方法和实例方法)
iOS - Runtime之面试题详解一
iOS-runtime之面试题详解二

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

推荐阅读更多精彩内容

  • runtime的文档网上多的一塌糊涂,细节原理就不讲了这里只是简单整理下,平时可能会用到的一些场景 申明:大部分代...
    pingpong_龘阅读 7,691评论 1 26
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,636评论 0 9
  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,674评论 7 64
  • 从来都抱有期待,期待明天如我所想,期待我爱你你也爱我,期待父母健康,期待所有的人正直可爱明辨是非,每一次期待落空,...
    周末保持距离阅读 215评论 0 0
  • 《查理九世》《哈利波特》《大脑门一旦》
    爱莲说Alice阅读 214评论 0 1