iOS-Runtime-实践篇

前言

首先, 如果不太了解Runtime的原理的, 可以去我的上一篇文章里先了解了解iOS-Runtime-原理篇

其次, 所有runtime代码都是基于C的函数, 所以要用到runtime的函数必须导入

#import <objc/objc-runtime.h> // 模拟器
或者
#import <objc/runtime.h> // 真机
#import <objc/message.h> // 真机

然后, 以下所有代码都可以在我的Github上面下载, 大家觉得有帮助的希望可以给个star.


目录

  1. 动态添加一个类
  2. 打印一个类的所有ivar, property 和 method
  3. 给分类增加属性
  4. 动态添加方法实现
  5. 更换方法调用者
  6. 更改特定方法的实现

1. 动态添加一个类

就像KVO一样, 系统是在程序运行的时候根据你要监听的类, 动态添加一个新类继承自该类, 然后重写原类的setter方法并在里面通知observer的.

那么, 如何动态添加一个类呢? show code~

// 创建一个类(size_t extraBytes该参数通常指定为0, 该参数是分配给类和元类对象尾部的索引ivars的字节数。)
Class clazz = objc_allocateClassPair([NSObject class], "GoodPerson", 0);
    
// 添加ivar
// @encode(aType) : 返回该类型的内部表示字符串, 如@encode(int) -> i
class_addIvar(clazz, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
    
class_addIvar(clazz, "_age", sizeof(NSUInteger), log2(sizeof(NSUInteger)), @encode(NSUInteger));
    
// 注册该类
objc_registerClassPair(clazz);
    
// 创建实例对象
id object = [[clazz alloc] init];
    
// 设置ivar
[object setValue:@"Tracy" forKey:@"name"];
    
Ivar ageIvar = class_getInstanceVariable(clazz, "_age");
object_setIvar(object, ageIvar, @18);
    
// 打印对象的类和内存地址
NSLog(@"%@", object);
    
// 打印对象的属性值
NSLog(@"name = %@, age = %@", [object valueForKey:@"name"], object_getIvar(object, ageIvar));
    
// 当类或者它的子类的实例还存在,则不能调用objc_disposeClassPair方法
object = nil;
     
// 销毁类
objc_disposeClassPair(clazz);

运行结果为
2016-09-04 17:04:08.328 Runtime-实践篇[13699:1043458] <GoodPerson: 0x1002039b0>
2016-09-04 17:04:08.329 Runtime-实践篇[13699:1043458] name = Tracy, age = 18

这样, 我们就在程序运行时动态添加了一个继承自NSObject的GoodPerson类, 并为该类添加了name和age成员变量. 这里我们需要注意的是, 添加成员变量的class_addIvar方法必须要在objc_allocateClassPairobjc_registerClassPair之间调用才行, 这里涉及到OC中类的成员变量的偏移量, 如果在类注册之后再addIvar的话会破坏原来类成员变量正确的偏移量, 这样的话会导致你访问的那个成员变量并不是你想访问的成员变量, 如图 :

在类中新增另一个实例变量前后的数据布局图

大家可以试试把class_addIvar方法放在objc_registerClassPair方法之后执行, 看看会发生什么? (用KVC赋值和取值直接报错, 用getIvar的话取值为null)

2. 打印一个类的所有ivar, property 和 method

这个还是比较简单的, 应该直接看代码都能看懂

Person *p = [[Person alloc] init];
[p setValue:@"Kobe" forKey:@"name"];
[p setValue:@18 forKey:@"age"];
//    p.address = @"广州大学城";
p.weight = 110.0f;
    
// 1.打印所有ivars
unsigned int ivarCount = 0;
// 用一个字典装ivarName和value
NSMutableDictionary *ivarDict = [NSMutableDictionary dictionary];
Ivar *ivarList = class_copyIvarList([p class], &ivarCount);
for(int i = 0; i < ivarCount; i++){
    NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivarList[i])];
    id value = [p valueForKey:ivarName];    
    if (value) {
        ivarDict[ivarName] = value;
    } else {
        ivarDict[ivarName] = @"值为nil";
    }
}
// 打印ivar
for (NSString *ivarName in ivarDict.allKeys) {
    NSLog(@"ivarName:%@, ivarValue:%@",ivarName, ivarDict[ivarName]);
}
    
// 2.打印所有properties
unsigned int propertyCount = 0;
// 用一个字典装propertyName和value
NSMutableDictionary *propertyDict = [NSMutableDictionary dictionary];
objc_property_t *propertyList = class_copyPropertyList([p class], &propertyCount);
for(int j = 0; j < propertyCount; j++){
    NSString *propertyName = [NSString stringWithUTF8String:property_getName(propertyList[j])];
    id value = [p valueForKey:propertyName];
        
    if (value) {
        propertyDict[propertyName] = value;
    } else {
        propertyDict[propertyName] = @"值为nil";
    }
}
// 打印property
for (NSString *propertyName in propertyDict.allKeys) {
    NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyDict[propertyName]);
}
    
// 3.打印所有methods
unsigned int methodCount = 0;
// 用一个字典装methodName和arguments
NSMutableDictionary *methodDict = [NSMutableDictionary dictionary];
Method *methodList = class_copyMethodList([p class], &methodCount);
for(int k = 0; k < methodCount; k++) {
    SEL methodSel = method_getName(methodList[k]);
    NSString *methodName = [NSString stringWithUTF8String:sel_getName(methodSel)];
        
unsigned int argumentNums = method_getNumberOfArguments(methodList[k]);
        
methodDict[methodName] = @(argumentNums - 2); // -2的原因是每个方法内部都有self 和 selector 两个参数
}
// 打印method
for (NSString *methodName in methodDict.allKeys) {
    NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodDict[methodName]);
}

打印结果为 : 
2016-09-04 17:06:49.070 Runtime-实践篇[13723:1044813] ivarName:_name, ivarValue:Kobe
2016-09-04 17:06:49.071 Runtime-实践篇[13723:1044813] ivarName:_age, ivarValue:18
2016-09-04 17:06:49.071 Runtime-实践篇[13723:1044813] ivarName:_weight, ivarValue:110
2016-09-04 17:06:49.072 Runtime-实践篇[13723:1044813] ivarName:_address, ivarValue:值为nil
2016-09-04 17:06:49.072 Runtime-实践篇[13723:1044813] propertyName:address, propertyValue:值为nil
2016-09-04 17:06:49.072 Runtime-实践篇[13723:1044813] propertyName:weight, propertyValue:110
2016-09-04 17:06:49.073 Runtime-实践篇[13723:1044813] methodName:setWeight:, argumentsCount:1
2016-09-04 17:06:49.073 Runtime-实践篇[13723:1044813] methodName:weight, argumentsCount:0
2016-09-04 17:06:49.074 Runtime-实践篇[13723:1044813] methodName:setAddress:, argumentsCount:1
2016-09-04 17:06:49.074 Runtime-实践篇[13723:1044813] methodName:address, argumentsCount:0
2016-09-04 17:06:49.074 Runtime-实践篇[13723:1044813] methodName:.cxx_destruct, argumentsCount:0

前面2节主要是熟悉runtime的函数调用, 毕竟有许多函数前缀objc, class, object等等. 其实这里面也有规律 :

  • objc_ : 高于类的操作, 例如添加类, 注册类, 销毁类还有许多高于一个类本身的操作一般都是objc开头
  • class : 对类的内部进行修改的, 例如添加ivar, 添加property, 添加method等等
  • object : 对某个对象进行修改, 例如设置ivar值, 获取ivar值, 设置property值, 获取property值, 调用某个method等等
  • ivar, property, method : 这三个方法大家可以手动去敲敲看一看

3. 给分类增加属性

在分类只能对原类扩充方法, 并不能扩充属性, 你可以创建一个分类, 然后在分类中敲几个@property, 然后用第二节的方法打印下原类的property看看存不存在? 答案显然是不存在这个属性.

那么我们可以使用runtime中的一个叫关联对象的办法, 给分类添加一个property, 并且打印原类的property列表是真真切切存在的. 上代码

// Person+RunningMan.h
@interface Person (RunningMan)

/** 速度(km/h) */
@property (nonatomic, assign) CGFloat speed;

@end

// Person+RunningMan.m
#import <objc/objc-runtime.h>

@implementation Person (RunningMan)

- (CGFloat)speed
{
    id value = objc_getAssociatedObject(self, _cmd);
    return [value doubleValue];
}

- (void)setSpeed:(CGFloat)speed {
    objc_setAssociatedObject(self, @selector(speed), @(speed), OBJC_ASSOCIATION_ASSIGN);
}

@end

好的, 我们看看加了这个分类之后再利用第二节的办法打印下瞧瞧~

2016-09-04 17:26:00.403 Runtime-实践篇[13795:1050331] ivarName:_name, ivarValue:Kobe
2016-09-04 17:26:00.404 Runtime-实践篇[13795:1050331] ivarName:_age, ivarValue:18
2016-09-04 17:26:00.405 Runtime-实践篇[13795:1050331] ivarName:_weight, ivarValue:110
2016-09-04 17:26:00.405 Runtime-实践篇[13795:1050331] ivarName:_address, ivarValue:值为nil
2016-09-04 17:26:00.405 Runtime-实践篇[13795:1050331] propertyName:speed, propertyValue:0
2016-09-04 17:26:00.405 Runtime-实践篇[13795:1050331] propertyName:address, propertyValue:值为nil
2016-09-04 17:26:00.405 Runtime-实践篇[13795:1050331] propertyName:weight, propertyValue:110
2016-09-04 17:26:00.405 Runtime-实践篇[13795:1050331] methodName:speed, argumentsCount:0
2016-09-04 17:26:00.406 Runtime-实践篇[13795:1050331] methodName:setWeight:, argumentsCount:1
2016-09-04 17:26:00.406 Runtime-实践篇[13795:1050331] methodName:setSpeed:, argumentsCount:1
2016-09-04 17:26:00.406 Runtime-实践篇[13795:1050331] methodName:weight, argumentsCount:0
2016-09-04 17:26:00.446 Runtime-实践篇[13795:1050331] methodName:setAddress:, argumentsCount:1
2016-09-04 17:26:00.447 Runtime-实践篇[13795:1050331] methodName:address, argumentsCount:0
2016-09-04 17:26:00.447 Runtime-实践篇[13795:1050331] methodName:.cxx_destruct, argumentsCount:0

看到了嘛? speed这个属性乖乖的在那儿呢.

其实关联对象这个技术就是用哈希表实现的, 将一个类映射到一张哈希表上, 然后根据key找到关联对象, 所以严格说, 关联对象跟本类没有任何联系, 它不是储存在类的内部的. 它的底层原理就不多介绍了, 不属于本文的范畴, 大家感兴趣的可以到以下两篇文章里面看看
Associated Objects
Objective-C Associated Objects 的实现原理

4. 动态添加方法实现

好了, 绕来绕去又回到了runtime强大的消息转发身上了, 当一个方法没有实现的时候, OC会怎么做的呢? 还记得那四个步骤吗, 不记得也没关系, 我们看代码!

/*
    Person类只有- (void)noIMPMethod方法的声明, 
    没有他的实现, 一般来说程序运行, 调用noIMPMethod这个方法, 肯定要报错的,
    我们可以在这个方法里动态添加该方法的实现
*/
    
// 用来实现noIMPMethod方法实现的函数
void otherFunction(id self, SEL cmd)
{
    NSLog(@"动态处理了noIMPMethod方法的实现");
}

// 第一步, 对象在收到无法解读的消息后, 首先调用其所属类的这个类方法
// 返回YES则结束消息转发, 返回NO则进入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // 如果是noIMPMethod方法
    if([NSStringFromSelector(sel) isEqualToString:@"noIMPMethod"]){
    // 动态添加方法实现
    class_addMethod([self class], sel, (IMP)otherFunction, "v@:");
        return YES;
    } else {
        return [super resolveInstanceMethod:sel];
    }
}

程序运行结果 : 
2016-09-04 17:38:24.301 Runtime-实践篇[13856:1054351] 动态处理了noIMPMethod方法的实现

代码应该也很明了, 当判断到无法解读的SEL后, 可以给该SEL动态添加方法的实现.

NOTE:
消息转发的另外3个方法会在下文放上, 因为本例子用不上所以就不放上来了

5. 更换方法调用者

试想一下, 一个腿部残疾的人, 他想跑, runtime知道他自己跑不了, 于是就让他的狗替代他去跑了(person没有run方法的声明和实现, dog有run方法的声明和实现)

// 第一步, 对象在收到无法解读的消息后, 首先调用其所属类的这个类方法
// 返回YES则结束消息转发, 返回NO则进入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return NO;
}

// 第二步, 动态方法解析失败, 则调用这个方法
// 返回的对象将处理该selector, 返回nil则进入下一步
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return nil;
}

// 第三步, 在这里返回方法的消息签名
// 返回YES则进入下一步, 返回nil则结束消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"run"]){
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    } else {
        return [super methodSignatureForSelector:aSelector];
    }
}

// 第四步, 最后一次处理该消息的机会
// 这里处理不了这个invocation就会结束消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    // 在这我们修改调用该方法的对象
    Dog *dog = [[Dog alloc] init];
    // 让dog去调用该方法
    [anInvocation invokeWithTarget:dog];
}

那么我们通过((void(*)(id, SEL))objc_msgSend)((id)p, @selector(run)); // 这里强转是为了不让编译器报参数过多的错误方法调用person的run方法, 得到的输出为 :

2016-09-04 17:49:42.634 Runtime-实践篇[13939:1059419] 是狗在跑步

同样, 其实可以在第二部就把这件事做了, 只需返回dog实例即可, 大家可以亲手操作试试

6. 更改特定方法的实现

一条狗在吃着骨头, 然后他的主人一把把一个球扔得远远的, 碍于主人的淫威之下, 狗就不得不停下来跑去捡球了(更改[dog eat]方法的实现为[dog run])

// 第一步, 对象在收到无法解读的消息后, 首先调用其所属类的这个类方法
// 返回YES则结束消息转发, 返回NO则进入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return NO;
}
    
// 第二步, 动态方法解析失败, 则调用这个方法
// 返回的对象将处理该selector, 返回nil则进入下一步
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return nil;
}

// 第三步, 在这里返回方法的消息签名
// 返回YES则进入下一步, 返回nil则结束消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"eat"]){
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    } else {
        return [super methodSignatureForSelector:aSelector];
    }
}

// 第四步, 最后一次处理该消息的机会
// 这里处理不了这个invocation就会结束消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    // 在这我们修改选择子为run
    [anInvocation setSelector:@selector(run)];
    // 让dog去调用该方法
    [anInvocation invokeWithTarget:self];
}

((void(*)(id, SEL))objc_msgSend)((id)dog, @selector(eat));的输出结果为 :

2016-09-04 17:56:53.238 Runtime-实践篇[14037:1063170] 是狗在跑步

大部分demo还是比较简单, 只要看了, 亲手敲敲代码都能掌握, 并不存在什么技术含量, 难的是在真实项目中出现这种需求的时候能够在脑子里唤醒这部分知识, 并灵活运用其中. 共勉吧, 程序员大兄弟们!

另外, demo在这里Github, 不要忘了给star哦~

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

推荐阅读更多精彩内容

  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,693评论 7 64
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,521评论 33 466
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 737评论 0 1
  • 测试
    xzyangg阅读 120评论 2 0