前言
Runtime是iOS开发者进阶必须学习的一个知识点。网上关于Runtime 有许多介绍,有深入有简单介绍,也有实际应用举例,但是都不够系统,相关的知识点得不到关联,对runtime 的认知不能形成一个体系。这里参照苹果官方文档,加上自己的一些理解,进行了系统的介绍总结。 文章篇幅很长,前面很大一部分是概念介绍有点枯燥,如果想直接学习runtime的使用可以直接查看后面的常见使用介绍,或者下载我写的Demo:WXSRuntime 欢迎star 下载。 (欢迎转载,转载注明出处)
目录
一、Runtime版本与平台介绍
二、使用Runtime的场景
1、Objective-C Source Code 2、NSObject Methods 3、Runtime Functions
三、消息机制(Messaging)
1、objc_msgSend 函数 2、使用隐藏的参数 3、获得方法地址
四、动态方法决议
1、动态方法决议(Dynamic Method Resolution) 2、动态加载(Dynamic Loading)
五、消息转发
1、消息转发(Forwarding) 2、消息转发与多继承(Forwarding and Multiple Inheritance) 3、代理对象(Surrogate Objects) 4、消息转发与继承(Forwarding and Inheritance)
六、Type Encodings
七、声明属性(Declared Properties)
1、属性类型和函数(Property Type and Functions) 2、属性类型字符串(Property Type String) 3、属性特性(Property Attribute)例子
正文
一、Runtime版本与平台介绍
Runtime 有两个版本 一个Legacy版本 ,一个Modern版本。Legacy版本用于Objective-C 1,32位 的OS X的平台上,Modern版本适用于Objective-C 2,iOS 和 OS X v10.5 及之后版本。很明显,我们现在采用的Runtime 为Modern版本。
二、使用Runtime的场景
OC程序使用Runtime 系统有三种情景:Objective-C Source Code、NSObject Methods、Runtime Functions;
1、Objective-C Source Code
大部分时候runtime是在幕后运行工作着的。在编译含有OC类、方法的代码时,编译器通过Runtime的消息机制在幕后完成创建数据、调用函数。Runtime的实质是消息的发送,官方文档称之为Messaging,翻译成中文为消息机制。消息机制在OC源码使用过程中会被调用。
2、NSObject Methods
官方文档原文的描述:
Some of the NSObject methods simply query the runtime system for information. These methods allow objects to perform introspection. Examples of such methods are the class method, which asks an object to identify its class; isKindOfClass: and isMemberOfClass:, which test an object’s position in the inheritance hierarchy; respondsToSelector:, which indicates whether an object can accept a particular message; conformsToProtocol:, which indicates whether an object claims to implement the methods defined in a specific protocol; and methodForSelector:, which provides the address of a method’s implementation. Methods like these give an object the ability to introspect about itself. Cocoa中绝大多数的类继承自NSObject,因此继承了NSObject的方法,其中一些方法可以查询Runtime系统的相关信息。例如: isKindOfClass: 用来判断是否是某个类或其子类的实例isMemberOfClass: 用来判断是否是某个类的实例 respondsToSelector: 用来判断是否有以某个名字命名的方法(被封装在一个selector的对象里传递)instancesRespondToSelector: 用来判断实例是否有以某个名字命名的方法conformsToProtocol: 判断是否遵循相关协议 methodForSelector: 可以获得一个指向方法实现的指针,并可以使用该指针直接调用方法实现
3、Runtime Functions
Runtime系统是一个C语言动态库,在Xcode的/usr/include/objc里可以看到由一些列函数和数据结构构成的公共接口,里面的许多函数可以用C语言去调用。这些函数可以在开发环境中用来生成一些工具,在一些功能场景中也可以用到。关于这些函数的详细介绍可以查看Objective-C Runtime Reference
三、消息机制(Messaging)
1、objc_msgSend 函数
在OC中,调用方法: [receiver message] 在编译器里会转换成消息机制里的消息发送形式:objc_msgSend(receiver, selector) 如果带参数的话: objc_msgSend(receiver, selector, arg1, arg2, ...) 消息功能为动态绑定做了很多有必要的工作: 1、通过selector 在 消息接收者 class 里选择方法实现(method implementation) 2、调用方法实现,传递到接收对象 with 参数 3、传递方法实现返回值。 Note:编译器才能启用消息函数,我们的代码不可以。 为了让编译器编译时,消息机制与类的结构关联上,每个类的结构里添加了两个基本的元素: 1、指向父类的指针(isa指针); 2、类调度表(A class dispatch table),通过Selector方法名在dispatch table里面匹配对应的方法地址(class-specific address)。 当一个对象被创建、分配内存时,它的实例里的变量会初始化,里面有一个指向它的类的结构体的指针,isa指针。 Paste_Image.png 消息发送到一个对象时,通过class结构体里的isa指针在dispatch table里寻找相应的 selector,如果找不到便进入其父类里找,一直到NSObject. 一旦定位到selector ,便调用该方法,传递相关数据。为了提高效率,Runtime 会缓存调用过的selector 和方法地址,在到disaptch table查找之前,先到Catch里查找。
2、使用隐藏的参数
在objc_msgSend运作过程中,在传送消息中传递所有参数,包括两个隐藏起来的参数: 1、接收对象 2、方法的selector 这两个参数为每个方法的实现提供调用时的相关信息,之所以说是被隐藏是因为在代码中不用声明这两个参数,没有体现出这两个参数,他们被嵌入在方法的实现中。 一个方法中,接收对象为self,它的方法选择器selector为_cmd,如下例子中,_cmd为strange方法的selector,self为接收strange方法的对象。
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
3、获得方法地址
绕过动态绑定的唯一方法只有获取方法地址然后直接调用。在某些场景下,需要连续执行一个方法多次,如果普通的调用会让每次执行该方法时相应的消息发送内容都重写一次,这里我们可以使用NSObject里面一个方法methodForSelector:防止消息每次都重写。 方法methodForSelector:可以让指针指向方法对应的实现,接着使用指针调用程序,执行方法。该方法的指针必须返回适当的函数类型,为了以防万一,返回和参数类型都应该包含进去。
void (setter)(id, SEL, BOOL);
int i;
setter = (void ()(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
传到程序(procedure)的消息中的前两个参数 是接收对象(self)和 方法选择器(_cmd)。这两个参数在方法语句中被隐藏了,但是当方法作为函数被调用时必须明确存在。 使用methodForSelector:方法绕过动态绑定可以节省很大一部分消息发送的时间。但是,只有在一个特定的消息重复许多次时,方法才会被签名认证,如上面例子中的for循环。平时开发中,我们可以在一些循环中采用methodForSelector:方法提高代码运行效率。 Using methodForSelector: to circumvent dynamic binding saves most of the time required by messaging. However, the savings will be significant only where a particular message is repeated many times 值得注意的是methodForSelector:方法是runtime系统中提供得方法,不是Objective-C语言自身的特性。
四、动态方法决议
1、动态方法决议(Dynamic Method Resolution)
在一些情景下我们想要动态实现方法,例如用@dynamic声明属性特征, @dynamic propertyName; 这行代码告诉编译器,与这个属性相关的方法动态实现。 我们可以通过resolveInstanceMethod: 和 resolveClassMethod: 动态实现一个实例和类的selector。 一个OC方法其实是一个至少带有self(接受对象)和 _cmd(执行的selector)的 C语言函数.我们可以通过class_addMethod:将一个函数添加成一个类中的方法,而添加的过程可以在resolveInstanceMethod:中添加
@implementation MyClass
- (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
转发消息和动态决议是紧密相关的。一个类在消息转发机制(forwarding mechanism)结束之前有一个机会动态处理一个方法,调用respondsToSelector:和instancesRespondToSelector:会先为selector提供IMP,从而可以进行动态方法处理。 如果你实现了resolveInstanceMethod:这个方法,但是想让某些selector通过消息转发机制转发,在实现中判断如果是这些selector 就return NO;
2、动态加载(Dynamic Loading)
OC程序可以在运行时加载和链接新的类,新加入的代码会合并进程序中,与一开始加载的类或类别同等对待。 动态加载可以用来做很多事情。例如,App的系统设置便是动态加载。 在Cocoa环境中,常用于App功能的自定义定制。你的程序可以在运行时加载其他模块,例如 Interface Builder加载颜色,和OS X系统设置应用加载偏好设置模块。动态加载模块扩展了应用的功能。 You provide the framework, but others provide the code. Runtime 提供了动态加载的方法objc_loadModules,但是Cocoa里的NSBundle提供了更方便的接口,可以在到这里NSBundle 看看其用法。
五、消息转发
Sending a message to an object that does not handle that message is an error. However, before announcing the error, the runtime system gives the receiving object a second chance to handle the message. 当一个对象不能正常及时处理发送过来的消息时会导致异常,runtime系统在发生异常之前提供了第二次机会处理消息的机会。
1、消息转发(Forwarding)
如果一个对象没有正常处理发送过来的消息,在异常之前 Runtime 向对象发送带有 NSInvocation对象作为基础参数的forwardInvocation: 消息,NSinvoction对象收纳了初始消息和参数。 我们可以实现forwardInvocation:方法做为对消息的默认回应,防止发生异常。forwardInvocation:正如其名字所形容的,就是转发消息到其他对象。 假如你想设计一个能响应negotiate方法的对象a,而negotiate方法的实现是在其他对象b中。你可以通过发送negotiate消息到实现这个方法的类b中去,简单地完成这个功能。 更进一步,假设你想要这个对象a能精确地(exactly)执行negotiate方法,一个方法是继承。但是它不可能在两个类的对象中传值。 我们可以通过实现一个方法简单地传送消息到类b的实例中去“借”negotiate方法。
- (id)negotiate
{
if ( [someOtherObject respondsTo:@selector(negotiate)] )
return [someOtherObject negotiate];
return self;
}
用这个方法会带来一些麻烦,对于每一个你想"借"的方法,你需要实现一个方法去获得,而且对于一些未知的方法,你无法去处理。 通过forwardInvocation:方法可以解决这个问题,这里我们可以采用动态的方式而不是静态。主要工作过程为:当一个对象因为没有匹配的selector方法而不能响应一个消息时,runtime系统向这个对象发送forwardInvocation:消息,每一个对象都从NSObject继承了forwardInvocation:方法,但是在NSObject中,这个方法结束后直接调用了doesNotRecognizeSelector:。我们必须自己重写forwardInvocation:方法执行之后相关的实现。这样我们就可以利用forwardInvocation:方法转发消息到其他类中去。 我的github这里面的Messaging文件是消息转发相关例子,注释中有进行了说明。 转发一个消息时,forwardInvocation:方法中需要做到: 1、确定消息发送到何处。 2、发送时带上原始参数。 消息可以通过invokeWithTarget:方法发送 - (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
返回值类型可以被传回到原始的传送者(sender)中,包括 id类型,结构体,双精度浮点数。 可以把forwardInvocation:方法当做无法辨认的消息的分发中心,将消息分配给各个接收者,它可以把所有消息发送到同一个目的地,也可以联合几个消息做出同一个响应。forwardInvocation:方法主要面向方法实现,但是它提供的通过消息转发链链接对象的机会为程序设计带来更多可能性。 Note: forwardInvocation: 只有在调用了不存在的方法导致消息无法处理时才会被调用。 可以到Foundation框架中查看NSInvocation的相关文档,获取更多的详细内容。
2、消息转发与多继承(Forwarding and Multiple Inheritance)
消息转发参照了继承,在OC程序中可以借用一些多继承的功能。 在下图中,一个对象对一个消息做出回应,类似于借来一个在其他类定义实现的方法。 Paste_Image.png 在这个插图中,warrior实例转发了一个negotiate消息到Diplomat实例中,执行Diplomat中的negotiate方法,结果看起来像是warrior实例执行了一个和Diplomat实例一样的negotiate方法,其实执行者还是Diplomat实例。 在上面的例子中,看起来相当于Warrior类继承了Diplomat。 The object that forwards a message thus “inherits” methods from two branches of the inheritance hierarchy—its own branch and that of the object that responds to the message 消息转发提供了许多类似于多继承的特性,但是他们之间有一个很大的不同: 多继承:合并了不同的行为特征在一个单独的对象中,会得到一个重量级多层面的对象。 消息转发:将各个功能分散到不同的对象中,得到的一些轻量级的对象,这些对象通过消息通过消息转发联合起来。
3、代理对象(Surrogate Objects)
消息转发 不仅参照了多继承,它还让用轻量级对象代替重量级对象成为了可能。 通过代理(Surrogate)可以为对象筛选消息。 代理管理发送到接收者的消息,确定参数值被复制,拯救等等。但是它不企图去做很多其他的,它不重复对象的功能只是简单地提供对象一个可以接收来自其他应用消息的地址。 举个例子,有一个重量级对象,里面加入了许多大型数据,如图片视频等,每次使用这个对象的时候都需要读取磁盘上的内容,需要消耗很多时间(time-consuming),所以我们更偏向于采用懒加载模式。 在这样的情况下,你可以初始化一个简单的轻量级对象来代理(surrogate)它。利用代理对象可以做到例如查询数据信息等,而不用加载一整个重量级对象。如果是直接用重量级对象的话,它会一直被持有占用资源。当代理的forwardInvocation:方法第一次接收消息的时候,它会确保对象是否存在,如果不存在边创建一个。 当这个代理对象发送的消息覆盖了这个重量级对象的所有功能时,这个代理对象就相当于和重量级对象一样。 创建一个轻量级的对象来代理一个重量级对象,完成相对应的功能,而不用一直持有着重量级对象,从而可以减少资源占用。
4、消息转发与继承(Forwarding and Inheritance)
尽管消息转发参照了继承,但是NSObject 不会混乱 像 respondsToSelector: 和 isKindOfClass: 这些方法只有在继承体系里看到,不会出现在消息转发链里。 例如, Warrio 对象是否响应negotiate
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...
大多情况下答案是No,尽管它能无错误地收到negotiate消息或者某种意义上通过转发到Diplomat来响应。 如果我们想通过消息转发设立一个代理对象或扩展类的功能,消息转发体质就得像继承一样清晰显然。如果我们想让对象看起来真正地继承了父类对象的行为特征,我们需要去重写respondsToSelector: 和isKindOfClass:方法。 - (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}
除了respondsToSelector: 和 isKindOfClass:两个方法外,instancesRespondToSelector方法也映射着消息转发规则。当涉及到协议时我们需要还要考虑conformsToProtocol:。同样,一个对象转发消息时,将会执行methodSignatureForSelector:,它描述了方法的签名认证等相关信息,如果一个对象能够转发详细到它的代理,我们需要实现methodSignatureForSelector:方法。 - (NSMethodSignature)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
注意:这是一个高级用法,只适用于没有其他可能解决方案的情况下使用,不建议用它来代替继承。当你使用这个方法的时候必须保证你完全熟悉类相关的行为特征以及消息转发的情况。 里面涉及的一些方法可以到NSObject 和NSInvocation中去查阅更详细的内容
六、Type Encodings
为了协助runtime 系统,编译器为每个方法用字符串 编码 返回和参数 类型,并将字符串与方法选择器相关联。所使用的编码表同样在其他context里是有用的,所以它是公共可用的with@encode()编译程序指令。提供一个类型说明时,@encode()返回一个编码这个类型的字符串。可以是基础类型,int, 指针,结构体,联合体。或一个类,事实上,可以用来做C语言sizeof()操作的参数
char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char buf3 = @encode(Rectangle);
下面是类型编码表。里面有许多是与archive 和 distribution 编码时重复的编码,但是 可以到NSCoder查阅更详细的内容 Objective_C type encodings
重要:OC不支持long double 类型,@encode(long double)将会返回d,即double类型;
数组的类型编码是在方括号里面的包含一个代表元素个数的数字和一个数组元素的类型编码,例如一个包含12个浮点数的数组:
[12^f]
结构体类型编码的显示是一个大括号里面包含名称和变量的类型编码,例如:
typedef struct example {
id anObject;
char aString;
int anInt;
} Example;
这个结构体的类型编码是:
{example=@i}
指向这个结构体的结构体指针类型编码为:
^{example=@i}
还有另外一种去除了内部的说明:
^^{example}
对象(Object)与结构体类似,例如NSObject的编码:
{NSObject=#}
NSObject只声明一个实例变量:isa,它是一个Class。 该注意的是,尽管@encode()指令不返回这些编码,但是当他们在协议中声明的方法中被使用到时,runtime系统为类型限定符提供了另外的编码,如下图。 Objective-C method encodings
七、声明属性(Declared Properties)
编译器在属性声明(Property declarations)的时候,它生成与类、类别、协议相关联的元数据,我们可以通过这些元数据使用这些函数:通过名字查看属性、得到属性的类型(@encode串形式)、复制出属性的相关参数(C语言字符串形式)列表等。
1、属性类型和函数(Property Type and Functions)
可以使用class_copyPropertyList和 protocol_copyPropertyList 获取属性数组
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
例如:有这么一个类如下:
@interface Lender : NSObject {
float alone;
}
@property float alone;
@end
可以获得其属性队列:
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
获取属性名称:
const char *property_getName(objc_property_t property)
可以通过一个已知名字的Class或协议获取属性
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
可以获取属性的相关特性,特性中包括了许多信息。例如类型编码字符串等,下面的章节中有具体讲解。
const char *property_getAttributes(objc_property_t property)
将以上的函数结合在一起,可以打印出类中所有的属性的信息:
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
2、属性类型字符串(Property Type String)
可以用property_getAttributes函数得到属性的名字、类型编码字符串、以及特性。 字符串以T开头,后面接着@encode类型编码,接着是逗号,接着是V,接着是属性名,在这中间,使用下面这个表中的符号,用逗号隔开。 Declared property type encodings.png 具体例子看下面的属性特性例子。
3、属性特性(Property Attribute)例子
先预处理:
enum FooManChu { FOO, MAN, CHU };
struct YorkshireTeaStruct { int pot; char lady; };
typedef struct YorkshireTeaStruct YorkshireTeaStructType;
union MoneyUnion { float alone; double down; };
下面是属性类型编程字符串的例子: Paste_Image.png Paste_Image.png
常见使用:
runtime 常见的使用有:
动态交换两个方法的实现
实现分类也可以添加属性 实现NSCoding的自动归档和解档 实现字典转模型的自动转换 Hook 这里是代码完整版WXSRuntime
动态交换两个方法的实现
//交换实例方法
NSLog(@"------exchange-----\n");
Method m1 = class_getInstanceMethod([ShowExchange class], @selector(firstMethod));
Method m2 = class_getInstanceMethod([ShowExchange class], @selector(secondMethod));
method_exchangeImplementations(m1, m2);
ShowExchange *test = [ShowExchange new];
[test firstMethod];
NSLog(@"------exchange InstanceMethod-----\n");
##实现分类也可以添加属性
-(void)setWxsTitle:(NSString )wxsTitle { objc_setAssociatedObject(self, WXSAddPropertyKeyTitle, wxsTitle, OBJC_ASSOCIATION_RETAIN); } -(NSString )wxsTitle { return objc_getAssociatedObject(self, WXSAddPropertyKeyTitle); }
##实现NSCoding的自动归档和解档
unsigned int outCount = 0; Ivar ivars = class_copyIvarList(self.class, &outCount); for (int i = 0; i< outCount; i++) { Ivar ivar = ivars[i]; const char ivarName = ivar_getName(ivar); NSString ivarNameStr = [NSString stringWithUTF8String:ivarName]; NSString setterName = [ivarNameStr substringFromIndex:1];
//解码
id obj = [aDecoder decodeObjectForKey:setterName]; //要注意key与编码的key是一致的
SEL setterSel = [self creatSetterWithKey:setterName];
if (obj) {
((void (*)(id ,SEL ,id))objc_msgSend)(self,setterSel,obj);
}
}
free(ivars);
##实现字典转模型的自动转换
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList(self.class, &outCount);
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
const char *propertyName = property_getName(property);
NSString *key = [NSString stringWithUTF8String:propertyName];
id value = nil;
if (![dict[key] isKindOfClass:[NSNull class]]) {
value = dict[key];
}
unsigned int count = 0;
objc_property_attribute_t *atts = property_copyAttributeList(property, &count);
objc_property_attribute_t att = atts[0];
NSString *type = [NSString stringWithUTF8String:att.value];
type = [type stringByReplacingOccurrencesOfString:@"“" withString:@""];
type = [type stringByReplacingOccurrencesOfString:@"@" withString:@""];
NSLog(@"type%@",type);
//数据为数组时
if ([value isKindOfClass:[NSArray class]]) {
Class class = NSClassFromString(key);
NSMutableArray *temArr = [[NSMutableArray alloc] init];
for (NSDictionary *tempDic in value) {
if (class) {
id model = [[class alloc] initWithDic:tempDic];
[temArr addObject:model];
}
}
value = temArr;
}
//数据为字典时
if ([value isKindOfClass:[NSDictionary class]] && ![type hasPrefix:@"NS"] ) {
Class class = NSClassFromString(key);
if (class) {
value = [[class alloc] initWithDic:value];
}
}
// 赋值 SEL setterSel = [self creatSetterWithKey:key]; if (setterSel != nil) { ((void (*)(id,SEL,id))objc_msgSend)(self,setterSel,value); }
}
## Hook
(void)viewDidLoad { [super viewDidLoad]; Method m1 = class_getInstanceMethod([self class], @selector(viewWillAppear:)); Method m2 = class_getInstanceMethod([self class], @selector(wxs_viewWillAppear:));
BOOL isSuccess = class_addMethod([self class], @selector(viewWillAppear:), method_getImplementation(m2), method_getTypeEncoding(m2)); if (isSuccess) {
// 添加成功:说明源方法m1现在的实现为交换方法m2的实现,现在将源方法m1的实现替换到交换方法m2中
class_replaceMethod([self class], @selector(wxs_viewWillAppear:), method_getImplementation(m1), method_getTypeEncoding(m1));
}else {
//添加失败:说明源方法已经有实现,直接将两个方法的实现交换即
method_exchangeImplementations(m1, m2);
} } -(void)viewWillAppear:(BOOL)animated { NSLog(@"viewWillAppear"); }
(void)wxs_viewWillAppear:(BOOL)animated { NSLog(@"Hook : 拦截到viewwillApear的实现,在其基础上添加了这行代码"); [self wxs_viewWillAppear:YES]; }
网友评论