OC底层原理二十:OC底层面试题
目录:
-
关联对象AssociationsManager
是否唯一
? -
分类方法
会覆盖本类方法
吗? - 所有
分类方法
都优先
于本类
吗? -
Runtime
是什么 - 方法的本质,
SEL
是什么?IMP
是什么?两者之间关系是什么? -
编译后
的类
能否添加实例变量
? - 能否向
运行时创建
的类
添加实例变量
? -
[self class]
和[super class]区别
和原理
分析 -
runtime
如何实现weak
,为什么可以自动
置为nil
? -
runtime Associate
方法关联
的对象
,是否需要在dealloc
中释放?
准备工作:
- 可编译的
objc4-781
源码: https://www.jianshu.com/p/45dc31d91000
1. 关联对象AssociationsManager
是否唯一
?
-
AssociationsManager
结构中,manager
只是对外代言人,并不是唯一的,AssociationsHashMap
哈希表才是唯一的。
1. 运行验证:
image.png
移除锁
,这样可以同时存在2个manager
了。
- 加入测试代码,创建2个
manager
,都调用get()
,发现2个读取的associations
是相同地址
。- 证明
AssociationsHashMap
在内存中是独一份的,而manager
只是外层包装,可以创建多个。
image.png
2. 分类方法
会覆盖本类方法
吗?
-
分类方法
会调用attachLists
,将分类方法
插入了本类方法
前面,全都存储
起来。并不是覆盖
本类方法。
详细的
attachLists
绑定流程: OC底层原理十八:类的加载(中) SEL & 分类的加载 中 4.4 attachLists 绑定数据
3. 所有分类方法
都优先
于本类
吗?
- 除了
+load
方法,其他分类方法
都会优先本类
。
(先
调用所有本类
的+load
方法,再
按顺序
去调用分类
的+load
)
1.
+load之外
的方法,分类方法
都会插入
本类方法前面
:
- 这个
attachLists
绑定流程: OC底层原理十八:类的加载(中) SEL & 分类的加载 中 4.4 attachLists 绑定数据,讲解得非常清楚。2.
+load
方法的顺序
为何是本类
在前面
?代码验证:
- 创建
HTPerson
类、HTStudent
类、HTPeroson(CatA)
分类、HTPeroson(CatB)
分类,都实现+load
方法:+(void)load { NSLog(@"%s", __func__); }
打印顺序如下:
image.png
先
打印了所有本类
的+load
方法,再
按顺序
去调用分类
的+load
源码分析
我们知道所有+load方法,都是在app启动过程中,
dyld
库调用objc库
的_objc_init
中,load_images
函数内进行的实现 (不熟悉的请看 【OC底层原理 学习大纲】 第5部分:dyld加载 & 类的加载)打开
image.pngobjc4源码
,搜索load_images
:
找到
prepare_load_methods
准备load函数和call_load_methods
调用load函数两部分:2.1 准备load函数
进入
image.pngprepare_load_methods
:
这里分别
image.png记录
了本类
和分类
的load函数
,先进入schedule_class_load
:
这里有
image.png递归
执行,添加操作是在add_class_to_loadable_list
中:
这里进行了
image.png读取
、检查扩容
、存储
操作。我们检查getLoadMethod
读取:
getLoadMethod
中从ro
中读取了类
的函数列表
,寻找并返回load
函数的Imp
。
------ 所有本类load
的函数列表已准备完毕 ------回到上面
image.pngadd_category_to_loadable_list
,检查分类的load列表
的准备:
检查
image.png分类
的读取_category_getLoadMethod
:
这里也从分类
的类方法列表
中找到load
函数并返回了imp
。
------ 所有分类load
的函数列表已准备完毕 ------2.2 调用load函数
image.png
进入
image.pngcall_load_methods
:
这里的do-while
循环内部,调用本类load
函数时,使用了while
循环。 所以load的调用顺序是:
先调用所有本类
的+load
方法,所有本类都没load方法时
,才调用分类的load方法
所以我们打印的结果,不仅
HTPerson本类
在CatA
和CatB
分类前面调用+load方法
,HTStudent本类
也在他们前面调用
4. Runtime
是什么
-
由
C
、C++
和汇编
实现的一套API
,为OC
语言加入了面向对象
、运行时
的功能 -
运行时(Runtime)
是指数据类型
的确定
,由编译器推
迟到运行时
(比如Extention
和category
的区别,extension
是编译期
就确定了,但是懒加载
的category
是在运行时
动态加入的)
5. 方法的本质,SEL
是什么?IMP
是什么?两者之间关系是什么?
-
方法
的本质
: 发送消息。
消息有以下几个流程:
- 快速查找(
objc_msgSend
)~cache_t
缓存信息中读取(汇编
) - 慢速查找(
lookUpImpOrForward
)~递归
自己/父类,从methodlist
中查找 - 查找不到消息时:
动态方法解析
~resolveInstanceMethod
- 消息快速转发 ~
forwardTargetForSelector
(更换实现对象) - 消息慢速转发 ~
methodSignatureForSelector
&forwardInvocation
(自定义实现,或飘走不管了~)
-
sel
是方法编号
~ 在read_images
期间,编译
进入内存
。
(sel
是带地址
的字符串
,方法排序
是依据SEL地址值
进行的排序
) -
imp
是函数实现
的指针
,找imp就是找函数的过程
SEL
和IMP
的关系
- 如同查字典,你想查
牛
的意思,首先在目录
通过niu
找到对应
的页数
,在页数内有关于牛
的详细解释
。
(sel
就是niu,imp
就是页码,而imp
指针指向的内容
,就是详细的解释)
6. 编译后
的类
能否添加实例变量
?
- 不可以。 因为编译好的实例变量存放的位置在ro,一旦编译完成,内存结构就完全确定了,无法修改。
7. 能否向运行时创建
的类
添加实例变量
?
- 在
register注册前
,可以添加
。但是调用运行时register注册后
,就完成了内存的注入
,内存结构确定了,无法修改
。
8. [self class]
和[super class]区别
和原理
分析
-
[self class]
就是发送消息objc_msgSend
,消息接受者是self
,方法编号(SEL)是class
-
[super class]
本质是objc_msgSendSuper
,消息接受者还是self
,方法编号是class
。
实际运行时,[super class]在汇编层
执行的是objc_msgSendSuper2
,直接从superclass
父类开始搜索,节约了一轮查找资源
测试代码:
@interface HTPerson : NSObject @end @implementation HTPerson - (instancetype)init { if (self = [super init]) { NSLog(@"%@ %@", [self class], [super class]); } return self; } @end int main(int argc, const char * argv[]) { @autoreleasepool { HTPerson * person = [[HTPerson alloc] init]; } return 0; }
image.png
- 打印结果:
都是HTPerson
1. 查看class实现:
image.png
进入
image.pngobject_getClass
,发现是getIsa()
读取的类
:
进入
image.pnggetIsa
,其实就是从指针优化
的isa
中,进行位运算
取出类cls
想知道
cls
是什么,我们必须确定消息
的接收对象
是谁,是谁的isa
clang
生成cpp
编译文件(clang -rewrite-objc main.m -o main.cpp
),打开main.cpp
文件:
- 搜索
HTPerson_init
,找到实例化
方法的编译结果
:
image.png可以看到
[self class]
,是使用objc_msgSend
发送的消息,第1个参数是self
,第2个参数是class
方法编号,执行对象是self
,测试代码中self
是HTPerson实例
。
所以读取到的是self
的isa指向的类
是HTPerson类
。[self class]
打印结果为HTPerson
而
image.png[super class]
编译后,是使用objc_msgSendSuper
发送消息,第1个参数是__rw_objc_super
结构体,第2个参数是class
方法编号。
执行对象
是第1个参数
,所以我们在当前文件中搜索__rw_objc_super
:
其中
object
是self
,superClass
是(id)class_getSuperclass(objc_getClass("HTPerson"))
我们在
image.pngobjc4
源码中搜索objc_msgSendSuper(
,查看其格式:
发现第1个参数是
image.pngstruct objc_super
结构,我们搜索struct objc_super
:
在这里,可以看到第1个参数是
receive接收者
,第二个是super_class
父类。到这,我们就已经完整的弄清楚了
[super class]
的实际调用对象
是__rw_objc_super
结构中的第1个入参self
,所以调用class
方法时,传入的是self
,self
的isa指向的类
是HTPerson类
。
- 所以
[super class]
打印结果为HTPerson
。
补充:
super
虽然可以直接调用方法
,但是它并不是对象
,只是
语法关键字
。真正的调用者,是当前对象
。虽然
image.pngclang编译
中我们看到[super class]
被编译为发送objc_msgSendSuper
消息,但实际断点检查汇编流程时,发现它callq
调用的是objc_msgSendSuper2
:
搜索
image.pngobjc_msgSendSuper2
,objc_super
说明了就是当前搜索类
,并不是父类
image.png
汇编
源码:直接调用CacheLookup
,从superclass
中的cache
快速查找方法
9. runtime
如何实现weak
,为什么可以自动
置为nil
?
- 通过
SideTable
找到我们的weak_table
-
weak_table
根据referent
找到或者创建weak_entry_t
- 然后
append_referrer(entry, referrer)
将我的新弱引用的对象
加进去entry
- 最后
weak_entry_insert
把entry
加入到我们的weak_table
10. runtime Associate
方法关联
的对象
,是否需要在dealloc
中释放?
- 不需要。因为设置
关联对象
时,会将isa
的has_assoc
设置为true
。当持有对象
被释放
时,会调用c++析构函数
,此时会将关联对象
全部释放
。
指针优化的
image.pngisa
中的has_assoc
记录了是否有关联属性
,在析构函数
触发时,会检查是否有关联属性
并主动释放
。
- 查看
hasAssociatedObjects
:
image.png
- 流程:
1:c++
函数释放:object_cxxDestruct
2:移除
关联属性 :_object_remove_assocations
3: 将弱引用
自动设置nil
:weak_clear_no_lock(&table.weak_table, (id)this)
;
4:引用计数
处理:table.refcnts.erase(this)
5:销毁
对象:free(obj)