前言
本片文章对一些面试题做一些整理和记录,以便在之后的面试中复习。
问题1:关联对象需要我们手动移除吗?
答:关联对象不需要我们手动移除,它会在对象销毁时dealloc
内移除。
这里我们详细的看下dealloc
方法的实现:
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return 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;
}
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();
}
}
从代码实现可以看到:dealloc
->_objc_rootDealloc(self)
->rootDealloc()
这里会对isa
的一些标识做一次判断,这里我们知道当添加关联对象后,isa.has_assoc
会标记为true
,所以此时的if
判断是不成立的,所以执行object_dispose()
->objc_destructInstance()
->_object_remove_assocations()
通过读取全局的AssociationHashMap
,根据object
查找对应的ObjectAssociationMap
,然后通过erase()
最终移除。
这里可以继续扩展:关联对象的使用场景以及内部的数据结构。
关联对象移除流程图:
问题2:主类和分类同时实现+(void)load,哪个类先执行?
通过实际的代码打印,我们会看到主类
的load
方法先执行,然后是分类的load
方法执行。这个问题回答到此,就显的非常的初级,面对面试官没有一点的说服力,所以我们要更加深入的去表述,围绕这2个问题去表述:
-
+ (void)load()
方法是什么时候被调用的?
首先我们要知道,我们的代码最终会编译生成一个Mach-O
的可执行文件,程序在启动时,首先由libDyld
介入,对Mach-O
文件进行相应的初始化,链接,装载,具体:_dyld_start
->_dyldbootstrap::star
->dyld::_main()
->dyly::initializeMainExecutable
等。
在dyly::initializeMainExecutable
中会触发libSystem_initializer
->libdispatch_init
->_os_object_init
->_objc_init
_objc_init
是libObjc
的方法,是对objc
的各种初始化准备工作:
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init(); //初始化环境变量
tls_init(); //初始化本地线程池
static_init(); //初始化静态函数
runtime_init(); //分类,类表
exception_init(); //初始化异常处理相关 uncaught_handler
cache_init(); //缓存相关处理
_imp_implementationWithBlock_init(); //block初始化实现
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
这里面有一个比较重要的方法:
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
Dyld
在这里注册了3个回调:map_images
,load_images
,unmap_image
,
关于load
问题的答案就藏在load_images
回调函数中,到此我们先看下堆栈信息:
在
_objc_init
中我们只看到了注册回调,但是还不知道什么时候触发回调。通过对自定义类的load
方法断点,查看堆栈信息发现,dyld::notifySingle()
之后会触发load_image()
->call_load_methods()
->call_class_load()
->[ZZStudent load]
:由此我们也就说明了
load
方法被触发的整个流程。其实这里我们可以先说一下应用程序的加载流程。
- 为什么会先执行
主类
的load
,然后执行分类
的load
?
从问题1我们知道了关于load
方法相关的内容是在load_images
内部,所有我们要详细看下load_images
的实现:
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
从源码中我们可以看到2个核心的方法:
//准备load方法
prepare_load_methods((const headerType *)mh);
//调用load方法
call_load_methods();
来看下prepare_load_methods((const headerType *)mh);
的实现:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
这里主要做了2件事:
获取classlist
->schedule_class_load
->add_class_to_loadable_list
获取categorylist
->add_category_to_loadable_list
再看下void call_load_methods(void)
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
从上面的代码,我们已经找到了答案,这里已经可以非常直观的看到在do while
循环内先调用了call_class_loads()
,然后执行call_category_loads()
,所以主类的load
方法先执行,然后是分类的load
方法执行。
我们可以继续再看下call_class_loads()
和call_category_loads()
的实现:
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, @selector(load));
cats[i].cat = nil;
}
}
//省略.......
}
都是通过遍历读取load_method_t
,并调用(*load_method)(cls, @selector(load));
,来触发了load
方法的执行。
load_images
流程图:
问题3:[self class]与[super class]
#import "ZZTeacher.h"
@implementation ZZTeacher
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@ - %@",[self class],[super class]);
}
return self;
}
通常这个问题开始于输出结果的讨论,答案是输出结果是相同的。为什么呐?我们可以围绕下面几个问题展开回答:
-
self
,super
是什么?
self
:是oc 方法的隐藏参数1,还有一个隐藏参2是sel _cmd
;
super
是一个关键字,super
调用方法时会跳过本类,直接访问父类的同名方法。 -
[self class]
和[super class]
的本质?
通过clang
来看下init
代码段:
static instancetype _I_LGTeacher_init(LGTeacher * self, SEL _cmd) {
self = ((LGTeacher *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("init"));
if (self) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yy_htpy_x9s09v1zf7ms0jgytwr0000gn_T_LGTeacher_f84efe_mi_0,
((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")),
((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)(
(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))},
sel_registerName("class")));
}
return self;
}
从编译后的源码可以看到:
[self class]
最终转换为objc_msgSend(self,sel_registerName("class"))
;
[super class]
最终转换为objc_msgSendSuper((objc_super){self,(id)class_getSuperclass(objc_getClass("LGTeacher"))},sel_registerName("class")) )
,简化一下即class_getSuperclass((objc_super){self,superClass},sel)
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
该方法接收两个参数:struct objc_super
结构体类型数据和sel
再看下struct objc_super
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
这里由receiver
(消息接收者,这里是self
)和super_class
(父类self->isa->superClass
)2个参数构成;
结构体的最后注释也说明了一切:
/* super_class is the first class to search */
父类是首先要查找的类。
这里我们也就看到了[super class]
->objc_msgSendSuper({self,super_class},"class")
,其中消息的接收者本质还是self
,跟[self class]
是一致的,所以返回的结果也是相同的。
-
class
方法做了什么?
我们通过libObjc
看下-(Class)class
的实现:
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
无论是[self class]
还是[super class]
最终都会定位到NSObject
的class
方法中,从源码看本质是调用了object_getClass(self)
方法,都是获取self
的class
,即获取self->isa
并返回,也就是当前实例对象的isa
类对象(这里就是ZZTeacher
类了),所以也证明了输出结果是相同的。
- 通过汇编进一步解释
我们会看到在运行时,super
的调用发生了变化,跟我们通过clang
后看到的稍微不一样,由原来的objc_msgSendSuper()
->objc_msgSendSuper2
指令,
看下objc_msgSendSuper2
的实现(这里基于objc-msg-arm64.s
)
// no _objc_msgLookupSuper
ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
ldp p0, p16, [x0] // p0 = real receiver, p16 = class
ldr p16, [x16, #SUPERCLASS] // p16 = class->superclass
CacheLookup NORMAL, _objc_msgSendSuper2
END_ENTRY _objc_msgSendSuper2
从汇编的源码可以看到,最终p0 = receiver, p16 = class->superclass
然后跳转到CacheLookup
指令开始从父类开始查找方法并调用。(关于CacheLookup
指令之前已经详细探索过了,这里就不展开说了,可以参考objc_msgSend的本质)
这里再说一下[self class]
和[super class]
的简单查找流程:
[self class]
->self->isa
->cache_t
->bits.data
->methods()
,如果没有找到,
->self->isa->superClass
->cache_t
........
[super class]
->self->isa->superClass
->cache_t
->bits.data
......
这里就是方法查找流程相关的内容了。
完。
问题4:内存平移问题?
首先我们定义了一个ZZPerson
类,并添加了一些属性和方法
@interface ZZPerson : NSObject
@property (nonatomic,copy) NSString *zz_name;
- (void)saySomething;
@end
@implementation ZZPerson
- (void)saySomething
{
NSLog(@"%s",__func__);
}
@end
在ViewController
中添加如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
Class cls = [ZZPerson class];
void *test = &cls;
[(__bridge id)test saySomething];
ZZPerson *person = [ZZPerson alloc];
[person saySomething];
}
问题一:test
能否成功调起saySomething
方法?
答案是肯定的,可以成功调用方法。
2020-10-26 15:05:21.852772+0800 TestMemoryShift[943:484379] -[ZZPerson saySomething]
2020-10-26 15:05:21.852900+0800 TestMemoryShift[943:484379] -[ZZPerson saySomething]
这里为什么呐?
首先person
能调用saySomething
方法是毋容置疑的,本质就是发送消息,并查找对应的IMP
,即person
通过isa
,找到ZZPerson
类,然后bits.data()->methods()
,找到对应实现;
而这里的test
指向的是cls
,即ZZPerson
类,最终都会到ZZPerson
类里面,
对于编译器而言,两种方式的本质都是一样的,编译器将test
也认为是一个对象,都是找到类查找方法,所以能调用到。指针指向入下图:
通过
LLDB
也可以很直观的看到:test
和person
都指向ZZPerson
的首地址0x10459d6d0
。问题二:
我们修改下
saySomething
的打印
- (void)saySomething
{
NSLog(@"%s,-%@",__func__,self.zz_name);
}
问:两次调用的打印结果是什么?为什么?
运行后我们看到打印结果:
2020-10-26 16:04:16.529901+0800 TestMemoryShift[955:494472] -[ZZPerson saySomething],-<ViewController: 0x10fe04720>
2020-10-26 16:04:16.530042+0800 TestMemoryShift[955:494472] -[ZZPerson saySomething],-(null)
[person saySomething]
输出self.zz_name = (null)
[test saySomething]
输出self.zz_name = <ViewController: 0x10fe04720>
这里关于person
的输出(null)
很好解释,因为zz_name
没有赋值,但是test
输出<ViewController: 0x10fe04720>
就让人匪夷所思了。
这里我们就需要分析下viewDidLoad()
代码块的整个栈帧排列了,首先我们要知道栈空间都是先进后出的排列方式,存放着各种指针数据;接下来我们转换一下viewDidLoad()
,将一些隐藏参数都显示出来:
void viewDidLoad(id self, SEL _cmd) {
objc_msgSendSuper({self , superClass} , sel_registerName("viewDidLoad"))
Class cls = [ZZPerson class];
void *test = &cls;
[(__bridge id)test saySomething];
ZZPerson *person = [ZZPerson alloc];
[person saySomething];
}
栈帧分布图:
看到这个图之后,一切就变的一目了然了;
首先,
[person saySomething]
,当前方法内的self
= person
对象,self.zz_name
,也就是person
通过平移8个字节去读取zz_name
的值,此时zz_name
没有赋值,所以为空;而[test saySomething]
,方法内的self
=test
,test
本身大小只有8个字节,此时再想下查询就会读取到ViewController *self
,所以就输出了ViewController
对象。通过LLDB看下
test
的内存分布情况:
(lldb) x/4gx test
0x16dd3dc58: 0x00000001020c96d8 0x0000000102707160
0x16dd3dc68: 0x00000001020c9610 0x00000001c799421e
(lldb) po 0x00000001020c96d8
ZZPerson
(lldb) po 0x0000000102707160
<ViewController: 0x102707160>
(lldb)
这里可以看到 test
本身只有8字节并存储了ZZPerson
,而下一个8字节就已经是<ViewController: 0x102707160>
。
总结
这里都是笔者自己的认知和理解,不对的地方请指出。