iOS-底层探索16:面试题&内存平移
目录
- 1. 设置关联对象后是否需要主动移除?
- 2. 类的方法和分类的方法重名会如何调用?
- 3. Runtime是什么?
- 4. 方法的本质,sel是什么? IMP是什么?两者之间的关系又是什么?
- 5. 能否向编译后的得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?
- 6. [sel class]和[super class]的区别及原理分析
- 7. Runtime是如何实现weak的,为什么可以自动置nil
- 8. 内存平移问题
1. 设置关联对象后是否需要主动移除?
不需要。
查看对象的析构方法:
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
后续方法调用情况如下:
rootDealloc(
)-->object_dispose
-->objc_destructInstance
-->_object_remove_assocations
在_object_remove_assocations
方法中会将所有关联的对象移除
23题:使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
https://github.com/ChenYilong/iOSInterviewQuestions/blob/master/01《招聘一个靠谱的iOS》面试题参考答案/《招聘一个靠谱的iOS》面试题参考答案(上).md
2. 类的方法和分类的方法重名会如何调用?
两种情况:
- 普通方法(含
+initialize
):分类的方法是在类realize
之后attach
到类的方法列表前面,所以会调用分类的方法。(注意:不是分类方法覆盖主类方法) -
+load
方法重名:先调用主类+load
方法,再依次调用分类+load
方法
load_images
方法中主类+load
方法、分类+load
方法加载及调用情况如下:
-
补充1:有父类、父类分类、子类、子类分类的情况:
父类+load
-->子类+load
-->父类分类+load
-->子类分类+load
-
补充2:
+initialize
方法也是系统主动调用,发生在第一次消息发生时
3. Runtime是什么?
-
Runtime
是由C、C++、汇编
实现的一套API
,为OC
语言加入了面向对象
,运行时
的功能 -
运行时(Runtime)
是指将数据类型的确定由编译时
推迟到了运行时
例子🌰:Extension-Category
的区别 - 平时写的
OC
代码,在程序运行过程中,其实最终都会转换成Runtime
的C
语言代码,Runtime
是Objective-C
的幕后工作者
4. 方法的本质,sel是什么? IMP是什么?两者之间的关系又是什么?
方法的本质:发送消息,消息会有以下几个流程
- 快速查找(
objc_ _msgSend
) ~cache_ _t
缓存消息 - 慢速查找 ~ 递归自己|父类 ~
lookUplmpOrForward
- 查找不到消息:动态方法解析~
resolveInstanceMethod
- 消息快速转发~
forwardingTargetForSelector
- 消息慢速转发~
methodSignatureForSelector & forwardInvocation
-
sel
是方法编号:在read_ _images
期间就编译进入了内存 -
imp
就是我们函数实现指针,找imp
就是找函数的过程 -
sel
就相当于书本的目录tittle
-
imp
就是书本的页码
查找具体的函数就是想看这本书里面具体篇章的内容
- 我们首先知道想看什么:
tittle (sel)
- 根据目录对应的页码
(imp)
- 翻到具体的内容
(函数实现)
5. 能否向编译后的得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?
- 不能向编译后得到的类中增加实例变量
- 运行时创建的类只要没注册到内存还是可以添加实例变量的
原因:我们编译好的实例变量存储的位置在ro,一旦编译完成内存结构就完全确定,无法修改。
可以添加属性+方法
6. [sel class]和[super class]的区别及原理分析
@implementation Son : Father
- (instancetype)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案:均输出Son
这个题目主要是考察关于objective-C 中对self 和super 的理解。
我们都知道: self
是方法的隐藏参数,指向当前调用方法的这个类的实例。其实super
是一个 Magic Keyword
, 它本质是一个编译器标示符,他们两个的不同点在于: super
会告诉编译器,调用class
这个方法时, 要去父类的方法,而不是本类里的。当使用self
调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用super
时,则从父类的方法列表中开始找,然后调用父类的这个方法。 ------------- (看不懂,移步原文有详细介绍)
-
[sel class]
本质是objc_msgSend
-
[super class]
本质是objc_msgSendSuper
,运行时汇编调用的方法是objc_msgSendSuper2
7. Runtime是如何实现weak的,为什么可以自动置nil
Runtime
对注册的类,会进行内存布局,从一个粗粒度的概念上来讲,这时候weak
对象会放入一个 hash
表中,这是一个全局表,表中是用 weak
指向的对象内存地址作为key
,用所有指向该对象的 weak
指针表(数量少为数组,数量多为表)作为value
。当此对象的引用计数为 0
的时候会 dealloc
,假如该对象内存地址是 address
,那么就会以 address
为key
,在这个weak
表中搜索,找到所有以 address
为键的weak
对象,从而设置为nil
。参考
当我们的对象释放的时候- dealloc
:
-
C++
函数释放:object_ cxxDestruct
- 移除关联属性:
_ object_ remove_ assocations
- 将弱引用自动设置nil :
weak_ clear_ no_ lock(&table.weak_ table, (id)this);
- 引用计数处理:
table.refcnts.erase(this);
- 销毁对象:
free(obj);
8. 内存平移问题
代码如下:
LGPerson.h
@interface LGPerson : NSObject
- (void)saySomething;
@end
LGPerson.m
@implementation LGPerson
- (void)saySomething {
NSLog(@"%s",__func__);
}
@end
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
}
问:saySomething方法能否正常调用
答案:能
我们知道正常情况下的下的对象方法调用:
LGPerson *person = [LGPerson alloc];
[person saySomething];
而kc
指针指向LGPerson
类的地址和对象person
的isa
指向LGPerson
类的地址一样,所以[(__bridge id)kc saySomething];
这里也能调用对象方法
。
现在修改代码:
LGPerson.h
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *kc_name;
@property (nonatomic, copy) NSString *kc_hobby;
- (void)saySomething;
@end
LGPerson.m
@implementation LGPerson
- (void)saySomething {
NSLog(@"%s - %@",__func__,self.kc_name);
}
@end
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
Class cls = [LGPerson class];
void *kc = &cls;
LGPerson *person = [LGPerson alloc];
[(__bridge id)kc saySomething];
[person saySomething];
}
问:ViewController.m的打印情况
答案:能,输出:-[LGPerson saySomething] -
这里主要考察self.kc_name
的值,我们知道person
对象的内存情况如下:
查找kc_name
需要从person
地址向下内存平移8
个字节,并获取8
个字节大小的值(person
在堆中,地址由低到高)。
kc
是内存中一个8
字节的指针指向cls
,就相当于person
指针指向LGPerson
的一个实例。self.kc_name
是内存向高地址
平移8
个字节,那么cls
地址+0x8存储的就是将要打印的内容。
分析cls
和kc
在内存中情况:
-
kc
是当前方法中的变量,存储在当前方法的栈中。当前方法中所有的变量,方法参数都会压入这个栈中。(栈
是一个先进后出的队列,内存从高
地址到低
地址分配,所以先压入栈的地址高) - 方法的参数会从前往后依次压入栈中
- 每个方法都带有两个隐藏参数:
id * self, SEL _cmd
viewDidLoad转为C++实现(参考:iOS 对象的本质)
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
Class cls = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("class"));
void *kc = &cls;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)(__bridgeid)kc, sel_registerName("saySomething"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
}
因此入栈的变量如下:
self
-->_cmd
--> cls
--> kc
--> person
但是[super viewDidLoad];
调用时会创建一个结构体传入参数,因此这个结构体也会被压入到当前栈中:(经测试结构体中从后往前压栈)
self
-->_cmd
-->(id)class_getSuperclass(objc_getClass("ViewController"))
-->self
--> cls
--> kc
--> person
注意:其他方法的入参是不会压入到当前栈中,上面结构体会作为一个id参数压入到
objc_msgSendSuper
方法的栈中
面试题6中说过objc_msgSendSuper
,运行时汇编调用的方法是objc_msgSendSuper2
,这个从这个方法的实现可看到是从当前类开始搜索,所以(id)class_getSuperclass(objc_getClass("ViewController"))
返回当前类ViewController
所以打印情况如下:
kc
的地址是0x7ffeed61d0d0
其指向cls:0x7ffeed61d0d8
,cls
内存加8
个字节就是0x7ffeed61d0e0 :
注意:
void *kc = &cls;
虽然好像获取了一个LGPerson
的实例,但是系统并未开辟一个LGPerson的
实例的内存,所以kc指向的其实不是一个实例。
再次现在修改代码:
LGPerson.h
@interface LGPerson : NSObject
@property (nonatomic, assign) int kc_name;//修改的代码
@property (nonatomic, copy) NSString *kc_hobby;
- (void)saySomething;
@end
LGPerson.m
@implementation LGPerson
- (void)saySomething {
NSLog(@"%s - %d",__func__,self.kc_name);
}
@end
问:ViewController.m的打印情况
答:不确定的数字
现在查找kc_name
需要从person
地址向下内存平移8
个字节,并获取4
个字节大小的值,这在viewDidLoad
方法栈中只能获取参数self
的前4
位,并将这个4
位的地址指向转为一个int
值。
共有 0 条评论