关联对象补充
上节课我们在探索关联对象设置流程
,在_object_set_associative_reference
方法源码中看到析构函数AssociationsManager manager;
,这里有疑问?它为什么不是一个单例?下面进行证明它为什么不是单例...
- 进入
_object_set_associative_reference
方法,编写如下代码
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
// 这里多创建一个manager2
AssociationsManager manager2;
AssociationsHashMap &associations2(manager2.get());
直接运行objc4-818.2源码
会崩溃,原因是重复加锁AssociationsManager() { AssociationsManagerLock.lock(); }
解决办法 先把class AssociationsManager
里面的加锁、解锁屏蔽掉,如下所示
// 修改前
class AssociationsManager {
using Storage = ExplicitInitDenseMap, ObjectAssociationMap>;
static Storage _mapStorage;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &get() {
return _mapStorage.get();
}
// 这里的static只是声明init方法是个类方法,并不是单例
static void init() {
// 这里面全局存储着3张表,关联对象表 AutoreleasePool表 散列表
_mapStorage.init();
}
};
// 修改后
class AssociationsManager {
using Storage = ExplicitInitDenseMap, ObjectAssociationMap>;
static Storage _mapStorage;
public:
AssociationsManager() { }
~AssociationsManager() { }
- 重新运行工程,进行
lldb调试
(lldb) p &manager
(objc::AssociationsManager *) $0 = 0x00007ffeefbff350
(lldb) p &manager2
(objc::AssociationsManager *) $1 = 0x00007ffeefbff338
由打印信息可知AssociationsManager manager;
不是单例
-
_mapStorage.init();
全局存储着3张表关联对象表
、AutoreleasePool表
、散列表
,下面进行断点调试
void arr_init(void)
{
AutoreleasePoolPage::init(); //AutoreleasePool表
SideTablesMap.init(); //散列表(里面包含两张表,弱引用表 引用计数表)
_objc_associations_init(); //关联对象表
}
关联对象释放
关联对象
是在什么时候释放的?
关联对象
也是需要移除的,关联对象的生命周期跟着object
一起的。下面反向推导什么时候调用objc_removeAssociatedObjects
?
- 全局搜索什么时候调用
objc_removeAssociatedObjects
没有找到,那就寻找对象释放函数dealloc
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
- 进入
_objc_rootDealloc
方法
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
- 继续进入
rootDealloc
方法
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
// 判断弱引用表,关联对象表
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
#if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor &&
#else
!isa.getClass(false)->hasCxxDtor() &&
#endif
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
- 进入
object_dispose
方法
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
- 进入
objc_destructInstance
方法
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, /*deallocating*/true);
obj->clearDeallocating();
}
return obj;
}
下面查看关联对象
的整个数据结构
?
(lldb) p refs_result
(std::pair, objc::DenseMap, objc::DenseMapInfo, objc::detail::DenseMapPair >, objc::DenseMapValueInfo, objc::DenseMapInfo, objc::detail::DenseMapPair > >,
// 查看DenseMapInfo
objc::DenseMapInfo >, objc::detail::DenseMapPair, objc::DenseMap, objc::DenseMapInfo,
// 查看DenseMapPair
objc::detail::DenseMapPair > >, false>, bool>) $0 = {
// 第一指针
first = {
Ptr = 0x0000000100717900
End = 0x0000000100717980
}
// 第二指针
second = true
}
- 下面查看底层源码是怎么
包装数据结构
?
template <
typename KeyT, typename ValueT,
typename ValueInfoT = DenseMapValueInfo,
typename KeyInfoT = DenseMapInfo,
typename Bucket = detail::DenseMapPair,
bool IsConst = false>
OC底层简单面试题
面试题一:load方法在什么时候调用?
load_images
方法的主要作用是加载镜像文件,其中最重要的有两个方法:prepare_load_methods
(加载) 和 call_load_methods
(调用)
- 进入
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 发现load方法
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods(); //调用load方法
}
- 进入
prepare_load_methods
->schedule_class_load
源码,这里主要是根据类的继承链递归调用获取load
,直到cls不存在才结束递归,目的是为了确保父类的load优先加载
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
// 添加到表里边
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
- 进入
add_class_to_loadable_list
,主要是将load方法
和cls类名
一起加到loadable_classes表中
- 进入
getLoadMethod
,主要是获取方法的sel为load的方法
进入call_load_methods源码
,主要有三部分操作
- 反复调用
类的+load
,直到不再有 - 调用一次
分类的+load
- 如果有类或更多未尝试的分类,则
运行更多的+load
initialize
-
initialize
是在第一次消息发送的时候进行调用,load先于initialize
- 分类中实现
initialize
方法会被优先调用,并且本类中的initialize不会被调用, -
initialize
原理是消息发送
,所有当子类没有实现时,会调用父类。是会被调用两次 - 如果子类,父类同时实现,
先调用父类,再调用子类
load
-
load
方法在应用程序加载过程中
(dyld)完成调用,在main之前
- 在底层进行
load_images
处理时,维护了两个load的加载表,一个是本类的表
,另一个是分类的表
,所以说有先对本类的load发起调用 - 在对
类 load方法
进行处理时,进行递归处理,以确保父类优先
被处理 - 在load方法的调用顺序是
父类、子类、分类
- 在分类中load调用顺序,是根据
编译的顺序
为准
c++构造函数
- 在分析dyld后,可以确定这样个调用流程
load->c++->main
- 但是如果c++写在objc工程中,在
objc_init()
调用时,会通过static_init()
方法优先调用c++函数,而不需要等到_dyld_objc_notify_register
向dyld注册load_images
之后再调用 - 同时如果
objc_init()
自启的话也不需要dyld进行启动,也可能会发生c++函数在load方法之前调用
的情况
疑问?如果有LGA LGB LGC
三个分类,哪个分类先加载呢?
这个主要看编译顺序,如果同名方法是load方法 -- 先主类load,后分类load
(分类之间,看编译顺序
)
面试题二:Runtime是什么?
- runtime是由
C和C++汇编实现的一套API
,为OC语言加入了面向对象
、以及运行时
的功能 - 运行时是指将数据类型的确定由
编译时
推迟到了运行时
举例:extension 和 category 的区别 - 平时编写的OC代码,在程序运行的过程中,其实最终会转换成
runtime
的C语言代码, runtime是OC的幕后工作者
面试题三:方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?
方法的本质:发送消息
,消息会有以下几个流程
-
快速查找
(objc_msgSend) -cache_t
缓存消息中查找 -
慢速查找
- 递归自己|父类 -lookUpImpOrForward
- 查找不到消息:
动态方法解析
-resolveInstanceMethod
-
消息快速转发
-forwardingTargetForSelector
-
消息慢速转发
- methodSignatureForSelector & forwardInvocation
sel是方法编号 - 在read_images
期间就编译进了内存
imp
是函数实现指针 ,找imp就是找函数的过程
sel
相当于 一本书的目录title
imp
相当于 书本的页码
查找具体的函数就是想看这本书具体篇章的内容
- 首先知道想看什么,即目录
title - sel
- 根据目录找到对应的页码 -
imp
- 通过页码去翻到具体的
内容
面试题四:能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?
不能向编译后
的得到的类中增加实例变量
原因是:编译好的实例变量存储的位置是ro,一旦编译完成,内存结构就完全确定了
可以向运⾏时
创建的类中添加实例变量
,只要类没有注册到内存,运行时还是可以添加的
可以通过objc_allocateClassPair
运行时创建类,并添加属性、实例变量、方法
等
const char *className = "SHObject";
Class objc_class = objc_getClass(className);
if (!objc_class) {
Class superClass = [NSObject class];
objc_class = objc_allocateClassPair(superClass, className, 0);
}
class_addIvar(objc_class, "name", sizeof(NSString *), log2(_Alignof(NSString *)), @encode(NSString *));
class_addMethod(objc_class, @selector(addName:), (IMP)addName, "V@:");
面试题五:[self class]和[super class]的区别以及原理分析
LGTeacher
中的init方法
中打印这两种class调用
// LGTeacher继承自LGPerson
#import "LGTeacher.h"
@implementation LGTeacher
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@ - %@",[self class],[super class]);
}
return self;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGTeacher *teacher = [[LGTeacher alloc] init];
NSLog(@"%@",teacher);
}
return 0;
}
// 运行工程打印如下
2021-08-30 17:41:17.662930+0800 KCObjc[79229:10118616] -LGTeacher - LGTeacher
首先来分析[self class]
打印的为什么是LGTeacher
?
- 首先这两个类中都没有实现
class方法
,那么根据继承关系,他们最终会调用到NSObject中的class方法
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
- 由打印可知这两个方法返回的都是
self对应的类
。关于这个self是谁,这里涉及到消息发送objc_msgSend
,有两个隐形参数
分别是id self
和SEL sel
,这里主要来说下id self
-
[self class]
输出LGTeacher
,这里消息的发送者是LGTeacher对象
,通过调用NSObject的class
,但是消息的接受者没有发生变化,所以是LGTeacher
接下来分析[super class]打印的为什么是LGTeacher?
-
[super class]
中super
是语法关键字,通过命令clang -rewrite-objc LGTeacher.m -o LGTeacher.cpp
查看super的本质。下面是编译时的底层源码,其中第一个参数是消息接收者
,是__rw_objc_super
结构
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_d7_5qn4fnqn0p197t4lkw1bw1p40000gn_T_LGTeacher_c79449_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")),((Class (*)(__rw_objc_super *, SEL))
// objc_msgSendSuper
(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("class")));
}
return self;
}
- 由上面我们看到
[super class]
的低层实现是objc_msgSendSuper方法
,同时存在id self
和SEL 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);
/// Specifies the superclass of an instance.
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 */
};
- 由上面可知
id receiver
和Class super_class
两个参数,其中super_class
表示第一个要去查找的类,至此我们可以得出结论,在LGTeacher中调用[super class]
,其内部会调用objc_msgSendSuper方法
,并且会传入参数objc_super
,其中receiver是LGTeacher对象
,super_class是LGTeacher的父类
,也就是要第一个查找的类。 - 我们再来看
[super class]
在运行时是否如上一步的底层编码所示,是objc_msgSendSuper
,打开汇编调试发现会调用objc_msgSendSuper2
,查看objc_msgSendSuper2汇编源码
发现是从superclass中的cache中
查找方法
OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.6, 2.0, 9.0, 1.0, 2.0);
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
-
[super class]
打印的是LGTeacher
,原因是当前的super是一个关键字,在这里只调用objc_msgSendSuper2
,其实他的消息接收者
和[self class]
是一模一样的,所以返回的是LGTeacher
面试题六:内存平移问题
调试一
创建一个LGPerson类
,并实现实例方法saySomething
,下面代码能否正常调用?
#import "LGPerson.h"
@implementation LGPerson
- (void)saySomething{
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson *person = [LGPerson alloc];
[person saySomething];
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
}
return 0;
}
// 控制台打印信息
2021-08-30 22:26:50.334631+0800 004-内存平移问题[90979:11344738] -[LGPerson saySomething]
2021-08-30 22:26:50.335322+0800 004-内存平移问题[90979:11344738] -[LGPerson saySomething]
分析:
-
person
的 isa指向类LGPerson
,即person的首地址
指向LGPerson的首地址
,我们可以通过LGPerson的内存平移
找到cache,在cache
中查找方法 -
[(__bridge id)kc saySomething]
中的kc是来自于LGPerson
这个类,然后有一个指针kc
将其指向LGPerson的首地址
- 所以
person是指向LGPerson类的结构
,kc也是指向LGPerson类的结构
,然后都是在LGPerson中的methodList
中查找方法
调试二
接着上面的案例,我们新增一个属性kc_name
进行打印,在return 0;
前面添加断点查看控制台打印
以及lldb调试打印
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *kc_name;
- (void)saySomething;
@end
#import "LGPerson.h"
@implementation LGPerson
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.kc_name);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson *person = [LGPerson alloc];
[person saySomething];
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
}
return 0;
}
// 控制台打印信息
2021-08-30 23:09:55.448534+0800 004-内存平移问题[91230:11372868] -[LGPerson saySomething] - (null)
2021-08-30 23:09:55.449692+0800 004-内存平移问题[91230:11372868] -[LGPerson saySomething] -
// lldb调试发现[(__bridge id)kc saySomething]; 打印的self.kc_name的LGPerson内存地址与下面person内存地址相同
(lldb) p person
(LGPerson *) $0 = 0x0000600002cef160
- 首先我们了解到取出
属性的值
其实是要先计算出偏移大小,再通过内存平移
获取值。其实是Person类
内部存储着成员变量
,每次偏移8字节
进行存取。 - 至于kc打印的
self. kc_name
的值
,是因为cls只有Person类
的内存首地址,但是没有person对象
的内存结构,所以kc只能在栈里面进行内存平移
。
调试三
修改属性kc_name
关键字为retain
,代码如下
@interface LGPerson : NSObject
@property (nonatomic, retain) NSString *kc_name;
- (void)saySomething;
@end
#import "LGPerson.h"
@implementation LGPerson
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.kc_name);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
LGPerson *person = [LGPerson alloc];
person.kc_name = @"cooci";
[person saySomething];
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
}
kc
表示8字节指针,self.kc_name
的获取相当于 kc首地址的指针平移8字节找kc_name
,那么此时的kc的指针地址是多少?平移8字节获取的是什么?
-
kc
是一个指针,是存在栈中的,栈是一个先进后出
的结构,参数传入
就是一个不断压栈
的过程。其中隐藏参数也会压入栈,且每个函数都会有两个隐藏参数(id self,sel _cmd)
,可以通过clang -rewrite-objc ViewController.m -o ViewController.cpp
查看底层编译 - 隐藏参数压栈其地址是
递减的
,而栈是从高地址->低地址
分配的,即在栈中参数会从前往后
一直压
super通过clang
查看底层的编译是objc_msgSendSuper
,其第一个参数是一个结构体__rw_objc_super
(self,class_getSuperclass),那么结构体中的属性是如何压栈的?
struct kc_struct{
NSNumber *num1;
NSNumber *num2;
} kc_struct;
- (void)viewDidLoad {
[super viewDidLoad];
struct kc_struct kcs = {@(10), @(20)};
LGPerson *person = [LGPerson alloc];
}
// LGPerson *person = [LGPerson alloc]; 下一行添加断点,lldb调试
(lldb) p &person
(LGPerson **) $0 = 0x00007ffee303b008
(lldb) p *(NSNumber **)0x00007ffee303b010
(__NSCFNumber *) $1 = 0x9f631adbc08bee8a (int)10
(lldb) p *(NSNumber **)0x00007ffee303b018
(__NSCFNumber *) $2 = 0x9f631adbc08bef6a (int)20
lldb调试
得出20先加入,10后加入,因此结构体内部的压栈情况是 低地址->高地址
递增的,即栈中结构体内部的成员是反向压入栈
,而对象属性参数压栈是高地址->低地址
正向压入栈
调试三
通过下面这段代码打印栈的存储
- (void)viewDidLoad {
[super viewDidLoad];
Class cls = [LGPerson class];
void *kc = &cls; //
LGPerson *person = [LGPerson alloc];
NSLog(@"%p - %p",&person,kc);
// 隐藏参数 会压入栈帧
void *sp = (void *)&self;
void *end = (void *)&person;
long count = (sp - end) / 0x8;
for (long i = 0; i
2021-08-31 22:25:31.815326+0800 004-内存平移问题[94665:11885963] 0x7ffee1e4e030 : viewDidLoad
// viewDidLoad方法里面 [super viewDidLoad]; 压栈的为什么不是UIViewController
2021-08-31 22:25:31.815461+0800 004-内存平移问题[94665:11885963] 0x7ffee1e4e028 : ViewController
2021-08-31 22:25:31.815608+0800 004-内存平移问题[94665:11885963] 0x7ffee1e4e020 :
2021-08-31 22:25:31.815744+0800 004-内存平移问题[94665:11885963] 0x7ffee1e4e018 : LGPerson
2021-08-31 22:25:31.815897+0800 004-内存平移问题[94665:11885963] 0x7ffee1e4e010 :
由此可知栈中从高地址到低地址
的顺序的:self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - person
,self和_cmd
是viewDidLoad
方法的两个隐藏参数,是高地址->低地址
正向压栈的。class_getSuperClass 和 self
为objc_msgSendSuper2
中的结构体成员,是从最后一个成员变量,即低地址->高地址
反向压栈的
注意
- 函数隐藏参数会从前往后一直压,即从
高地址->低地址
开始入栈 - 结构体内部的成员是从
低地址->高地址
通过上面调试,这里有几个疑问?结构体压栈的原理?什么东西才会压栈?上面打印栈的存储
viewDidLoad方法里面[super viewDidLoad];
压栈的为什么不是UIViewController
?
-
临时变量
(传入函数栈帧里面的参数)才会压栈,其中viewDidLoad
方法里面的隐藏参数self
和_cmd
压入LGPerson
的class的函数栈帧中,跟viewDidLoad
的函数栈帧没有任何关系 -
viewDidLoad
方法的结构体{objc class}
为什么会压栈进来呢?[super viewDidLoad];
潜在含义是objc_msgSend发送消息
,等同与objc_msgSendSuper(&kc_objc_super, @selector(viewDidLoad))
压栈的为什么不是UIViewController
? 真机运行工程,通过汇编代码查看[super viewDidLoad];
的底层含义
- 结构体第一个参数
receiver
是self
,第二个参数是superclass
还是currentclass
? 如果当前传入的是currentclass
意味着压栈的是ViewController
,反之压栈的是UIViewController
- 现在查看
objc_msgSendSuper
底层源码实现
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.6, 2.0, 9.0, 1.0, 2.0);
- 进行
lldb调试
当前的结构体里面super_class
传入的是ViewController
,即当前类类名