OC底层原理20:OC底层面试题解析
【面试题1】Runtime Associate 方法关系的对象,需要在dealloc中释放?
当我们对象释放时,会调用 dealloc
- C++函数释放:
objc_cxxDestruce
- 移除关联属性:
_object_remove_associations
- 将弱引用自动设值nil:
weak_clear_no_lock(&table_weak_table。(id)this);
- 引用计数处理:
table_refcnts_erase(this)
- 销毁对象:
free(obj)
所以,关联对象 不需要我们手动移除,会在对象析构即 dealloc 时释放
dealloc源码
dealloc的源码查找路径:dealloc -> _objc_rootDealloc -> rootDealloc -> object_dispose(释放对象) -> objc_destructInstance -> _object_remove_assocations
- 在objc源码中搜索
dealloc
的源码实现
- 进入
_objc_rootDealloc
源码实现,主要是对对象进行析构
- 进入
rootDealloc
源码实现,发现其中有关联属性时设置bool值
,当有这些条件时,需要进入else流程
- 进入
object_dispose
源码实现,主要是销毁实例对象
- 进入
objc_destructInstance
源码实现,在这里有一处关联属性的方法
- 进入
_object_remove_assocations
源码,关联属性的移除,主要是从全局哈希map中招待相关对象的迭代器,然后将迭代器中关联属性,从头到尾的移除
【面试2】方法的调用顺序
类的方法
和 分类方法
重名,如果调用,是什么情况?
如果同名方法是
普通方法
,包括initialize
–先调用分类方法
- 因为
分类的方法是在类realize之后attach进去的
,插在类的方法的前面,所以优先调用分类的方法
(注意:不是分类覆盖主类!!),所以你看到的只有类的initialize
- 因为
* `initialize` 方法什么时候调用?`initialize` 方法也是 `主动调用`,即 `第一次消息时` 调用,为了不影响整个load,可以将需要 `提前加载的数据` 写到 `initialize` 中
如果同名方法是
load
方法 – 先主类load
,后分类load(分类之间,看编译的顺序)
- 原因:参考 iOS-底层原理18:类的加载(下) 文章中的
load_images
原理分析
- 原因:参考 iOS-底层原理18:类的加载(下) 文章中的
【面试3】Runtime是什么?
runtime
是由C和C++
汇编实现的一套API
,为OC
语言加入了面向对象、以及运行时的功能
运行时是指将
数据类型的确定由编译时推到了运行时
- 举例:
extension
和category
的区别
- 举例:
平时编写的
OC
代码,在程序运行过程中,其实最终会转变成runtime
的C语言代码
,runtime是OC的幕后工作者
1. category类别、分类
专门用来给类添加新的方法
不能给类添加成员变量
,添加了成员属性,也无法取到注意:其实
可以通过runtime给分类添加属性
,即属性关联,重写setter、getter方法分类中用
@property
定义变量,只会生成
变量的setter、getter
方法的声明
,不能生成方法实现和带下划线的成员变量
2. extension 类扩展
可以说成是
特殊的类
,也可以称作匿名的类
可以
给类添加成员属性
,但是是私有变量
可以
给类添加方法
,也是私有方法
【面试4】方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?
方法的本质:
发送消息
,消息会有以下几个流程快速查找(
objc_msgSend
)->cache_t
缓存消息中查找慢速查找 -
递归自己|父类
->lookUpImpOrForward
查找不到消息 ->
动态方法解析
-resolveInstanceMethod
消息快速转发 -
forwardingTargetForSelector
消息慢速转发 -
methodSignatureForSelector & forwardInvocation
sel
是方法编号
- 在read_images
期间就编译进了内存,相当于一本书的目录title
imp
是函数实现指针
,找imp就是找函数的过程
,相当于书本的页码
查找具体的函数就是想看这本书具体篇章的内容
- 首先知道想看什么,即目录 title-sel
- 根据目录查找到对应的页码 - imp
- 通过页码去翻到具体的内容
【面试5】能否像编译后得到的类中增加实例变量?能否像运行时创建的类中添加实例变量?
不能
像编译后得到的类中增加实例变量- 只要类没有注册到内存还是可以添加的
- 可以
添加属性+方法
【原因】 :编译好的实例变量存储的位置是ro,一旦编译完成,内存结构就完全确定了
【面试6】[self class] 和 [super class]的区别以及原理分析
[self class]
就是发送消息objc_msgSend
,消息接收者是self
,方法编号是class
[super class]
本质就是objc_msgSendSuper
,消息的接收者还是self
,方法编号class
,在运行时,底层调用的是_objc_msgSendSuper2
【重点!!!】只是
objc_msgSendSuper2
会更快,直接跳过self
的查找
【代码调试】
ZJPerson
中的init
方法中打印这两种class
调用
- 进入
[self class]
中的class
源码
其底层是 获取对象的isa
,当前的 对象是ZJPerson
,其isa是其同名的 ZJPerson
,所以 [self class]
打印的是 ZJPerson
- [super class]中,其中
super
是语法的关键字
,可以通过clang
看super
的本质,这是编译时
的底层源码,其中第一个参数是消息接收者,是一个__rw_objc_super
结构
- 底层源码中搜索
__rw_objc_super
,是一个中间结构体
- objc中搜索
objc_msgSendSuper
,查看其隐藏参数
- 搜索
struct objc_super
通过 clang
的底层编译代码可知,当前 消息的接收者
是 self
,而 self
等于 ZJPerson
,所以 [super class]
进入 class
方法来源后,其中的 self是init后的实例对象,实例对象的 isa
指向的是本类,即消息接收者是 ZJPerson
本类
- 我们再来看
[super class]
在运行时是否如上一步的底层编码所示,是objc_msgSendSuper
,打开汇编调试,调试结果如下
- 搜索
objc_msgSendSuper2
,从注释得知,是从类开始查找的
,而不是父类
- 查看
objc_msgSendSuper2
的汇编源码,是从superclass
中的cache
中查找方法
1 | ENTRY _objc_msgSendSuper2 |
【完成回答】
[self class]
方法调用的本质是发送消息
,调用class的消息流程,拿到元类的类型
,在这里是因为类已经加载到内存,所以在读取时是一个字符串类型,这个字符串类型是在map_images
的readClass
时已经加入表中,所以打印为ZJPerson
[super class]
打印的是ZJPerson
,原因是当前的super
是一个关键字
,在这里只是调用objc_msgSendSuper2
,其实他的消息接收者和[self class]是一模一样的,所以返回的是ZJPerson
【面试7】内存平移问题
ZJPerson中有一个 属性zj_name
和 实例方法saySomething
,通过下面代码这种方式能否调用实例方法?为什么?
1 | @interface ZJPerson : NSObject |
【代码调试】
- 我们在日常开发中的调用方式是下面这种
1 | // 常见的调用实例方法 |
- 通过运行发现,是可以执行的,打印结果如下:
1 | 2022-03-16 10:30:27.686250+0800 load+initialize-demo[6296:4626148] -[ZJPerson saySomething] |
[person saySomething]
的本质是对象发送消息
,那么当前的person
是什么?person
的isa
指向类ZJPerson
, 即person
的首地址指向 ZJPerson
的首地址,我们可以通过ZJPerson
的内存平移找到cache
,在cache
中查找方法
[(__bridge id)zj saySomething]
中的zj
是来自于ZJPerson
这个类,然后有一个指针zj
,将其指向ZJPerson的首地址
所以,person
是指向 ZJPeron
类的结构,zj
也是指向 ZJPerson
类的结构,然后都是在 ZJPerson
中的 methodList
中查找方法
修改:saySomething里面有属性 self.zj_name 的打印
1 | - (void)saySomething{ |
查看这两种调用方式的打印结果,如下所示
zj
方式的调用打印的zj_name
是<ViewController: 0x100e06eb0>
- person 方式调用打印的
zj_name
是(null)
为什么会出现打印不一致的情况?
- 其中
person
方式的zj_name
是由于self指向person的内部结构
,然后通过内存平移8字节,取出去zj_name
,即self指针首地址平移8字节获取
【方式一】其中
zj
指针中没有任何,所以zj表示8字节指针
,self.zj_name
的获取,相当于zj首地址的指针也需要平移8字节找zj_name
,那么此时的zj的指针地址是多少?平移8字节获取的是什么?zj
是一个指针,是存在栈
中的,栈是一个先进后出
的结构,参数传入就是一个不断压栈的过程其中
隐藏参数会压入栈
,且每个函数都会有两个隐藏参数(id self,sel _cmd)
,可以通过clang
查看底层编译隐藏参数压栈
的过程,其地址是递减
的,而栈是从高地址 -> 低地址分配
的,即在栈中
,参数会从前往后一直压
super通过clang查看底层的编译,是
objc_msgSendSuper
,其第一个参数是一个结构体__rw_objc_super(self,class_getSuperclass)
,那么结构体中的属性时如何压栈的?可以通过自定义一个结构体,判断结构体内部成员的压栈情况p &person3
p *(NSNumber **)0x000000016d2f59f0
p *(NSNumber **)0x000000016d2f59f8
1 | struct zj_struct{ |
所以图中可以得到 20先加入,再加入10,因此 结构体内部
的压栈情况是 低地址 -> 高 地址
,递增
的,栈中 结构体内部
的成员是 反向
压入栈,即 低地址 -> 高地址
,是递增的
所以到目前为止,栈中 从高地址到低地址 的顺序的:
self -> _cmd -> (id)class_getSuperclass(objc_getClass("ViewController")) -> self -> cls -> zj -> person
self
和_cmd
是viewDidLoad
方法的两个隐藏的参数,是高地址 -> 低地址正向压栈
的class_getSuperClass
和self
为objc_msgSendSuper2
中的结构体成员,是从最后一个成员变量,即低地址 -> 高地址反向压栈
的
可以通过下面这段代码打印下栈的存储是否如上面所说
其中为什么 class_getSuperclass
是 viewController
,因为 objc_msgSendSuper2
返回的是 当前类
,两个self,并不是同一个self,而是栈的指针不同,但是指向同一片内存空间
[(__bridge id)zj saySomething]
调用时,此时的zj
是ZJPerson:0x16bbc1a08
,所以saySomething
方法中传入的self
还是ZJPerson
,但并不是我们通常认为的ZJPerson
,使我们当前传入的消息接收者
,即ZJPerson:0x16bbc1a08
,是ZJPerson的实例对象,此时操作与普通的ZJPerson是一致的,即ZJPerson的地址内存平移8字节
普通person流程:
person -> zj_name -> 内存平移8字节
zj流程:
0x16bbc1a08 + 0x8 - > 0x16bbc1a10
,即为self
,指向<ViewController: 0x10470a370>
,如下图所示
其中 person
与 ZJPerson
的关系是 person是以ZJPerson为模板实例化对象
,即alloc有一个指针地址,指向isa,isa指向ZJPerson
,它们之间关联是由一个isa指向
而zj也是指向ZJPerson的关系,编译器会人为 zj也是ZJPerson的一个实例化对象
,即 zj相当于isa,即首地址,指向ZJPerson
,具有和person一样的效果,简单来说,我们已经完全将编译器骗过了,即zj也有zj_name,由于 person查找zj_name是通过内存地址平移8字节
,所以zj也是通过内存地址平移8字节去查找zj_name。
哪些东西在栈里?哪些在堆里?
alloc
的对象都在堆
中指针、对象
在栈
中,例如person指向的空间
在堆
中,person所在的空间
在栈
中临时变量
在栈
中属性值
在堆
,属性随对象是在栈
中
【注意】
堆
是从小到大,即低地址 -> 高地址栈
是从大到小,即从高地址 -> 低地址分配- 函数隐藏参数会
从前往后
一直压,即从高地址 -> 低地址 开始入栈
。 - 结构体内部成员是
从低地址 -> 高地址
- 函数隐藏参数会
一般情况下,内存地址有如下规则
0x60
开头表示在堆
中0x70
开头的地址表示在栈
中0x10
开头的地址表示在全局区域
中
【面试8】runtime是如何实现weak的,为什么可以自动置为nil?
- 通过
sideTable
找到我们的weak_table
weak_table
根据referent
找到或创建weak_entry_t
- 然后
append_referrer(entry,referrer)
将我的新弱引用的对象加进去entry
- 最后
weak_entry_insert
,把entry
加入到我们的weak_table
底层源码调用流程如下图所示
- Post title:OC底层原理20:OC底层面试题解析
- Post author:张建
- Create time:2020-12-15 15:22:19
- Post link:https://redefine.ohevan.com/2020/12/15/OC底层原理/OC底层原理20:OC底层面试题解析/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.