iOS 底层原理 文章汇总
【面试-1】Runtime Asssociate方法关联的对象,需要在dealloc中释放?
当我们对象释放时,会调用
dealloc
- 1、C++函数释放 :
objc_cxxDestruct
- 2、移除关联属性:
_object_remove_assocations
- 3、将弱引用自动设置nil:
weak_clear_no_lock(&table.weak_table, (id)this);
- 4、引用计数处理:
table.refcnts.erase(this)
- 5、销毁对象:
free(obj)
所以,
关联对象
不需要我们手动移除,会在对象析构即
dealloc
时释放
dealloc 源码
dealloc的源码查找路径为:
dealloc
->
_objc_rootDealloc
->
rootDealloc
->
object_dispose
(释放对象)->
objc_destructInstance
->
_object_remove_assocations
- 在objc源码中搜索
的源码实现dealloc
- 进入
源码实现,主要是对_objc_rootDealloc
对象进行析构
- 进入
源码实现,发现其中有rootDealloc
,当有这些条件时,需要进入else流程关联属性时设置bool值
- 进入
源码实现,主要是object_dispose
销毁实例对象
- 进入
源码实现,在这里有移除关联属性的方法objc_destructInstance
- 进入
源码,关联属性的移除,主要是_object_remove_assocations
从全局哈希map中找到相关对象的迭代器,然后将迭代器中关联属性,从头到尾的移除
【面试-2】方法的调用顺序
类的方法 和 分类方法 重名,如果调用,是什么情况?
- 如果同名方法是
,包括普通方法
– 先调用分类方法initialize
- 因为
,插在类的方法的前面,所以分类的方法是在类realize之后 attach进去的
(注意:不是分类覆盖主类!!)优先调用分类的方法
-
方法什么时候调用?initialize
方法也是主动调用,即initialize
调用,为了不影响整个load,可以将需要第一次消息时
写到提前加载的数据
中initialize
- 因为
- 如果同名方法是
方法 – 先load
,后主类load
(分类之间,看编译的顺序)分类load
- 原因:参考iOS-底层原理 18:类的加载(下)文章中的
原理分析load_images
- 原因:参考iOS-底层原理 18:类的加载(下)文章中的
【面试-3】Runtime是什么?
-
是由runtime
汇编实现的一套C和C++
,为OC语言加入了API
面向对象、以及运行时的功能
- 运行时是指将
数据类型的确定由编译时 推迟到了 运行时
- 举例:extension 和 category 的区别
- 平时编写的OC代码,在程序运行的过程中,其实最终会转换成runtime的C语言代码,
runtime是OC的幕后工作者
1、category 类别、分类
-
专门用来给类添加新的方法
-
,添加了成员属性,也无法取到不能给类添加成员属性
- 注意:其实
,即属性关联,重写setter、getter方法可以通过runtime 给分类添加属性
- 分类中用
定义变量,@property
变量的只会生成
方法的setter、getter
,声明
不能生成方法实现 和 带下划线的成员变量
2、extension 类扩展
- 可以说成是
,也可称作特殊的分类
匿名分类
- 可以
,但给类添加成员属性
是是私有变量
- 可以
,也给类添加方法
是私有方法
【面试-4】方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?
- 方法的本质:
,消息会有以下几个流程发送消息
- 快速查找(
) - cache_t缓存消息中查找objc_msgSend
- 慢速查找 - 递归自己|父类 -
lookUpImpOrForward
- 查找不到消息:动态方法解析 -
resolveInstanceMethod
- 消息快速转发 -
forwardingTargetForSelector
- 消息慢速转发 -
methodSignatureForSelector & forwardInvocation
- 快速查找(
-
是sel
- 在方法编号
期间就编译进了内存read_images
-
是imp
,函数实现指针
找imp就是找函数的过程
-
相当于 一本书的sel
目录title
-
相当于 书本的sel
页码
- 查找
就是想看这本书具体篇章的内容具体的函数
- 1、首先知道想看什么,即目录 title - sel
- 2、根据目录找到对应的页码 - imp
- 3、通过页码去翻到具体的内容
【面试-5】能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量
- 1、
向编译后的得到的类中增加实例变量不能
- 2、
只要类没有注册到内存还是可以添加的
- 3、可以
添加属性+方法
【原因】:编译好的实例变量存储的位置是ro,一旦编译完成,内存结构就完全确定了
【经典面试-6】 [self class]和[super class]的区别以及原理分析
-
就是发送消息[self class]
,消息接收者是objc_msgSend
,方法编号self
class
-
本质就是[super class]
,消息的接收者还是objc_msgSendSuper
,方法编号self
,在运行时,底层调用的是class
【重点!!!】_objc_msgSendSuper2
- 只是
会更快,直接跳过self的查找objc_msgSendSuper2
代码调试
-
中的LGTeacher
方法中打印这两种class调用 运行程序,打印结果如下init
- 进入
中的[self class]
源码class
- (Class)class {
return object_getClass(self);
}
👇
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
其底层是
获取对象的isa
,当前的
对象是LGTeacher
,其isa是同名的
LGTeacher
,所以
[self class]
打印的是
LGTeacher
- [super class]中,其中
是语法的super
,可以通过关键字
看clang
的本质,这是super
的底层源码,其中第一个参数是消息接收者,是一个编译时
结构__rw_objc_super
- 底层源码中搜索
,是一个中间结构体__rw_objc_super
- objc中搜索
,查看其隐藏参数objc_msgSendSuper
- 搜索
通过struct objc_super
的底层编译代码可知,当前clang
等于消息的接收者
,而self
等于self
,所以LGTeacher
进入[super class]
方法源码后,其中的class
即为self
,所以最后还是LGTeacher
,即获取LGTeacher的isa
同名LGTeacher元类
- 底层源码中搜索
- 我们再来看[super class]在运行时是否如上一步的底层编码所示,是
,打开汇编调试,调试结果如下objc_msgSendSuper
- 搜索
,从注释得知,是objc_msgSendSuper2
,而不是父类从 类开始查找
- 查看
的汇编源码,是从objc_msgSendSuper2
中的superclass
中查找方法cache
- 搜索
ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
ldp p0, p16, [x0] // p0 = real receiver, p16 = class 取出receiver 和 class
ldr p16, [x16, #SUPERCLASS] // p16 = class->superclass
CacheLookup NORMAL, _objc_msgSendSuper2//cache中查找--快速查找
END_ENTRY _objc_msgSendSuper2
完整回答
所以,最完整的回答如下
-
方法调用的本质是[self class]
,调用class的消息流程,拿到发送消息
,在这里是因为类已经加载到内存,所以在读取时是一个字符串类型,这个字符串类型是在元类的类型
的map_images
时已经加入表中,所以打印为readClass
LGTeacher
-
打印的是[class class]
,原因是当前的super是一个关键字,在这里只调用LGTeacher
,其实他的消息接收者和objc_msgSendSuper2
是一模一样的,所以返回的是LGTeacher[self class]
【面试-7】内存平移问题
Class cls = [LGPerson class];
void *kc = &cls; //
[(__bridge id)kc saySomething];
LGPerson中有一个属性
kc_name
和一个实例方法
saySomething
,通过上面代码这种方式,能否调用实例方法?为什么?
代码调试
- 我们在日常开发中的调用方式是下面这种
LGPerson *person = [LGPerson alloc];
[person saySomething];
- 通过运行发现,是可以执行的,打印结果如下
-
的本质是[person saySomething]
,那么当前的person是什么?对象发送消息
-
的person
指向类isa
即LGPerson
,我们可以通过LGPerson的内存平移找到person的首地址 指向 LGPerson的首地址
,在cache中查找方法cache
-
-
中的[(__bridge id)kc saySomething]
是来自于kc
这个类,然后有一个指针LGPerson
,将其kc
指向LGPerson的首地址
所以,
person
是指向
LGPerson
类的结构,
kc
也是指向
LGPerson
类的结构,然后都是在
LGPerson
中的
methodList
中查找方法
修改:saySomething里面有属性 self.kc_name 的打印
代码如下所示
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.kc_name);
}
//下面这两种方式调用
//方式一
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
//方式二:常规调用
LGPerson *person = [LGPerson alloc];
[person saySomething];
- 查看这两种调用方式的打印结果,如下所示
-
方式的调用打印的kc
是kc_name
<ViewController: 0x7fe29170b560>
-
方式的调用打印的person
是kc_name
(null)
-
为什么会出现打印不一致的情况?
- 其中person方式的
是由于kc_name
,然后通过self指向person的内存结构
,即内存平移8字节,取出去kc_name
self指针首地址平移8字节获得
- 【方式一】其中
指针中没有任何,所以kc
,kc表示8字节指针
的获取,相当于self.kc_name
,那么此时的kc的指针地址是多少?平移8字节获取的是什么?kc首地址的指针也需要平移8字节找kc_name
-
是一个指针,是存在kc
中的,栈是一个栈
的结构,参数传入就是一个不断压栈的过程,先进后出
- 其中
,且每个函数都会有两个隐藏参数隐藏参数会压入栈
,可以通过(id self,sel _cmd)
查看底层编译clang
-
的过程,其地址是隐藏参数压栈
的,而递减
的,即栈是从高地址->低地址 分配
在栈中,参数会从前往后一直压
- super通过clang查看底层的编译,是
,其第一个参数是一个结构体objc_msgSendSuper
,那么结构体中的属性是如何压栈的?可以通过自定义一个结构体,判断结构体内部成员的压栈情况__rw_objc_super(self,class_getSuperclass)
-
p &person3
-
p *(NSNumber **)0x00007ffee83a8090
-
所以图中可以得出 20先加入,再加入10,因此p *(NSNumber **)0x00007ffee83a8098
的压栈情况是结构体内部
,低地址->高地址
的,栈中递增
的成员是结构体内部
压入栈,即反向
,是递增的,低地址->高地址
-
- 其中
-
- 所以到目前为止,栈中
的顺序的:从高地址到低地址
self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - person
-
和self
是_cmd
方法的两个隐藏参数,是高地址->低地址viewDidLoad
的正向压栈
-
和class_getSuperClass
为self
中的结构体成员,是从最后一个成员变量,即低地址->高地址objc_msgSendSuper2
的反向压栈
-
可以通过下面这段代码打印下栈的存储是否如上面所说
void *sp = (void *)&self;
void *end = (void *)&person;
long count = (sp - end) / 0x8;
for (long i = 0; i<count; i++) {
void *address = sp - 0x8 * i;
if ( i == 1) {
NSLog(@"%p : %s",address, *(char **)address);
}else{
NSLog(@"%p : %@",address, *(void **)address);
}
}
运行结果如下
其中为什么
class_getSuperclass
是
ViewController
,因为
objc_msgSendSuper2
返回的是
当前类
,两个
self
,并不是同一个self,而是栈的指针不同,但是指向同一片内存空间
-
调用时,此时的kc是[(__bridge id)kc saySomething]
,所以LGPerson: 0x7ffeec381098
方法中传入的saySomething
还是LGPerson,但并不是我们通常认为的LGPerson,使我们当前self
,即传入的消息接收者
,是LGPerson的实例对象,此时的操作与普通的LGPerson是一致的,即LGPerson: 0x7ffeec381098
LGPerson的地址内存平移8字节
- 普通person流程:
person -> kc_name - 内存平移8字节
- kc流程:
,即为0x7ffeec381098 + 0x80 -> 0x7ffeec3810a0
,指向self
,如下图所示<ViewController: 0x7fac45514f50>
- 普通person流程:
其中
person
与
LGPerson
的关系是
person是以LGPerson为模板的实例化对象,即alloc有一个指针地址,指向isa,isa指向LGPerson
,它们之间关联是有一个
isa指向
,
而kc也是指向LGPerson的关系,编译器会认为
kc也是LGPerson的一个实例化对象
,即
kc相当于isa,即首地址,指向LGPerson
,具有和person一样的效果,简单来说,我们已经完全将编译器骗过了,即
kc
也有
kc_name
。由于
person查找kc_name是通过内存平移8字节
,所以kc也是通过内存平移8字节去查找kc_name
哪些东西在栈里 哪些在堆里
-
的对象 都在alloc
中堆
-
在指针、对象
中,例如栈
在person指向的空间
中,person所在的空间在栈中堆
-
在临时变量
中栈
-
在属性值
,属性随对象是在堆
中栈
注意:
是从小到大,即低地址->高地址
堆
- 栈是从大到小,即从高地址->低地址分配
- 函数隐藏参数会
一直压,即
从前往后
,
从高地址->低地址 开始入栈
- 结构体内部的成员是
从低地址->高地址
- 一般情况下,内存地址有如下规则
开头表示在
0x60
中
堆
开头的地址表示在
0x70
中
栈
开头的地址表示在
0x10
中
全局区域
### 【面试-8】 Runtime是如何实现weak的,为什么可以自动置nil
- 1、通过
找到我们的SideTable
weak_table
- 2、
根据weak_table
找到或者创建referent
weak_entry_t
- 3、然后
将我的新弱引用的对象加进去entryappend_referrer(entry,referrer)
- 4、最后
,把weak_entry_insert
加入到我们的entry
weak_table
底层源码调用流程如下图所示