关键词:
运行时
,isa
,superclass
,消息传递
,消息转发
,meta-class
面试题:
- 什么是Runtime?
- 方法的本质是什么?
- 实际开发中有哪些地方用到了Runtime?
一. Runtime介绍
Runtime顾名思义,运行时;
OC是一种动态性比较强的语言,它是对C语言进行了扩展,并集成了smalltalk的动态语言特性,能够让程序在运行时做更多的事情。
Runtime是由C和C++、汇编实现的一套API,是OC面向对象和动态机制的基石;
二. Runtime消息机制
在Runtime机制中,方法的本质是消息发送objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
;
receive
为消息的接收者,sel
为方法名;
消息发送主要会经过「消息传递」和「消息转发」两大阶段;
其中,「消息转发」阶段有:「动态方法解析」、「备用接受者」和「完整的消息转发」三个过程;
1. 消息传递
流程大致为:
1)通过对象的isa
指针,找到对应的类或者元类;
2)在类中的method_list
中查找,如果找到调用方法实现,没找到则通过superClass
在其父类中查找,就这样一直向上查找,如果都没找到就进入消息转发阶段;
3)但这种实现有个问题,设想一下,加入每次我调用这个方法的时候,都这么找的话,效率其实会很低。
4)所以呢,就引出了另外一个重要的成员:objc_cache
5)objc_cache
会第一次在方法找到的时候,以方法名为key,方法的实现为value进行一个缓存。等下一次在调用的时候呢,会先在缓存中查找,这样一来效率不就提高了么。
具体的流程如下如:
2. 消息转发
消息转发会经历三个过程:
1)首先是「动态方法解析」,Objective-C运行时会调用 resolveInstanceMethod:
,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。
2)然后是「备用接收者」,如果目标对象实现了forwardingTargetForSelector:
,RunTime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。
3)最后是「完整的消息转发」,首先它会发送methodSignatureForSelector:
消息获得函数的参数和返回值类型。如果methodSignatureForSelector:
返回nil
,Runtime则会发出 doesNotRecognizeSelector:
消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation
对象并发送 forwardInvocation:
消息给目标对象。
具体的流程如下如:
三. Runtime运用场景
1. 关联对象,给分类category添加属性
#import <UIKit/UIKit.h>
@interface UIImage (downLoadURL)
@property (nonatomic, strong) NSString *downLoadURL;
@end
#import "UIImage+downLoadURL.h"
#import <objc/runtime.h>
@implementation UIImage (downLoadURL)
-(void)setDownLoadURL:(NSString *)downLoadURL{
objc_setAssociatedObject(self, @selector(downLoadURL), downLoadURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)downLoadURL{
return objc_getAssociatedObject(self, @selector(downLoadURL));
}
@end
2. 方法替换
#import <Foundation/Foundation.h>
@interface NSObject (Swizzling)
+(void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector;
@end
#import "NSObject+Swizzling.h"
#import <objc/runtime.h>
@implementation NSObject (Swizzling)
+(void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
bySwizzledSelector:(SEL)swizzledSelector{
Class class = [self class];
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzleMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, swizzleMethod);
}
}
@end
3. KVO
KVO的实现依赖于 Objective-C 强大的 Runtime,当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。
4. 字典转模型
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSNumber *age;
- (instancetype)modelWithDict:(NSDictionary *)dict;
@end
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
- (instancetype)modelWithDict:(NSDictionary *)dict
{
if (self = [super init]) {
// 获取类的属性及属性对应的类型
NSMutableArray *keys = [NSMutableArray array];
NSMutableArray *attributes = [NSMutableArray array];
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);
// 根据类型给属性赋值
for (NSString *key in keys)
{
id value = [dict valueForKey:key];
if (value == nil) {
continue;
}
[self setValue:value forKey:key];
}
}
return self;
}
@end
// 模型转换
NSDictionary *dict = @{@"name":@"Jack",@"age":@(18)};
Person *person = [[Person alloc] modelWithDict:dict];
NSLog(@"name: %@", person.name);
说明:这里的代码只是简单的提现一下思路,实际的转模型要复杂的多,比如对于数组、字典、自定义对象等类型的属性要如何处理等;
5. 自动归档解档
// 归档
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
Class c = self.class;
// 截取类和父类的成员变量
while (c && c != [NSObject class]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(c, &count);
for (int i = 0; i < count; i++) {
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
id value = [aDecoder decodeObjectForKey:key];
if (value){// 容错
[self setValue:value forKey:key];
}
}
// 获得c的父类
c = [c superclass];
free(ivars);
}
}
return self;
}
// 解档
- (void)encodeWithCoder:(NSCoder *)aCoder{
Class c = self.class;
// 截取类和父类的成员变量
while (c && c != [NSObject class]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(c, &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id value = [self valueForKey:key];
if (value){
[aCoder encodeObject:value forKey:key];
}
}
c = [c superclass];
// 释放内存
free(ivars);
}
}
说明:也可写成宏定义;
6. 利用消息转发机制解决方法找不到的异常问题
四. Runtime相关API
1. 类
// 1)动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
// 2)注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls)
// 3)销毁一个类
void objc_disposeClassPair(Class cls)
// 4)获取isa指向的Class
Class object_getClass(id obj)
// 5)设置isa指向的Class
Class object_setClass(id obj, Class cls)
// 6)判断一个OC对象是否为Class
BOOL object_isClass(id obj)
// 7)判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)
// 8)获取父类
Class class_getSuperclass(Class cls)
2. 成员变量
// 1)获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)
// 2)拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
// 3)设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)
// 4)动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
// 5)获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)
3. 属性
// 1)获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)
// 2)拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
// 3)动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
// 4)动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
// 5)获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)
4. 方法
// 1)获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)
// 2)方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)
// 3)拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)
// 4)动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
// 5)动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
// 6)获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)
// 7)选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)
// 8)用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)
五. Runtime相关面试题
1. objc中向一个nil对象发送消息将会发生什么?
如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。也不会发生崩溃。
详解:
1)如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil);
2)如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*) ,float,double,long double 或者long long的整型标量,发送给nil的消息将返回0;
3)如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0;
4)如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。
2. 什么时候会报unrecognized selector的异常?
objc在向一个对象发送消息时:
1)首先会经过「消息传递」阶段,会先在对象所在类/元类中的cache中查找方法,如未找到则在method_list中查找,还未找到则根据superclass指针在父类中重复查找;
2)如果「消息传递」阶段仍未找到,则进入「消息转发阶段」,消息转发有三大阶段;
- 动态方法解析;
在这个阶段允许开发者动态的添加方法实现,此时系统会重新启动一次消息发送; - 备用接收者;
在这个阶段允许开发者将方法的实现传递给其他对象; - 完整的消息转发;
在这个阶段允许开发者提供一个函数签名invocation来实现方法;
如果在以上阶段都没找到或实现消息发送的方法,最终会发送崩溃,报unrecognized selector异常;
3. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
不能向编译后得到的类中增加实例变量;
能向运行时创建的类中添加实例变量;
1)因为编译后的类已经注册在Runtime中,类结构体中的objc_ivar_list
实例变量的链表和instance_size
实例变量的内存大小已经确定,同时Runtime会调用class_setvarlayout
或class_setWeaklvarLayout
来处理strong
weak
引用.所以不能向存在的类中添加实例变量;
2)运行时创建的类是可以添加实例变量,调用class_addIvar
函数. 但是的在调用objc_allocateClassPair
之后,objc_registerClassPair
之前,原因同上;
4. isKindOfClass
与isMemberOfClass
的区别
isKindOfClass
来确定一个对象是否是一个类的成员,或者是派生自该类的成员。
isMemberOfClass
只能确定一个对象是否是当前类的成员。
isKindOfClass的实现代码大致如下:
-(BOOL)isKindOfClass:(Class)aClass {
for (Class tcls = isa; tcls; tcls-> superclass) {
if (tcls == aClass) {
return YES;
}
}
return NO;
}
isMemberOfClass的实现代码大致如下:
-(BOOL)isMemberOfClass:(Class)aClass {
return isa == aClass;
}
5. isa
和superclass
的关系
Runtime.png
6. 以下代码的输出结果是什么,为什么?
@interface Person : NSObject
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Person class] isKindOfClass:[Person class]];
BOOL res4 = [(id)[Person class] isMemberOfClass:[Person class]];
NSLog(@"%d %d %d %d", res1, res2, res3, res4);
}
return 0;
}
输出:1000
分析:
(id)[NSObject class]
和(id)[Person class]
获取的都是对应的类对象,而不是实例对象;
7. 以下代码能不能正常运行,为什么?
@interface NSObject (Test)
+ (void)test;
- (void)test;
@end
@implementation NSObject (Test)
- (void)test {
NSLog(@"%s", __func__);
}
@end
// 测试代码
[[NSObject new] performSelector:@selector(test)];
[NSObject test];
结果:全都正常输出,编译和运行都没有问题;
分析如下:
[[NSObject new] performSelector:@selector(test)]
能正常输出没什么好说的;
主要是为什么[NSObject test]
也能正常输出呢?
首先,[NSObject test]
方法调用时,会根据NSObject类对象的isa指针找到其元类对象,开始查找test
的方法实现;
显然,在NSObject元类中没有test
的方法实现,则会根据superclass指针,开始向上查找。
恰巧的是,我们这里的代码又是NSObject的category。
根据上图,我们可以看到NSObject元类的superclass指针,指向了NSObject类对象。
所以,此时[NSObject test]
便会调用NSObject中的test
对象方法;
8. 以下代码的输出结果是什么,为什么?
NSArray *arr = [NSArray array];
NSArray *mtArr = [NSMutableArray array];
NSLog(@"%d", [arr isKindOfClass:[NSObject class]]);
NSLog(@"%d", [arr isMemberOfClass:[NSObject class]]);
NSLog(@"%d", [mtArr isKindOfClass:[NSObject class]]);
NSLog(@"%d", [mtArr isMemberOfClass:[NSObject class]]);
NSLog(@"%d", [arr isKindOfClass:[NSArray class]]);
NSLog(@"%d", [arr isMemberOfClass:[NSArray class]]);
NSLog(@"%d", [mtArr isKindOfClass:[NSArray class]]);
NSLog(@"%d", [mtArr isMemberOfClass:[NSArray class]]);
NSLog(@"%d", [mtArr isKindOfClass:[arr class]]);
NSLog(@"%d", [mtArr isMemberOfClass:[arr class]]);
输出:1010101000
你可能会对以下输出产生疑问:
NSLog(@"%d", [arr isMemberOfClass:[NSArray class]]);
// 输出0
NSLog(@"%d", [mtArr isKindOfClass:[arr class]]);
// 输出0
原因在于NSArray是一个类簇,意味着每个NSArray的实例都是NSArray内部子类的一个实例,我们会发现[[NSArray array] class]
返回值为__NSArray0
,[[NSMutableArray array] class]
返回值为__NSArrayM
, 而[NSArray class]
返回值为NSArray
,所以二者不等。
8. 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
无论在MRC下还是ARC下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的object_dispose()方法中释放。
1)调用-release
引用计数变为零 对象正在被销毁,生命周期即将结束.
不能再有新的__weak
弱引用,否则将指向nil
.
调用[self dealloc]
;
2)父类调用-dealloc
继承关系中最直接继承的父类再调用-dealloc
如果是 MRC 代码 则会手动释放实例变量们(iVars)
继承关系中每一层的父类 都再调用-dealloc
3)NSObject 调用-dealloc
只做一件事:调用 Objective-C runtime 中object_dispose()
方法
4)调用object_dispose()
为 C++ 的实例变量们(iVars)
调用destructors
为 ARC 状态下的 实例变量们(iVars) 调用 -release
解除所有使用 runtime Associate方法关联的对象
解除所有__weak
引用
调用free()
网友评论