iOS底层学习 - Runtime之砖厂面试答疑

经过对类,Runtime等底层的相关探索,对原理已经有了掌握,本章经过几个面试题来加深印象,会保持持续更新。面试

先上几篇原理文章,对面试题的理解会更深入缓存

传送门☞iOS底层学习 - Runtime之方法消息的前世此生(一)bash

传送门☞iOS底层学习 - Runtime之方法消息的前世此生(二)markdown

1.什么是Runtime?

答:是由C 和C++ 汇编 实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能。平时编写的OC代码,在程序运⾏过程当中,其实最终会转换成Runtime的C语⾔代 码,RuntimeObjective-C 的幕后⼯做者。app

好比:将数据类型的肯定由编译时推迟到了运⾏时,比较典型的就是类的rorw属性。ro在编译期就肯定好(read-only),而rw是运行时才肯定,能够进行修改(read-write)。像下面的例子就比较典型👇框架

2.方法的本质是什么?

方法的本质就是消息的发送,就底层_objc_msgSennd方法寻找方法IMP的过程,主要经历了如下几个步骤:ide

  1. 快速查找流程:经过汇编(objc_msgSend)查找cache_t中缓存的消息
  2. 慢速查找流程:经过C代码函数lookUpImpOrForward递归查找当前类和父类的rwmethodlist的方法
  3. 动态方法解析:查找不到方法后进入此流程,经过调用和实现resolveInstanceMethod方法,来实现消息动态处理
  4. 消息快速转发:无方法无动态解析进入此流程,经过CoreFoundation框架来触发消息转发流程,forwardingTargetForSelector实现快速转发,其余类可实现处理方法
  5. 消息慢速转发:经过实现methodSignatureForSelector方法,来获取到方法的签名,从在生成相对应的invocation,经过forwardInvocation来对invocation进行处理,通常处置崩溃都在此处理
  6. 未找到消息:没法找到IMP,形成崩溃,打印log

3.sel是什么?IMP是什么?二者之间的关系⼜是什么?

SEL是方法编号,也是方法名,在dyld加载镜像带内存时,经过_read_image方法加载到内存的表中了函数

IMP 就是咱们函数实现指针 ,找IMP就是找函数的过程oop

SEL就至关于书本的⽬录 tittle,post

IMP 就是书本的⻚码,

函数就是具体页码对应的实现内容

查找具体的函数就是想看这本书⾥⾯具体篇章的内容

  1. 咱们⾸先知道想看什么 ~ tittle (sel)
  2. 根据⽬录对应的⻚码 (imp)
  3. 翻到具体的内容

4.可否向运⾏时建立的类中添加实例变量?

不能。

由于咱们编译好的实例变量存储的位置在ro,⼀旦编译完成,内存结构就彻底肯定 就⽆法修改,只能修改rw中的方法或者能够经过关联对象的方式来添加属性

关联对象添加的主要步骤以下:

  1. objc_setAssociatedObject设置set方法:找到关联对象的总哈希表,而后经过指针地址找到该类的哈希表,而后经过key值进行存储
  2. objc_getAssociatedObject设置get方法:和set方法同样查询表,找到值
  3. 在类的dealloc会清除关联对象的哈希表

5.isKindOfClassisMemberOfClass区别

原理探究

先上例子🌰

1.第一个

BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];       
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];     
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
复制代码

👆上述Log的打印结果为1,0,0,0

2.第二个

BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       // 1
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     // 1
BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]];       // 1
BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];     // 1
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
复制代码

👆上述Log的打印结果为1,1,1,1

相信对于第二个例子,你们都使用的很是熟练。咱们发现两个例子的主要区别在于消息接受者是实例对象仍是类对象,打印结果,咱们看源码探究

既然[NSObject class]类也能够调用这两个方法,说明这两个方法是有对应的类方法和实例方法的,只不过咱们平时不适用类方法而已😂

/*********************************************************************** * object_getClass. * Locking: None. If you add locking, tell gdb (rdar://7516456). **********************************************************************/
Class object_getClass(id obj) {
    if (obj) return obj->getIsa();
    else return Nil;
}
复制代码
✅//object_getClass()取得的是对象的isa指针指向的对象,也就是判断传入的类对象的元类对象是否与传入的这个对象相等,因此这个cls应该是元类对象才有可能相等
 + (BOOL)isMemberOfClass:(Class)cls {
 return object_getClass((id)self) == cls;
 }

 ✅//判断传入的实例对象的类对象是否与传入的对象相等,因此cls只有多是类对象才有可能相等
 - (BOOL)isMemberOfClass:(Class)cls {
 return [self class] == cls;
 }

✅//循环判断传入的类对象的元类对象及其父类的元类对象是否等于传入的cls
 + (BOOL)isKindOfClass:(Class)cls {
 for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
 if (tcls == cls) return YES;
 }
 return NO;
 }

✅//循环判断实例对象的父类的类对象是否等于传入的对象cls,也就是判断实例对象是不是cls及其子类的一种
 - (BOOL)isKindOfClass:(Class)cls {
 for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
 if (tcls == cls) return YES;
 }
 return NO;
 }
复制代码

经过这个两个方法的源码咱们能够知道,

  • isMemberOfClass:是检测方法调用者对象的类是否等于传入的这个类。
  • isKindOfClass:是判断方法调用者对象的类是否等于传入的这个类或者其子类。

小结

还有一个适用于这四个方法的一点是,若是方法调用者是实例对象,那么传入的就应该是类对象;若是方法调用者是类对象,那么传入的就应该是元类对象。

分析BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];为啥打印是1呢?

答:[NSObject class]类对象调用isKindOfClass代表其元类会递归判断是否等于当前类或者其父类,咱们知道NSObject的元类为根元类,根据继承链关系根元类的父类即为NSObject类对象,即[NSObject class],因此相等

6.[self class]和[super class]的区别以及原理分析

原理探究

建立一个Student类继承子Person类,下面代码打印出什么

NSLog(@"[self class] = %@", [self class]);
NSLog(@"[super class] = %@", [super class]);
NSLog(@"[self superclass] = %@", [self superclass]);
NSLog(@"[super superclass] = %@", [super superclass]);
复制代码

先上正确答案:

2020-01-17 15:54:02.224686+0800 TEST[8409:174143] [self class] = Student
2020-01-17 15:54:02.224922+0800 TEST[8409:174143] [super class] = Student
2020-01-17 15:54:02.225040+0800 TEST[8409:174143] [self superclass] = Person
2020-01-17 15:54:02.225922+0800 TEST[8409:174143] [super superclass] = Person
复制代码

咱们发现第二个和第四个和咱们猜测的貌似不太同样,并非Person,先上源码看一下第一个和第三个的理解

/*******************************************************
✅ //经过对象的isa指针获取类的类对象
Class object_getClass(id obj)
 {
 if (obj) return obj->getIsa();
 else return Nil;
 }

 + (Class)class {
 return self;
 }

 - (Class)class {
 return object_getClass(self);
 }

 + (Class)superclass {
 return self->superclass;
 }

 - (Class)superclass {
 return [self class]->superclass;
 }

Class class_getSuperclass(Class cls)
{
    if (!cls) return nil;
    return cls->superclass;
}
******************************************************/
复制代码

咱们知道,这里方法中的self都是指消息的接受者,在问题中就表示Student类,根据以上源码,第一个和第三个没啥疑问,都是寻找Studentisa和父类,那么super调用时,有啥不一样,咱们须要先知道super的本质

经过clang编译代码,咱们能够发现,底层调用时super调用的方法不是magSend,而是objc_msgSendSuper,传入了一个super的结构体和方法,而super的结构体以下

objc_msgSendSuper(object ,superclass, @selector(class))
复制代码
struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};
复制代码

两个参数分别为消息的接受者和父类,在这里Student即为消息接受者,Person为父类.

咱们知道消息发送的时候,慢速查找流程是须要从自身递归查找到NSObject的,而objc_msgSendSuper就表示直接从消息接受者的父类开始递归查找,跳过了自己的方法列表,这样查找的速度能够更快

因此调用[super class][super superclass]本质上消息的接受者仍是self,即Student类,因为class方法的实现,实际上是在基类NSObject中的,因此不论是从Student类方法列表开始查询,仍是从父类Person方法列表查询,最终都会走到基类中的class方法,而此方法根据源码就是寻找消息接受者的isasuperclass,因此出现上述的打印结果

小结

[self class] 就是发送消息objc_msgSend,消息接受者是 self,⽅法编号:class

[super class] 本质就是objc_msgSendSuper, 消息的接受者仍是 self ⽅法编号:class

只是objc_msgSendSuper 会更快 直接跳过 self 的查找,可是都会走到NSObject基类的实现方法中,可是都是以self为接受者

7.Runtime是如何实现weak的,为何能够⾃动置nil?

主要总结以下:

  1. 经过SideTable找到咱们的weak_table

  2. weak_table 根据referent 找到或者建立 weak_entry_t

  3. 而后append_referrer(entry, referrer)将新弱引⽤的对象加进去entry

  4. 最后weak_entry_insertentry加⼊到咱们的weak_table

  5. 在类dealloc时,会根据插入的步骤找到对应的弱引用,并置为nil

关于weak的相关知识作了单独的总结,详情能够看下方文章👇

iOS底层学习 - 内存管理之weak原理探究

8.Method Swizzling的坑与应⽤

关于Method Swizzling方法交换的总结,详情能够看下面的文章:

iOS底层学习 - Runtime之Method Swizzling黑魔法

9.压轴大题:下列代码可否运行,打印结果是什么?

题目

***********************LGPerson***************************
@interface LGPerson : NSObject
@property (nonatomic, copy)NSString *name;
//@property (nonatomic, copy)NSString *subject;
//@property (nonatomic)int age;

- (void)print;
@end

@implementation LGPerson
- (void)print{
    NSLog(@"NB %s - %@",__func__,self.name);
}
@end

**************************调用*****************************
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *tem = @"WY";
    id cls = [LGPerson class];
    void *obj= &cls;
    [(__bridge id)obj print];
    
    //    LGPerson *person = [LGPerson alloc];
    //    [person print];
    
}

复制代码

运行结果

2020-01-20 17:40:15.322404+0800 LGTest[86411:20872420] NB -[LGPerson saySomething] - KC
复制代码

相信看到这个结果你们都是和我同样一脸懵逼。下面就分为2个问题,咱们来研究一下

问题1: 可否运行

经过结果也看到了,这段代码是能够运行的。而且成功调用了[LGPerson saySomething]方法,咱们首先看为什么能成功调用,它和咱们注释的普通的实例对象调用为什么能同样。

首先来看一下普通方法调用的时序:

  • 实例对象p的指针 --> 实例对象p的isa --> [LGPerson class]类地址

如图所示:

接着来分析代码中的执行时序:

  1. id cls = [LGPerson class];获取到类对象指针
  2. void *obj= &cls;获取到指向该类对象cls的对象obj
  3. [(__bridge id)obj print];消息发送

按照上面的时序的结构总结一下,就是:

  • 指针obj --> 指针cls --> [LGPerson class]类地址

因此从本质上来讲,最后消息发送的时候,都是根据isa获取到类的内存空间,而后再方法列表中查找IMP。而obj指向的是一个cls,可是cls正巧也是指向类的内存空间的,因此一样也能够找到方法列表中的print方法,进行调用

问题2: 打印内容及为何会打印这个结果

经过运行结果咱们能够看到,方法调用self.name居然打印出了VC中的NSString的临时变量的值,那么为何会出现这个结果呢?

咱们知道任何OC方法的底层都是一个C函数,而且函数头两个参数是默认参数id selfSEL _cmd,那么self是谁呢

首先咱们仍是来看一下正常的调用self.name是如何找到的

  • 此时,咱们知道消息发送时的self(消息接受者)指的就是咱们实例化的对象person,而person指向的是实例对象的内存空间首地址,而内存空间首地址是第一个元素isa,占用8字节,name是第二个元素,也是占用8个字节。寻找self.name的过程就是指针偏移的过程,由于isa占用了8个,因此找到name的值时,只须要向后便宜8个字节便可。如图所示:

那么咱们再来看一下代码中时如何找到self.name

经过上面的时序,咱们知道这二者在调用方法上是对等的。这里消息发送时的self(消息接受者)指的就是objcls指针至关于person指针所指向的实例对象里面的isa指针,同理,此时指向的类的首地址,可是要找的是name,向下指针偏移8个后,找到了生命的临时变量的值,因此会打印出来

函数的栈空间简介

这里就涉及到了为何会找到临时变量的问题,若是去掉了临时变量,又会打印什么呢,这里就有了函数栈空间的做用

栈空间的做用,是用来存放被调用函数其内部所定义的局部变量的。咱们都知道栈的特色是先入后出,因此先存进栈的在底层。因为方法的调用和super的调用,都会产生局部变量,因此viewdidload的栈示意图以下:

此时代码指向的就是图中橘色obj的首地址,若是没有临时变量,便宜8位后,就会指向 self,即当前的 viewcontroller

若是加了一个临时变量NSString,栈的结构就会变成以下所示,因此会打印它的值

能够看出此时对象的实例变量获取即为void *ivar = &obj + offset(N)

可是咱们指针偏移的offset不正确,获取不到对应变量的首地址,此时就会出现野指针的状况,因此千万不能像代码中那么用

参考

iOS底层学习 - OC对象前世此生

Runtime笔记(八)—— 面试题中的Runtime

神经病院 Objective-C

神经病院objc runtime入院考试

相关文章
相关标签/搜索