1.我们关联的对象是否需要手动移除,为什么
不需要手动移除,在对象的dealloc中
在关联对象时,如果是第一次,我们会设置对象的has_assoc为true,看dealloc代码
- (void)dealloc {
_objc_rootDealloc(self);
}
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);
}
}
2.类的方法和分类的方法重名调用顺序
一般方法是优先调用分类的方法(包括initialize),因为在添加分类方法是把分类的方法插入到本类methodlist的前面。
load方法呢?
我们来看一下iOS对load的处理load_images,load_images中对load进行两个处理
,prepare_load_methods和call_load_methods
prepare_load_methods
1.schedule_class_load
通过递归,通过add_class_to_loadable_list把类加入到数组中,如果父类没有实现load方法则不加入,如果数组申请的内容小了,则扩容,扩容原则是之前容量的2倍加16.
2.add_category_to_loadable_list
通过加载顺序把分类和方法加入到数组中,扩容方式和schedule_class_load相同。
call_load_methods
调用顺序,先调用本类的,按照schedule_class_load的数组存入先后顺序,所以父类的load优于子类先调用,然后调用分类的load的方法。
补充 :initialize是系统自己调用的,在类或者对象第一次调用方法时系统调用initialize,先父类再子类,如果分类实现了initialize,会调用分类的initialize,不调用本类的initialize方法,因为分类的initialize在methodlist中在本类initialize的前面。
3.[self class]和[super class]的不解之缘
@interface LGPerson : NSObject
@end
@implementation LGPerson
@end
@interface LGTeacher : LGPerson
@end
@implementation LGTeacher
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@ - %@",[self class],[super class]);
}
return self;
}
@end
我们调用[ LGTeacher alloc] init];打印日志如下
LGTeacher - LGTeacher,是不是出乎我们意料,但是super 到底做了什么呢?
看汇编[super class]调到objc_msgSendSuper2方法。
objc_msgSendSuper2做了什么呢?
objc_msgSendSuper2是从父类的cache中查询class方法,如果没有则从父类的方法列表中查询class,因为class的实现是在NSObject所以无论是[self class]还是[Super class]调用的方法都是从NSObject方法列表中找到的
- (Class)class {
return object_getClass(self);
}
又因为[self class]和[Super class]的接收者不变,所以打印一直。
objc_msgSendSuper2的调用过程可以参考objc_msgSend缓存中读取IMP和objc_msgSend慢速查找两者流程大同小异,在此就不做分析了。
4.下面的调用会成功吗?
Class cls = [LGPerson class];
void *kc = &cls; //
LGPerson *person = [LGPerson alloc];
[(__bridge id)kc saySomething]; // 1 2 - <ViewController: 0x7f7f7ec09490>
[person saySomething];
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString* kc_name;
- (void)saySomething;
@end
@implementation LGPerson
- (void)saySomething{
NSLog(@"%s ",__func__);
}
@end
打印日志
2020-10-22 22:24:19.290465+0800 004-内存平移问题[63882:4725050] -[LGPerson saySomething]
2020-10-22 22:24:19.290639+0800 004-内存平移问题[63882:4725050] -[LGPerson saySomething]
为什么呢?
方法调用实质上是发送消息,发送消息objc_msgSend(objc_msgSendSuper)里面自带两个参数接收着和方法编号,接收者实质上是内存地址,然后通过快速查找和慢速查找寻找方法的实现IMP。查看kc和person的内存地址情况
(lldb) x/4gx kc
0x7ffee54b6038: 0x000000010a74f648 0x00007ffea74046a0
0x7ffee54b6048: 0x000000010a74f580 0x00007fff5e0889bb
(lldb) x/4gx person
0x600000e0cca0: 0x000000010a74f648 0x0000000000000000
0x600000e0ccb0: 0x0000000000000000 0x0000000000000000
(lldb)
[person saySomething]调用情况是,读0x600000e0cca0地址可知isa地址0x000000010a74f648,查找sel的imp实现消息的发送,
[(__bridge id)kc saySomething];调用情况,读0x7ffee54b6038地址可知isa地址0x000000010a74f648,和 [person saySomething]一样
修改saySomething方法
- (void)saySomething{
NSLog(@"%s--%@",__func__,self.kc_name);
}
Class cls = [LGPerson class];
void *kc = &cls; //
LGPerson *person = [LGPerson alloc];
person.kc_name = @"name";
[(__bridge id)kc saySomething];
[person saySomething];
打印结果
[LGPerson saySomething]--<ViewController: 0x7fe4406066c0>
[LGPerson saySomething]--name
这又是为什么呢?
查看内存情况
(lldb) x/4gx kc
0x7ffee5bfb038: 0x000000010a00a5f0 0x00007fe4406066c0
0x7ffee5bfb048: 0x000000010a00a528 0x00007fff5e0889bb
(lldb) p *(void **)0x7ffee5bfb040
(ViewController *) $1 = 0x00007fe4406066c0
(lldb) x/4gx person
0x600001b0c420: 0x000000010a00a5f0 0x000000010a005038
0x600001b0c430: 0x0000000000000000 0x0000000000000000
(lldb) p *(void **)0x600001b0c428
(__NSCFConstantString *) $3 = 0x000000010a005038 "name"
我们去self.kc_name的值,根据LGPerson对象的内存布局,需要对象起始地址偏移8个字节,然后读取地址得到self.kc_name的值
kc的其实地址为0x7ffee5bfb038加8字节得到0x7ffee5bfb040读取地址得到(ViewController *) $1 = 0x00007fe4406066c0,同理person起始地址偏移8字节得到0x600001b0c428读取地址得到0x000000010a005038 "name"。那为什么kc偏移8地址读取到的是ViewController呢?下面就介绍压栈
5.压栈
函数的压栈规律
void kcFunction(id person, id kcSel, id kcSel2){
NSLog(@"person = %p",&person);
NSLog(@"person = %p",&kcSel);
NSLog(@"person = %p",&kcSel2);
}
LGPerson *person = [LGPerson alloc];
kcFunction(person, person, person);
打印结果
person = 0x7ffeea97cfd8
person = 0x7ffeea97cfd0
person = 0x7ffeea97cfc8
函数中指针压栈过程是从高地址到低地址
结构体的压栈规律
struct kc_struct{
NSNumber *num1;
NSNumber *num2;
} kc_struct;
struct kc_struct struct1 = {@(10),@(20)};
lldb调试
(lldb) p &struct1
(kc_struct *) $8 = 0x00007ffee39b9018
(lldb) p *(NSNumber **)0x00007ffee39b9018
(__NSCFNumber *) $9 = 0xa9b8175a99c3a687 (int)10
(lldb) p *(NSNumber **)0x00007ffee39b9020
(__NSCFNumber *) $10 = 0xa9b8175a99c3a767 (int)20
结构体中指针压栈过程是从低地址到高地址
viewDidLoad指针压栈情况
- (void)viewDidLoad {
[super viewDidLoad];
// ViewController 当前的类
// self cmd (id)class_getSuperclass(objc_getClass("LGTeacher")) self cls kc person
Class cls = [LGPerson class];
void *kc = &cls; //
LGPerson *person = [LGPerson alloc];
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);
}
}
// LGPerson - 0x7ffeea0c50f8
[(__bridge id)kc saySomething]; // 1 2 - <ViewController: 0x7f7f7ec09490>
[person saySomething]; // self.kc_name = nil - (null)
//
}
打印结果
0x7ffee0ca3058 : <ViewController: 0x7f9be6408500>
0x7ffee0ca3050 : viewDidLoad
0x7ffee0ca3048 : ViewController
0x7ffee0ca3040 : <ViewController: 0x7f9be6408500>
0x7ffee0ca3038 : LGPerson
0x7ffee0ca3030 : <LGPerson: 0x7ffee0ca3038>
1.0x7ffee0ca3058 和0x7ffee0ca3050 :
viewDidLoad方法默认两个对象第一个是id第二个是cmd
所以打印ViewController对象和方法
- 0x7ffee0ca3048和0x7ffee0ca3040
因为调用[super viewDidLoad];会产生一个结构题第一个是接收者第二个是Class
有因为结构体压栈规律是低到高,所以class是被压倒高地址,接收者self被压倒低地址。
6.Runtime是什么
runtime 是由C 和C++ 汇编 实现的一套API,为OC语言加入了面向对象,运行时的功能
运行时(Runtime)是指将数据类型的确定由编译时推迟到了运行时
平时编写的OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代 码,Runtime
是 Object-C
的幕后工作者
7.方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?
方法的本质:发送消息 , 消息会有以下几个流程
1:快速查找 (objc_msgSend)~ cache_t 缓存消息
2:慢速查找~ 递归自己| 父类 ~ lookUpImpOrForward
3:查找不到消息: 动态方法解析 ~ resolveInstanceMethod
4:消息快速转发~ forwardingTargetForSelector
5:消息慢速转发~ methodSignatureForSelector & forwardInvocation
sel 是方法编号 ~ 在read_images 期间就编译进入了内存 imp 就是我们函数实现指针 ,找imp 就是找函数的过程 sel 就相当于书本的目录 tittle
查找具体的函数就是想看这本书里面具体篇章的内容
1:我们首先知道想看什么 ~ tittle (sel)
2:根据目录对应的⻚码 (imp)
3.翻到具体的内容
imp与SEL 的关系
SEL : 方法编号
IMP : 函数指针地址
SEL 相当于书本目录的名称
IMP : 相当于书本目录的⻚码
1:首先明白我们要找到书本的什么内容 (sel 目录里面的名称)
2:通过名称找到对应的本⻚码 (imp)
3:通过⻚码去定位具体的内容
8能否向编译后的得到的类中增加实例变量?
能否想运行时创建的类中添加实例变量
答案:
1:不能向编译后的得到的类中增加实例变量
2:只要类没有注册到内存还是可以添加
原因:我们编译好的实例变量存储的位置在 ro,一旦编译完成,内存结构就完全确定 就无法修改
可以添加属性 + 方法
9.Runtime是如何实现weak的,为什么可以自动置nil
1.通过SideTable找到我们的weak_table
2.weak_table 根据referent 找到或者创建 weak_entry_t 3.然后append_referrer(entry, referrer)将我的新弱引用的对象加进去entry 4.最后weak_entry_insert 把entry加入到我们的weak_table
为什么可以自动置为nil呢
在对象dealloc时被置为nil的
dealloc->_objc_rootDealloc()->[ obj->rootDealloc()]->object_dispose((id)this)->objc_destructInstance(obj)->[obj->clearDeallocating()]-> clearDeallocating_slow();
补充 SideTable是什么时候初始化的
在map_images中初始化的
map_images->map_images_nolock->arr_init()->SideTablesMap.init();