专栏名称: Cocoa开发者社区
CocoaChina苹果开发中文社区官方微信,提供教程资源、app推广营销、招聘、外包及培训信息、各类沙龙交流活动以及更多开发者服务。
目录
相关文章推荐
51好读  ›  专栏  ›  Cocoa开发者社区

事岀无常必有妖-iOS捉妖记之(Runtime)

Cocoa开发者社区  · 公众号  · ios  · 2016-11-04 08:02

正文

▲点击上方“CocoaChina”关注即可免费学习iOS开发


来源:http://blog-lision.com/


看完本篇之后你将获得:


  • 了解什么是runtime

  • 知道可以利用runtime做到哪些事情

  • 掌握用runtime开发的常用方法


Runtime是开源的,任何时候你都可以从http://opensource.apple.com获取。事实上查看 Objective-C 源码是我理解它是如何工作的第一种方式,在某些问题上要比读苹果的文档要好。


引言


相信很多从事iOS开发的小伙伴们都听过这样一句形容runtime的话:


runtime就像是iOS开发中的妖怪,谁都听说过,但少有人见(用)到过!


这句话是某知名培训机构内某老师对学生们说的一句话,相信不少人尤其是初学的萌新们还没了解过runtime,听了这句话就被吓到了!直接在心里给runtime打个一打标签[危险,慎用,底层,难,用不到,不用掌握]。以至于很多人做了有一段时间的iOS开发却依然对其一知半解……


定义


Objective-C 的 Runtime 是一个运行时库(Runtime Library),它是一个主要使用 C 和汇编写的库,为 C 添加了面相对象的能力并创造了 Objective-C。这就是说它在类信息(Class information) 中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime 创建了所有需要的结构体。


其实就在下个人的理解:runtime就是丫Objective-C 的灵魂!Objective-C之所以叫Objective-C是因为他比C语言不同,是面向对象的。但是Objective-C为什么有面相对象的能力?就是因为有runtime这个鬼东西!


进阶


我们为什么要学习runtime?


  • runtime可以遍历对象的属性

  • runtime可以动态添加/修改属性,动态添加/修改/替换方法,动态添加/修改/替换协议

  • runtime可以动态创建类/对象/协议等等

  • runtime可以方法拦截调用


其实runtime所能做的还不止这些,你甚至可以利用它来把一个Class A的实例对象a在程序中当作Class B的实例对象来用。所以很多iOS开发者把runtime叫做obj-C的黑魔法!


常用方法


先来个最简单最基本的也是几乎所有runtime文必备的例子:


obj-C: [obj func];

runtime:objc_msgSend(obj, @selector(func);


很多初学者除了知道runtime把对象的方法调用转化成消息发送的代码之后就不知道其他的了,但是显然仅仅知道上述的转化并没有什么“吡-”用,我们来看runtime中比较常用(实用)的几种基本用法:


  • 遍历对象的属性

首先定义一个简单的类Person


@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) NSInteger age;

@end


然后在需要遍历对象的属性时


id personClass = objc_getClass("Person");

unsigned int outCount;

objc_property_t *properties = class_copyPropertyList(personClass, &outCount);

for (int i = 0; i

objc_property_t property = properties[i];

printf("%s:%s\n", property_getName(property), property_getAttributes(property));

}

free(properties);


这时就会打印出这个类对象的属性相关信息:


name:T@"NSString",C,N,V_name

age:Tq,N,V_age


  • 消息转发


[消息转发]指的就是我上面提到的动态方法解析,重定向以及消息转发,我们先来看一张图:



动态方法解析:


从上图可以知道,当对一个实例对象obj发送一条消息func时[obj func],当前obj如果没有对func实现对应的方法,那么就runtime会调用+ (BOOL)resolveInstanceMethod:(SEL)sel方法允许开发者对当前受到的消息func做出响应,这就是动态方法解析。


继续拿上面的Person举例子,给Person类加一个体重weight属性


@property (nonatomic, assign) NSInteger weight;


然后在.m文件中加入一下代码


@implementation Person

@dynamic weight;  //避免自动生成getter/setter方法

//重写resolveInstanceMethod方法,动态方法解析

+ (BOOL)resolveInstanceMethod:(SEL)sel

{

if (sel == @selector(setWeight:)) {

class_addMethod([self class], sel, (IMP)setPropertyDynamic, "v@:");

return YES;

}

return [super resolveInstanceMethod:sel];

}

//用来响应setWeight的c语言方法

void setPropertyDynamic(id self, SEL _cmd) {

NSLog(@"Dynamic setWeight");

}

@end


然后可以在代码就调用Person的setWeight方法


Person *lision = [[Person alloc] init];

lision.weight = 75;


这时候如果不重写+ (BOOL)resolveInstanceMethod:(SEL)sel方法本应该异常的,但是你可以发现程序会打印出信息:


Dynamic setWeight


重定向:


那么还是看图说话,如果没有重写+ (BOOL)resolveInstanceMethod:(SEL)sel方法,那就就会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法,把这个消息让另一个对象来处理,这次叫做重定向。


跟着上面的例子走,先另一个类People用来等待重定向:


@interface People : NSObject

@end


给新写的People类加一个weight方法,但是注意:People没有weight属性!


- (NSInteger)weight

{

return 70;

}


接下来我们重写- (id)forwardingTargetForSelector:(SEL)aSelector方法:


- (id)forwardingTargetForSelector:(SEL)aSelector

{

if (aSelector == @selector(weight)) {

People *people = [[People alloc] init];

return people;

}

return [super forwardingTargetForSelector:aSelector];

}


然后我们在刚才的执行代码中:


NSLog(@"weight = %ld", lision.weight);


然后运行,经历过上面的例子你肯定知道不会异常啦,而且你会发现虽然你给weight属性赋值明明是75,可是打印结果是:weight = 70。这就是Person类- (id)forwardingTargetForSelector:(SEL)aSelector方法中把这条信息抛给了people对象,调用了People类的weight方法!


消息转发:


那么如果上面的两个方法都没有重写,并且消息依然是当前对象没有实现的方法,runtime才会启用消息转发调用– (void)forwardInvocation:(NSInvocation *)anInvocation,需要注意的是很多文章没有提到这个方法花费代价较大,如果要实现把消息转发类似的功能建议最好使用重定向,而且再调用这个方法前runtime会先调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法。


我们跟着上面的例子,继续给Person类加入属性:


@property (nonatomic, copy) NSString *ID;


以及上面提到的两个方法:


- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

{

if (aSelector == @selector(setID:)) {

NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];

//"v@:"代表的意思参见Objective-C Type Encodings,这里的意思是返回值为空

return sig;

}

return nil;

}

- (void)forwardInvocation:(NSInvocation *)anInvocation

{

People *people = [[People alloc] init];

if ([people respondsToSelector:anInvocation.selector]) {

[anInvocation invokeWithTarget:people];

}

}


别忘记了在People类中添加对应的方法:


- (void)setID:(NSString *)ID

{

NSLog(@"People setID: %@", ID);

}


最后,我门只需要在执行代码块中加入代码:


lision.ID = @"xxxx";


结果显而易见,相信各位都知道将会打印信息:


People setID: xxxx


写在最后


其实runtime就是我们无时无刻不在用的东西,只是人们习惯对看不到的东西怀有恐惧心理而已。我们平时的obj-C代码都是被runtime转译为c和汇编语言运行的。我个人认为大公司为什么喜欢在面试时问runtime相关的东西是因为大公司往往不仅仅要会干活的人,它还会要求这些会干活的人知道其中的原理!我们自己也应该要求自己或多或少的理解这些原理,知道我们为什么写出的obj-C代码经历了哪些过程run到我们的设备上,不要敲了很多年的代码还是一只只会干活的码农。



微信号:CocoaChinabbs


▲长按二维码“识别”关注即可免费学习 iOS 开发

月薪十万、出任CEO、赢娶白富美、走上人生巅峰不是梦

--------------------------------------

商务合作QQ:2408167315

投稿邮箱:[email protected]