【面试-1】通过 Asssociate 方法关联的对象,需要在dealloc中释放
当对象释放时,系统会自动调用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源码
-
在objc源码中搜索
dealloc
实现
-
_objc_rootDealloc
源码实现,主要是对象进行析构
-
rootDealloc
源码实现
-
object_dispose
源码实现,销毁实例对象
-
objc_destructInstance
源码实现,移除关联属性
-
_object_remove_assocations
源码,移除关联属性
,主要是从全局哈希map中找到相关对象的迭代器,然后将迭代器中关联属性,彻底移除
【面试-2】类的方法 和 分类方法 重名,方法调用顺序
-
如果是
普通方法同名
,包括initialize
,都会先调用分类方法
- 因为
分类的方法是类realize 后attach进去的
,插在类的方法的前面,所有优先调用分类的方法
(不是分类覆盖分类) -
initialize
方法在第一次消息时
主动调用,为了不影响整个load,可以将提前加载的数据
写到initialize
中
- 因为
-
如果同名方法时
load
方法,先调用主类load
,后加载分类load
(分类之间看编译顺序)- 原理可以查看iOS 类的加载(下)文章中的
load_images
原理分析
- 原理可以查看iOS 类的加载(下)文章中的
【面试-3】Runtime是什么?
runtime
是由C和C++
汇编实现的一套API
,为OC语言加入了面向对象,以及运行时的功能
-
将数据类型的确定从 编译时 推迟到了 运行时
平时编写的OC代码,在程序运行的过程中,其实最终会转换成
runtime
的C语言
代码,runtime是OC的幕后工作者
【面试-4】方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?
-
方法的本质:
发送消息
- 快速查找
objc_msgSend
:从cache_t
缓存消息中查找 - 慢速查找:递归查找自己和父类
lookUpImpOrForward
- 查找不到消息
- 动态方法解析
resolveInstanceMethod
- 消息快速转发
forwardingTargetForSelector
- 消息慢速转发
methodSignatureForSelector & forwardInvocation
- 动态方法解析
- 快速查找
sel
是方法编号
-- 在read_images
期间就编译进了内存,相当于一本书的目录名字
imp
是函数实现指针
,找imp就是找函数的过程
,相当于书本的页码
-
查找
具体的函数
就是想看这本书具体内容- 1、首先知道想看什么,即目录title--sel
- 2、根据目录找到对应的页码,即imp
- 3、通过页码去找到具体内容
【面试-5】能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量
- 1、
不能
向编译后得到的类中增加实例变量,因为编译好的实例变量存储的位置是ro
,一旦编译完成,内存结构就确定了 - 2、
只要类没有注册到内存就可以添加
,可以添加属性+方法
【面试-6】 [self class]和[super class]的区别以及原理分析
-
[self class]
就是发送消息objc_msgSend
,消息接收者是self
,方法编号是class
-
[super class]
本质是objc_msgSendSuper
,消息接收者还是self
,方法编号还是class
,super
只是一个关键字
,在运行时,底层调用的是_objc_msgSendSuper2
,_objc_msgSendSuper2速度更快,会跳过self的查找
[self class]
-
[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 -rewrite-objc LGTeacher.m -o LGTeacher.cpp
来查看super
本质,这是编译时
的底层源码,其中第一个参数是消息接收者
,另外一个是__rw_objc_super
结构
-
在底层源码中搜索
__rw_objc_super
,是一个中间结构体
-
在objc中搜索
objc_msgSendSuper
,看到了影藏参数
-
继续搜索
struct objc_super
通过clang
的底层编译代码可知,当前的消息的接收者
==self
,而self
==LGTeacher
,所以[super class]
进入class
方法源码后,其中self是init后的实例对象
,实例对象的isa
指向本类,即消息接收者是LGTeacher本类
【面试-7】内存平移问题
LGPerson中有一个属性kc_name
和一个实例方法saySomething
,通过下面代码,能否调用实例方法?
- (void)saySomething{
NSLog(@"%s",__func__);
}
//下面这两种方式调用
//方式一
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
//方式二:常规调用
LGPerson *person = [LGPerson alloc];
[person saySomething];
代码分析
-
[person saySomething]
的本质是对象发送消息
,-
person
的isa
指向类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
表示8字节指针
,self.kc_name
的获取,相当于kc首地址的指针也需要平移8字节找到kc_name
,那么此时的kc的指针地址是多少?平移8字节获取的是什么?-
kc
是一个指针,存在栈
中,因为栈是先进后出
的结构,参数的传入是一个不断压栈的过程- 其中
影藏参数也会压入栈
,每个函数都会有两个隐藏参数(id self, sel _cmd)
,可以通过clang
查看底层编译 -
隐藏参数压栈
的过程,其地址是递减
的,而栈是从高地址->低地址 分配
的,即在栈中,参数会从前往后一直压
- 因为
objc_msgSendSuper
中第一个参数是一个结构体__rw_objc_super(self,class_getSuperclass)
那么结构体中的属性是如何压栈的?可以通过自定义一个结构体,判断结构体内部成员的压栈情况
从图可知20是先加入的,再加入10,因此结构体内部
的压栈情况是低地址->高地址
,递增
的,栈中结构体内部
成员是反向
压入栈的
- 其中
-
-
所以到目前为止,栈中
高地址->低地址
的顺序是:self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - person
-
sel
f和_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的地址内存平移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、然后a
ppend_referrer(entry,referrer)
将我的新弱引用的对象加进去 - 4、最后
weak_entry_insert
,加入到我们的weak_table
底层源码调用流程如下图所示