前言
我们类的底层探索
已经告一段落,我们梳理一下常见的面试题,希望对你有些帮助。
问题
- 1.
runtime
是什么?- 2.
runtime
如何实现weak
,为什么可以自动置为nil
?- 3.
runtime
Associate方法关联的对象,是否需要在dealloc
中释放?- 4.关联对象
AssociationsManager
是否唯一?- 5.
分类
方法会覆盖本类
方法吗?- 6.所有
分类
方法都优先于本类
吗?- 7.方法的本质,
SEL
是什么?IMP
是什么?两者之间关系是什么?- 8.
编译后
的类能否添加实例变量?能否向运行时创建的类添加实例变量?- 9.
[self class]
和[super class]
区别和原理分析- 10.
内存平移
问题
问题一:runtime
是什么?
runtime
是由C
和C++
和汇编
实现的⼀套API,为OC
语⾔加⼊了⾯向对象,运⾏时
的功能。
运⾏时(Runtime
)是指将数据类型
的确定由编译时
推迟到了运⾏时
.
举个例⼦: extension - category
的区别(extension
是编译期就确定了,但是懒加载的category
是在运行时动态加入的)。
平时我们编写的OC
代码,在程序运⾏过程中,其实最终会转换成Runtime
的C
语⾔代码,Runtime 是Object-C
的幕后⼯作者
问题二:runtime
如何实现weak
,为什么可以自动置为nil
?
- 通过
SideTable
找到我们的weak_table
-
weak_table
根据referent
找到或者创建weak_entry_t
- 然后
append_referrer(entry, referrer)
将我的新弱引用的对象
加进去entry
- 最后
weak_entry_insert
把entry
加入到我们的weak_table
底层源码调用流程如下图所示
问题三:runtime
Associate方法关联的对象,是否需要在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
的源码实现
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
- 进入
_objc_rootDealloc
源码实现,主要是对对象进行析构
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
-
进入
rootDealloc
源码实现,发现其中有关联属性时设置bool值
,当有这些条件时,需要进入else流程 进入
object_dispose
源码实现,主要是销毁实例对象
/***********************************************************************
* object_dispose
* fixme
* Locking: none
**********************************************************************/
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
- 进入
objc_destructInstance
源码实现,在这里有移除关联属性的方法
/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
- 进入
_object_remove_assocations
源码,关联属性的移除,主要是从全局哈希map中找到相关对象的迭代器,然后将迭代器中关联属性,从头到尾的移除
// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.
void
_object_remove_assocations(id object)
{
ObjectAssociationMap refs{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
//获取迭代器
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
//从头到尾逐个移除
if (i != associations.end()) {
refs.swap(i->second);
associations.erase(i);
}
}
// release everything (outside of the lock).
for (auto &i: refs) {
i.second.releaseHeldValue();
}
}
问题四:关联对象AssociationsManager
是否唯一?
AssociationsManager
结构中,manager
只是对外代言人,并不是唯一的,AssociationsHashMap
哈希表才是唯一的。
1. 运行验证:
移除锁
,这样可以同时存在2个manager
了。
- 加入测试代码,创建2个
manager
,都调用get()
,发现2个读取的associations
是相同地址
。- 证明
AssociationsHashMap
在内存中是独一份的,而manager
只是外层包装,可以创建多个。
问题五:分类
方法会覆盖本类
方法吗?
-
分类方法
会调用attachLists
,将分类方法
插入了本类方法
前面,全都存储
起来。并不是覆盖
本类方法,这个在我们之前的文章中 iOS-类的加载(下)有详细的解释。
问题六:所有分类方法都优先于本类吗?
类的方法 和 分类方法 重名,如果调用,是什么情况?
-
如果同名方法是
普通方法
,包括initialize
-- 先调用分类方法因为
分类的方法是在类realize之后 attach进去的
,插在类的方法的前面,所以优先调用分类的方法
(注意:不是分类覆盖主类!!)initialize
方法什么时候调用?initialize
方法也是主动调用,即第一次消息时
调用,为了不影响整个load,可以将需要提前加载的数据
写到initialize
中
-
如果同名方法是
load
方法 -- 先主类load
,后分类load
(分类之间,看编译的顺序)- 原因:参考iOS-类的加载(下)文章中的
load_images
原理分析
- 原因:参考iOS-类的加载(下)文章中的
问题七:方法的本质,SEL
是什么?IMP
是什么?两者之间关系是什么?
方法的本质:发送消息,消息会有以下几个流程
- 快速查找(
objc_msgSend
) -cache_t
缓存消息中查找- 慢速查找 - 递归自己|父类 -
lookUpImpOrForward
- 查找不到消息:动态方法解析 -
resolveInstanceMethod
- 消息快速转发 -
forwardingTargetForSelector
- 消息慢速转发 -
methodSignatureForSelector & forwardInvocation
sel
是方法编号 - 在read_images
期间就编译进了内存
imp
是函数实现指针 ,找imp
就是找函数的过程
打个比方:加入你要从一本字典中查找某个字,那么sel
相当于 字典的目录title
,imp
相当于 字典的页码。
问题八:编译后
的类能否添加实例变量?能否向运行时创建的类添加实例变量?
1、不可以。 因为编译好的实例变量存放的位置在类的ro
,一旦编译完成,内存结构
就完全确定了,无法修改。
2、运行时在register
注册前,可以添加。但是调用运行时register
注册后,就完成了内存
的注入,内存结构
确定了,无法修改。
问题九:[self class]
和[super class]
区别和原理分析
[self class]
就是发送消息objc_msgSend
,消息接受者是self
,方法编号(SEL)是class
[super class]
本质是objc_msgSendSuper
,消息接受者还是self
,方法编号是class
。
实际运行时,[super class]在汇编层
执行的是objc_msgSendSuper2
,直接从superclass
父类开始搜索,节约了一轮查找资源
测试代码:
@interface ZGPerson : NSObject
@end
@implementation ZGPerson
- (instancetype)init {
if (self = [super init]) {
NSLog(@"%@ %@", [self class], [super class]);
}
return self; }
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZGPerson * person = [[ZGPerson alloc] init];
}
return 0;
}
- 打印结果:
都是ZGPerson
结果与我想的不一样,为什么不是ZGPerson
和NSObject
呢?我们查看源码分析一下
我们查看 [self class]
中的class
源码
- (Class)class {
return object_getClass(self);
}
👇
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
其底层是获取对象的isa
,当前的对象是ZGPerson
,其isa
是同名的ZGPerson
,所以[self class]
打印的是ZGPerson
而[super class]
中,其中super
是语法的 关键字
,可以通过clang
看super
的本质,clang
生成cpp
编译文件(clang -rewrite-objc ZGPerson.m -o ZGPerson.cpp)
,打开main.cpp
文件:
底层源码中搜索__rw_objc_super
,是一个中间结构体
objc中搜索objc_msgSendSuper
,查看其隐藏参数
搜索struct objc_super
通过clang
的底层编译代码可知,当前消息的接收者
等于 self
,而self
等于 LGTeacher
,所以 [super class]
进入class
方法源码后,其中的self是init后的实例对象
,实例对象的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
时已经加入表中,所以打印为ZGPerson
[super class]
打印的是ZGPerson
,原因是当前的super
是一个关键字,在这里只调用objc_msgSendSuper2
,其实他的消息接收者和[self class]
是一模一样的,所以返回的是ZGPerson
问题十:runtime
是什么?内存平移问题
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
即person的首地址 指向 LGPerson的首地址
,我们可以通过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_name
,那么此时的kc的指针地址是多少?平移8字节获取的是什么?-
kc
是一个指针,是存在栈
中的,栈是一个先进后出
的结构,参数传入就是一个不断压栈的过程,其中
隐藏参数会压入栈
,且每个函数都会有两个隐藏参数(id self,sel _cmd)
,可以通过clang
查看底层编译隐藏参数压栈
的过程,其地址是递减
的,而栈是从高地址->低地址 分配
的,即在栈中,参数会从前往后一直压
-
super通过clang查看底层的编译,是
objc_msgSendSuper
,其第一个参数是一个结构体__rw_objc_super(self,class_getSuperclass)
,那么结构体中的属性是如何压栈的?可以通过自定义一个结构体,判断结构体内部成员的压栈情况p &person3
p *(NSNumber **)0x00007ffee83a8090
-
p *(NSNumber **)0x00007ffee83a8098
所以图中可以得出 20先加入,再加入10,因此
结构体内部
的压栈情况是低地址->高地址
,递增
的,栈中结构体内部
的成员是反向
压入栈,即低地址->高地址
,是递增的,
-
-
所以到目前为止,栈中
从高地址到低地址
的顺序的: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,而是栈的指针不同,但是指向同一片内存空间
-
[(__bridge id)kc saySomething]
调用时,此时的kc是LGPerson: 0x7ffeec381098
,所以saySomething
方法中传入的self
还是LGPerson,但并不是我们通常认为的LGPerson,使我们当前传入的消息接收者
,即LGPerson: 0x7ffeec381098
,是LGPerson的实例对象,此时的操作与普通的LGPerson是一致的,即LGPerson的地址内存平移8字节
普通person流程:
person -> kc_name - 内存平移8字节
-
kc流程:
0x7ffeec381098 + 0x80 -> 0x7ffeec3810a0
,即为self
,指向<ViewController: 0x7fac45514f50>
,如下图所示
其中 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` 开头的地址表示在`全局区域`中
以上就是全部的内容了,如有错误,还望指正。