上一篇文章《iOS-底层原理16-类扩展和关联对象底层原理》介绍了关联对象底层原理
1.分析AssociationsManager不唯一和AssociationsHashMap唯一
模拟新建AssociationsManager和AssociationsHashMap两个,查看内存地址是否一致,先将AssociationsManager构造函数中的锁去掉,否则会死锁
不同的AssociationsManager manager和manager1创建的associations和associations1内存地址一样,唯一
AssociationsManager构造函数去掉锁
manager不唯一,manager内存地址不可读,调用初始化方法,并不会调用init方法,那么init方法在什么时候调用的呢?断点调试下
AssociationsManager() { }
~AssociationsManager() { }
在arr_init()中调用AssociationsManager::init()方法,属于类方法对AssociationsManager进行环境准备并没有初始化,init()方法并没有返回值,map_images-->arr_init()-->_objc_associations_init()-->AssociationsManager::init()-->_mapStorage.init()
2.加锁的原因:会对唯一的表AssociationsHashMap中的数据进行读取和安放,防止多线程对数据进行篡改而导致数据不同步
相当于如下:操作前加锁,操作后解锁
3.整体结构:
AssociationsHashMap:整个项目
LGPerson LGTeacher LGStudent
对象 key -> ObjctAssociationMap (LGPerson)(name age hobby)
key -> ObjcAssociation(policy value)
Buckets桶子里面包含桶子,Buckets桶子的结构为objc::detail::DenseMapPair<const void *, objc::ObjcAssociation>,结构复用,代码复用
4.面试题:请问关联对象设置后是否应该移除?不需要移除,为什么呢?
对象在释放的时候会自动移除,进入dealloc方法,- (void)dealloc
-->_objc_rootDealloc(self)
--> obj->rootDealloc()
--> object_dispose((id)this)
--> objc_destructInstance(obj)
--> _object_remove_assocations(obj)
,从总表AssociationsHashMap中挨个移除,Bucket移除
5.面试题:主类和分类同名方法,优先调用哪一个?
- 非load方法会先调用分类中的
-
2.load方法会先调用主类的,再调用分类的,为什么会是先主类后分类呢?
5.load_images分析
prepare_load_methods((const headerType *)mh)
--> schedule_class_load(remapClass(classlist[i]))
--> add_class_to_loadable_list(cls)
在schedule_class_load方法中进行递归,将父类中的方法进行添加loadable_classes,若类有load方法,将类添加到loadable_classes中,已经开辟的和正在使用的classes_loadable类数量是否相等,若相等,则进行扩容
method = cls->getLoadMethod(),判断是否是load方法,返回imp
类中的load方法加载完成,再加载分类中的load方法,加到loadable_categories,若已经使用的分类和开辟的相等,则进行扩容
调用load方法call_load_methods(),循环先调用类中load方法,后调用分类中load方法,函数指针消息发送(*load_method)(cls, @selector(load))传入两个参数cls和@selector(load)
调用完毕后,调用自动释放池回收整片内存空间
load_images分析流程图
面试题
initialize方法在第一次消息发送的时候调用
load方法调用是先主类后分类
其他方法并不是分类覆盖了主类,而是分类中的方法编译时写在了前面,会先调用
Runtime是什么
runtime是由C和C++汇编实现的一套API,为OC语言加入了面向对象,运行时的功能
运行时(Runtime)是指将数据类型的确定由编译时推迟到了运行时
举例子:extension-category的区别
平常编写的OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代码,Runtime
是Object-C
的幕后工作者
6.[self class]和[super class]的区别
2020-12-27 19:12:35.385708+0800 KCObjc[65535:2691177] LGTeacher - LGTeacher
2020-12-27 19:12:35.386547+0800 KCObjc[65535:2691177] <LGTeacher: 0x100507b90>
- 1.[self class]会调用object_getClass(self),返回对象的isa,也就是类LGTeacher
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
-
2.super是关键字,clang LGTeacher.m查看LGTeacher的源码得到
[super class]编译为了如下代码,__rw_objc_super为中间形态,搜objc_msgSendSuper源码
((Class (*)(__rw_objc_super *, SEL))(void
*)objc_msgSendSuper)((__rw_objc_super){(id)self,
(id)class_getSuperclass(objc_getClass("LGTeacher"))},
sel_registerName("class"))
objc_msgSendSuper结构第一个参数为结构体struct objc_super *super,结构体中有两个参数id receiver,class super_class,故源码中是self调用sel_registerName("class"),在方法instancetype _I_LGTeacher_init中的self为LGTeacher对象,[super class]相当于是[self class],和[self class]是同一套调用源码流程顺序,会调用object_getClass(self),返回对象的isa,也就是类LGTeacher
// super : 关键字
// [super class] (class)(id self , sel _cmd)
// self->isa LGTeacher
// self 消息的接受者 LGTeacher
运行程序,断点object_getClass,发现方法会进入两次,间接证明输出的是LGTeacher
3.[self class]和[super class]的区别,self再去查找class方法的时候,不先从本类中去查找了,直接从父类中去查找,跳过了本类的查找流程,比[self class]查找速度更快super_class is the first class to search,消息的接收者还是self,查找的方式变化了
假设将[super class]改为[LGPerson class]呢,查看源码
由上面可知编译时期[super class]调用的方法为objc_msgSendSuper,那么运行时期呢?实质上调用的方法为objc_msgSendSuper2
将当前类传入结构体struct objc_super中,在结构体内部再取当前类的父类,而不是现在就把当前类的父类传进去,结构体内部objc_super中会从类的父类super_class开始查找,这一点和objc_msgSendSuper不一样,消息的接收者receiver还是本类self,[super class]还是输出LGTeacher
[self class]从本类中查找class方法,[super class]从父类中开始查找class方法
查看汇编代码:是直接从superclass中查找class方法
完整回答:[self class]和[super class]两个都会输出LGTeacher,[self class]调用的本质是消息发送msgSend,通过调用class底层方法获取到对象的isa即LGPerson元类型,类已经加载到内存,获取元类类型,在map_images的readClass方法中类名已经加载到类名表中,读取%@时是一个字符串类型,打印LGTeacher,super是一个关键字,底层调用objc_msgSendSuper2,消息接收者为self,和[self class]消息接收者一模一样,返回LGTeacher
7.内存平移
- (void)viewDidLoad {
[super viewDidLoad];
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
}
#import "LGPerson.h"
@implementation LGPerson
- (void)saySomething{
NSLog(@"%s",__func__);
}
输出的结果一模一样,可以正常调用实例方法,kc调用saySomething和person对象调用saySomething指向的内存空间一致,可以调用
-
在saySomething方法中增加获取self.kc_name,[person saySomething]打印的self.kc_name为nil,[(__bridge id)kc saySomething]打印的self.kc_name为什么呢?
打印出来的结果为-[LGPerson saySomething] - <ViewController: 0x7f984b6063a0>
,为什么打印的是ViewController???
-
1.分析[person saySomething]调用self.kc_name的内存平移情况,在函数-(void)viewDidLoad中,栈的情况,栈是先进后出,最先压栈的是-(void)viewDidLoad方法中的隐藏的两个参数(self,_cmd),之后[super viewDidLoad]方法clang编译会生成一个结构体
{(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}
结构体两个参数self和super_class分别压入栈中
2.分析栈的情况
隐藏参数会压入栈帧:参数从前往后一直压栈,栈区是从高地址到低地址,person先压入栈分配的是高地址,person2和person3后压入栈,依次分配比person低的地址
- 函数调用的压栈情况[super viewDidLoad]分析:查看编译源码分析能得到,会生成一个结构体{(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))},会传入结构体属性self和(id)class_getSuperclass(objc_getClass("ViewController")),那么结构体属性,是怎么一个压栈情况呢
objc_msgSendSuper({(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("class")));
结构体属性的压栈情况:结构体里面的属性num2的内存地址为0x00007ffeed838178,num1的内存地址为0x00007ffeed838170,person3的内存地址为0x00007ffeed838168,说明结构体中的属性,后面的属性先压栈,即先压入20,再压入10,因此objc_msgSendSuper方法中的结构体{(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}先入栈的是super_class,后入栈的是self,压栈顺序为self-->cmd-->(id)class_getSuperclass(objc_getClass("ViewController"))-->self
完整压栈顺序:self-->cmd-->(id)class_getSuperclass(objc_getClass("ViewController"))-->self-->cls-->kc-->person
理解指针和地址的概念:p是指针变量,在栈中赋值前后p和q的地址0x7ffee5373160、0x7ffee5373158不变,给指针变量p赋值&a(a的地址0x7ffee537316c),p的地址0x7ffee5373160中存放a的地址0x7ffee537316c,a的内存地址0x7ffee537316c中存放了整形数值10,q的地址0x7ffee5373158存放p的地址0x7ffee5373160,q取双重指针得到10
- 3.理解了指针和地址的概念,查看当前栈中内存地址情况,*(void *)address打印address这一个内存地址中存放的地址所
指向的区域,比如address的内存地址中存放
kc的内存地址,则(void **)address为kc的内存
地址中所存放的对象或值的内存情况,kc的内存地址中指向的内容为cls的内存地址
kc的内存地址中存放的内容为cls的内存地址,cls的内存地址中存放的是[LGPerson class],address的内存地址中存放的是kc的内存地址
address的内存地址
self为消息接收者 - LGPerson <LGPerson: 0x7ffee119e178>
从person对象中找到唯一的属性kc_name,需要将person对象内存地址平移一个isa指针8字节的位置,获取kc_name的值,0x7ffee119e178平移8字节得0x7ffee119e180,0x7ffee119e178+0x8 = 0x7ffee119e180正好是ViewController的内存地址
(0x7ffee119e180 - <ViewController: 0x7fd47c40b3a0>)
,故会输出-[LGPerson saySomething] - <ViewController: 0x7fd47c40b3a0>
//LGPerson: 0x7ffee119e178
//person VS LGPerson(实例化)(isa)
//kc -> LGPerson (实例化) kc_name
第二个问题:为什么第三个参数super_class返回的是ViewController?(id)class_getSuperclass(objc_getClass("ViewController")),因为objc_msgSendSuper2返回的是当前的类ViewController,为什么不是ViewController的父类UIViewController呢???
[super viewDidLoad]方法,运行时经过汇编走的代码是objc_msgSendSuper2,在进入汇编之前要传入两个参数,一个是结构体指针struct objc_super * _Nonnull super
,一个是SEL _Nonnull op
为sel_registerName("viewDidLoad"),查看结构体struct objc_super中存在两个参数一个是receiver,一个是super_class,消息的接收者为self本类,super_class为多少呢???
查看objc_msgSendSuper2的解释,super_class传入的是当前类ViewController并不是当前类的父类UIViewController,在汇编中去查找方法viewDidLoad时才去查找当前类的父类,若传入的是当前类的父类UIViewController,则在汇编中查找的是父类的父类UIResponer,
所以第三项打印为当前类(本类)ViewControler,objc_msgSendSuper2的汇编代码实质为
objc_msgSendSuper2({self, objc_getClass("ViewController")}, sel_registerName("viewDidLoad"));
改一下LGPerson中的属性结构,结果变化如何:增加一个属性,则平移isa + kc_hobby总共16字节,0x7ffee7808178 + 0x8 + 0x8 = 0x7ffee7808188
为self ——> ViewController
再改下LGPerson的属性结构
此时person对象<LGPerson: 0x7ffee5d98178>平移isa(8字节)再平移int类型(4字节),即将内存地址
0x7ffee5d98180 - <ViewController: 0x7ff545f0ac70>
劈开一半,读取出来为数值1463859280,不是一段完整的数据,程序没有崩溃
此面试题的一次,外层传一个cls,无论是什么cls,都可以用kc接收,更加实现多态化,但是不安全,对象属性变化,内存访问不到,会崩溃
kc为嘛能调用LGPerson中的对象方法
Class cls = [LGPerson class];
void *kc = &cls;//ISA
LGPerson *person = [LGPerson alloc];// person - 指针 - ISA -> LGPerson
person指针地址里面有ISA,ISA指向LGPerson,cls的首地址是ISA,kc相当于ISA,故能调用类中的方法