iOS常见面试题——Runtime
类
[self class]和[super class]返回的分别是什么?
[self class]会被编译器转化成 Objc_msgSend(self, @selector(class))
[super class]会被编译器转化成 objc_msgSendSuper(super, @selector(class)),super在这里只是一个结构体(objc_super),objc_super结构体重有一个 receiver的变量,也就是方法的接收者,这里的接收者也是当前对象self。
super实际调用的方法所以这两个消息传递的接收者都是当前对象self,两个方法的运行结果也就相同,都会返回当前对象的class。
讲一下对象、类对象、元类跟元类的结构体是如何关联的?
所有的OC对象的基础结构体都是objc_object,而类和元类的结构体objc_class也是继承于objc_object的,所以类和元类其实也是一种“对象”。
objc_object有isa指针,对象、类对象、元类对象正是通过isa指针相连。
isa指向为什么对象方法没有保存到对象的结构体里,而是保存在类对象的结构体里?
有以下几点好处:
-
假如保存在对象中,当我已经创建一个对象后,再为这个类动态的添加一个对象方法,这时由于已经创建的对象的内存布局已经确定,也就没办法添加这个方法了,这个对象就会有问题。也就无法实现动态添加方法的功能。
-
将对象方法保存在类对象类,那么所有的对象方法只要在类对象中保存一份即可,而不需要再每个对象里都保存一份,会节省大量的内存。
-
正是因为方法保存在类对象中,而每次调用时通过isa指针找到类对象再找到方法调用,才保证了OC语言的动态特性,方法可以动态添加、修改,否则就不能实现动态的效果。
-
在方法的实际调用中有这样的过程:先找缓存->在找当前类中的对象方法列表->再逐级查找父类的对象方法列表,通过把常用的方法放入缓存中,及提高了访问速度,也节省了不必要的内存开销。通过把对象方法放在父类才能很好的实现这个功能,放在对象中实现的话,就非常浪费内存。
class_ro_t和class_rw_t的区别?
根据结构体名称就可以得知一个是只读类型、一个是可读写型。
class_ro_t是class_rw_t的一个变量,用于保存宿主类的成员变量列表信息、协议列表、对象方法列表、属性列表信息(这三个列表都是一位数组),这些是在类注册到runtime中就固定了,无法再修改。
class_rw_t的成员变量除了class_ro_t之外,还有methods protocols property三个二维数组,之所以是二维数组是因为一个类可以有多个分类,而每个分类都可以有多个方法、协议、属性,所以就是二维数组。class_rw_t是可以动态修改的,当运行时动态的给一个类添加一个方法时,这个方法就保存在methods中。
这里再说明一下,类在编译时初始化时会创建class_ro_t结构体,里边的内容都是只读的,在运行时初始化会创建class_rw_t,将class_rw_t的ro指针指向之前创建的class_ro_t结构体,并将class_ro_t中的方法列表添加到class_rw_t的methodList中,在之后进行方法查找时就不需要再查class_ro_t了。
iOS中内省的几个方法?class方法和objc_getClass方法有什么区别?
内省是对象揭示自己作为一个运行时对象的详细信息的一种能力,这些信息包括对象在继承树上的位置、对象是否遵循特定协议、以及是否响应特定的消息。
NSObject协议和类定义了很多内省方法用于查询运行时信息,以便对对象的特征进行识别。
[self class]方法,获取当前对象的所属类,以此可以判断出非常多的信息,包括具有什么属性、方法、协议、成员变量等。
isKindOfClass方法和isMemberOfClass可以检查类是否从属某个类。
responseToSelector是否实现了特定方法
conformsToProtocol是否遵循了某个正式协议(对于非正式协议用responseToSelector来判断是否实现具体方法)
[obj class] 和 objc_getClass(obj)的区别:
objc_getClass方法返回的是isa指针的指向,而class主要是返回当前实例或类的Class
-
当obj为实例对象,两者都返回 类对象Class
-
当obj为类对象,class方法返回自身, objc_getClass方法返回元类对象
-
当obj为元类对象,class方法返回自身,objc_getClass方法返回根元类对象
-
当objc为根元类对象,class方法返回自身,objc_getClass方法返回自身
isa指向图
SEL和IMP的区别?
SEL相当于是方法名,IMP则是函数实现,在类对象的方法缓存哈希表中,SEL和IMP相当于对应着key和value。
能否向编译后的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
不能,当一个类已经编译好了,它就已经注册到runtime中,其对象的内存布局已经确定,无法再修改。假如能修改的话,当我已经创建了一个对象A,这时这个A的内存大小和布局已经确定,如果这时动态向类A中增加一个Int型的变量,则意味着新的对象的内存大小和布局都要改变,而这个对象A已经确定下来无法改变,也就成了废对象,这会引起严重的后果。
但在运行时创建的类还没注册到runtime中,可以为其添加成员变量,因为此时的内存布局还未确定下来。
在运行时创建类的方法objc_allocateClassPair的方法名尾部为什么是pair(成对的意思)?
一个是类对象,一个是元类对象,所以是pair
消息机制
谈一谈对消息机制的理解
runtime的核心就是消息机制,而runtime又是OC语言的核心。当我们正常通过OC语言调用一个方法[obj func]时,并不是直接调用其函数实现,而是向obj对象发送了一个消息。至于具体的方法实现是什么,在编译时是并不知道的,只有在运行时才能确定下来。这就是消息机制,而通过这种消息的方式,则体现了OC语言的动态属性,使用起来更加灵活。
具体在调用[obj func]时,是在调用obj对象的名称为func的对象方法,对象方法是保存在类对象中的,所以obj会通过其isa指针找到其类对象,然后先查方法缓存中是否已经有此方法(具体查找方法为哈希查找)。如果没找到的话,就在类对象的对象方法列表中查找func方法(如果方法列表已经排过序就按二分法查找,如果没排序就按遍历查找,并且以先查找分类的方法,后查找宿主类的方法)。如果还没有查找到,就逐级查找父类的方法缓存和方法列表。如果找到func方法,就把其IMP缓存到obj的类对象的方法缓存中。如果到根类都没找到的话,就进入到消息转发流程。
objc在向一个对象发送消息的时候,发生了什么?
同上题
消息转发
消息转发的流程
-
动态解析环节:查看当前类有没有实现动态解析 +(BOOL)resolveInstanceMethod:(SEL)selector 或者 +(BOOL)resolveClassMethod:(SEL)selector 如果已经实现就调用。使用这个方法解决问题的前提是,相关方法的实现代码已经存在,只是要在运行时动态的插到类里面。此方法可以用来实现@dynamic(阻止编译器自动生成类方法)
-
快速转发环节:如果没实现就进入到快速转发流程 - (id)forwardingTargetForSelector:(SEL)selector;将当前selector消息转发到其他对象上。
-
完整转发环节:如果还没实现则进入到最终的转发流程
3.1 先调用-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector,根据selector返回一个方法签名,这个签名按规则包含了返回值类型、传参类型(最常见的“v@:”,v表示返回值为void类型,@表示参数为id类型,:表示参数为SEL)
3.2 再调用 -(void)forwardInvocation:(NSInvocation *)invocation;可以进行消息转发。如果已经重写了此方法,那么即使没有实现转发,也不会崩溃了,因此消息转发也常用来兼容crash情况。
消息转发到forwarding invocation方法,如何拿到返回值?
invocation方法,有getReturnValue方法,可以获取返回值。
runtime使用
什么是methods swizzling(hook)?
方法混淆,将两个方法的实现交换,通过runtime的method_exchangeImplementation方法进行交换。常用方法混淆来交换系统的某个方法,添加一些log信息,便于调试。
不过由于交换过一次就不用再交换了,所以一般方法混淆都在相关类的+(void)load方法中进行。
@implementation NSObject (Hook)
+ (void)hookWithNewSelector:(SEL)newSelector originalSelector:(SEL)originalSelector {
IMP originalImp = class_getMethodImplementation(self, originalSelector);
IMP newImp = class_getMethodImplementation(self, newSelector);
Method newMethod = class_getInstanceMethod(self, newSelector);
const char *types = method_getTypeEncoding(newMethod);
BOOL addFlag = class_addMethod(self, originalSelector, newImp, types);
if (addFlag) {//添加方法成功,当前类没有实现该方法,用的是父类的方法
//用新的方法实现,替换父类的方法实现
class_replaceMethod(self, newSelector, originalImp, types);
} else {//添加方法失败,当前类已经实现了该方法
//用新的方法替换 当前类的方法
Method originalMethod = class_getInstanceMethod(self, originalSelector);
method_exchangeImplementations(originalMethod, newMethod);
}
}
@end
@implementation NSObject (Crash)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self hookWithNewSelector:@selector(crash_forwardingTargetForSelector:) originalSelector:@selector(forwardingTargetForSelector:)];
});
}
- (id)crash_forwardingTargetForSelector:(SEL)aSelector {
Method currentClassMethod = class_getInstanceMethod(self.class, @selector(forwardInvocation:));
Method NSObjectMethod = class_getInstanceMethod(NSObject.class, @selector(forwardInvocation:));
if (currentClassMethod == NSObjectMethod) {
return [CrashManager shareInstance];
} else {
return [self crash_forwardingTargetForSelector:aSelector];
}
}
@end
如图中代码,我做了一个简易的recognized selector crash收集器,通过hook的方式(method swizzling)将NSObject的forwardingTargetForSelector跟我自己的crash_forwardingTargetForSelector进行调换,判断当前类是否已经重写了forwardInvocation方法(即是否有自己的消息转发逻辑),如果有 则调用[self crash_forwardingTargetForSelector:aSelector],交由它自己去处理消息转发流程。否则,交给[CrashManager shareInstance]来执行,其内部进行一个崩溃分析,并可以选择上报崩溃报告。(不过这个模型非常简陋,实际项目中不能这么简单的处理)
这里需要说明一下,图中画红线的部分看起来始终调用当前方法,应该会死循环啊?但实际上不会,在执行这个方法的时候,NSObject的forwardingTargetForSelector方法已经跟我的crash_forwardingTargetForSelector调换过了,所以实际上执行的是forwardingTargetForSelector方法,所以不会死循环。
实际执行的内容
hook在iOS项目中有非常广泛的应用,比如通过Aspects库实现AOP,就是通过hook来实现的。不过我们通常用hook主要是进行一些日志的插入,而不应该真的更换系统方法的实现,这样做是有风险的。