面试题

iOS常见面试题——Runtime

2019-12-01  本文已影响0人  空山和新雨

[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指向

为什么对象方法没有保存到对象的结构体里,而是保存在类对象的结构体里?

有以下几点好处:

  1. 假如保存在对象中,当我已经创建一个对象后,再为这个类动态的添加一个对象方法,这时由于已经创建的对象的内存布局已经确定,也就没办法添加这个方法了,这个对象就会有问题。也就无法实现动态添加方法的功能。

  2. 将对象方法保存在类对象类,那么所有的对象方法只要在类对象中保存一份即可,而不需要再每个对象里都保存一份,会节省大量的内存。

  3. 正是因为方法保存在类对象中,而每次调用时通过isa指针找到类对象再找到方法调用,才保证了OC语言的动态特性,方法可以动态添加、修改,否则就不能实现动态的效果。

  4. 在方法的实际调用中有这样的过程:先找缓存->在找当前类中的对象方法列表->再逐级查找父类的对象方法列表,通过把常用的方法放入缓存中,及提高了访问速度,也节省了不必要的内存开销。通过把对象方法放在父类才能很好的实现这个功能,放在对象中实现的话,就非常浪费内存。

class_ro_t和class_rw_t的区别?

根据结构体名称就可以得知一个是只读类型、一个是可读写型。

class_ro_t是class_rw_t的一个变量,用于保存宿主类的成员变量列表信息、协议列表、对象方法列表、属性列表信息(这三个列表都是一位数组),这些是在类注册到runtime中就固定了,无法再修改

class_ro_t结构体

class_rw_t的成员变量除了class_ro_t之外,还有methods protocols property三个二维数组,之所以是二维数组是因为一个类可以有多个分类,而每个分类都可以有多个方法、协议、属性,所以就是二维数组。class_rw_t是可以动态修改的,当运行时动态的给一个类添加一个方法时,这个方法就保存在methods中

class_rw_t结构体

这里再说明一下,类在编译时初始化时会创建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

  1. 当obj为实例对象,两者都返回 类对象Class

  2. 当obj为类对象,class方法返回自身, objc_getClass方法返回元类对象

  3. 当obj为元类对象,class方法返回自身,objc_getClass方法返回根元类对象

  4. 当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在向一个对象发送消息的时候,发生了什么?

同上题

消息转发

消息转发的流程

  1. 动态解析环节:查看当前类有没有实现动态解析 +(BOOL)resolveInstanceMethod:(SEL)selector 或者 +(BOOL)resolveClassMethod:(SEL)selector 如果已经实现就调用。使用这个方法解决问题的前提是,相关方法的实现代码已经存在,只是要在运行时动态的插到类里面。此方法可以用来实现@dynamic(阻止编译器自动生成类方法)

  2. 快速转发环节:如果没实现就进入到快速转发流程 - (id)forwardingTargetForSelector:(SEL)selector;将当前selector消息转发到其他对象上。

  3. 完整转发环节:如果还没实现则进入到最终的转发流程
    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主要是进行一些日志的插入,而不应该真的更换系统方法的实现,这样做是有风险的。

上一篇下一篇

猜你喜欢

热点阅读