全局表
三张全局表:关联对象表、 弱引用表 、引用计数表
void arr_init(void)
{
AutoreleasePoolPage::init();
SideTablesMap.init(); //散列表
_objc_associations_init();// 关联对象表
}
其中散列表里面包含:弱引用和引用计数表
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; //引用计数表
weak_table_t weak_table;// 弱引用
}
关联对象移除时机
在函数objc_removeAssociatedObjects
找到_object_remove_assocations(object, /*deallocating*/false);
查找调用的方法链
object_dispose(id obj)
->objc_destructInstance
-> _object_remove_assocations
关联对象跟obj绑定
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;
}
load方法在什么时候调用
_objc_init
里的_dyld_objc_notify_register(&map_images, load_images, unmap_image)
load_images
的方法里面,进行单个类的收集在一个类的load方法表和一个分类的load方法表
load_images
-> prepare_load_methods
-> schedule_class_load
->add_class_to_loadable_list
-> add_category_to_loadable_list
-> call_load_methods
-> call_class_loads
-> call_category_loads
调用顺序:
objc内的C++函数(static_init的时候调用),也就是自启的c++函数 > load()
> C++函数 > initialize()(第一次消息发送lookupImp的时候调用)
> main
分类load的加载顺序,主要由编译的顺序决定
Runtime
runtime 是由C 和C++ 汇编 实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能,运⾏时(Runtime)是指将数据类型的确定由编译时推迟到了运⾏时。平时编写的OC代码,在程序运⾏过程中,其实最终会转换成Runtime的C语⾔代 码,Runtime
是 Object-C
的幕后⼯作者 。
比如 addMethods、 addPropertys、 rwe
⽅法的本质,sel和imp分别是什么
sel 是⽅法编号 ~ 在read_images 期间就编译进⼊了内存 imp 就是我们函数实现指针 ,找imp 就是找函数的过程 sel 就相当于书本的⽬录 tittle imp 就是书本的⻚码 查找具体的函数就是想看这本书⾥⾯具体篇章的内容 1:我们⾸先知道想看什么 (sel) 2:根据⽬录对应的⻚码 (imp) 3:翻到具体的内容 ⽅法的本质:发送消息 , 消息会有以下⼏个流程 1:快速查找 (objc_msgSend)~ cache_t 缓存消息 2:慢速查找~ 递归⾃⼰| ⽗类 ~ lookUpImpOrForward 3:查找不到消息: 动态⽅法解析 ~ resolveInstanceMethod 4:消息快速转发~ forwardingTargetForSelector 5:消息慢速转发~ methodSignatureForSelector & forwardInvocation
能否向编译后的类中增加实例变量
不能,因为我们编译好的实例变量存储的位置在 ro,⼀旦编译完成,内存结构就完全确定就⽆法修改。可以添加属性和方法。
能否向运行时创建的类中添加实例变量
看时机,只要类没有注册到内存还是可以添加的.在方法objc_registerClassPair
调用之前都可以。在源码中找到class_addIvar
BOOL class_addIvar(Class cls, const char *name, size_t size,
uint8_t alignment, const char *type)
{
bool result = YES;
if (!cls) return NO;
if (ISMETA(cls)) return NO;
if (!(cls->info & CLS_CONSTRUCTING)) return NO;
//...
}
!(cls->info & CLS_CONSTRUCTING)
这个是个关键,只要这个成立,就不能添加实例变量,那么我们看下,这个条件什么时候成立。在objc_registerClassPair
中
void objc_registerClassPair(Class cls)
{
//...
// Clear "under construction" bit, set "done constructing" bit
cls->info &= ~CLS_CONSTRUCTING;
cls->ISA()->info &= ~CLS_CONSTRUCTING;
cls->info |= CLS_CONSTRUCTED;
cls->ISA()->info |= CLS_CONSTRUCTED;
NXHashInsertIfAbsent(class_hash, cls);
}
[self class]
和[super class]
的区别以及原理分析
self
是个形参名,而super
是个关键字
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@ - %@",[self class],[super class]);// 打印Person, Person
}
return self;
}
这里的[self class]
其实调用的是NSObject的class方法。就是发送消息objc_msgSend(void/* id self, SEL op, ... */ )
,消息接受者是 self
- (Class)class {
return object_getClass(self); // self是指隐藏参数(id self , sel _cmd)
}
super
其实是个关键字objc_msgSendSuper(void/* struct objc_super *super, SEL op, ... */ )
是个struct objc_super
的结构体
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;//第一个去查找的类
};
[super class]
本质就是objc_msgSendSuper
, 消息的接受者还是self ⽅法编号:class ,只是objc_msgSendSuper
会更快直接跳过 self 的查找
objc_msgSendSuper
在汇编的时候自动变成了objc_msgSendSuper2
内存平移问题
Person
内有一个对象方法saySomething
,下面这种写法是否能调起该方法
- (void)saySomething{
NSLog(@"%s",__func__);
}
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [Person alloc];
Class cls = [Person class];
void *kc = &cls;
[person saySomething];
[(__bridge id)kc saySomething];
}
打印输出,我们发现都能调起
我们知道saySomething
在Person类的data中,person可以通过isa找到类然后经过平移就可以得到data中的方法。而kc是一个指针,指向的是Person类的所以也能得到data中的方法。
我们重新修改一下saySomething
的实现
@property (nonatomic, copy) NSString *name;
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.name);
}
这里的[(__bridge id)kc saySomething];
为什么会输出person的地址呢?
person
vs kc
区别:
person是一个已经开辟了内存的对象,而kc只是一个指针而已,没有内存空间。person可以通过内存平移找到name(self+ offset)
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [Person alloc];
Class cls = [Person class];
void *kc = &cls;
[person saySomething]; // person + 0x8
[(__bridge id)kc saySomething];// cls + 0x8
}
函数的栈帧是一个栈结构,我们找到cls+0x8的上一个对象是谁就ok了 所以这也解释出了上面为什么会打印出person。
压栈
在name之前新增一个属性,此时看下栈帧情况
@property (nonatomic, copy) NSString *hobby;
@property (nonatomic, copy) NSString *name;
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.name);
}
- (void)viewDidLoad {// 参数压栈(id self, sel _cmd)
[super viewDidLoad];// 结构体压栈(objc, class)
Person *person = [Person alloc];
Class cls = [Person class];
void *kc = &cls;
[person saySomething]; // person + 0x10
[(__bridge id)kc saySomething];// cls + 0x10(16个字节因为新增了一个8字节的属性)
}
那么这个ox10
到底指的哪里,这个viewDidLoad
到底哪些东西压栈了
1.参数压栈(隐藏参数)
2.结构体压栈(super关键字)
结构体压栈
struct kc_struct{
NSNumber *num1;
NSNumber *num2;
} kc_struct;
结构体压栈情况,结构体压栈成员变量是是从后向前压栈。
参数压栈
// 高地址 -> 低地址
void kcFunction(id p1, id p2){
NSLog(@"%p\n",&p1);
NSLog(@"%p",&p2);
}
所以参数压栈是从前往后压。
- (void)viewDidLoad {// 参数压栈(id self, sel _cmd)
[super viewDidLoad];// 结构体压栈(receiver, class)
Person *person = [Person alloc];
Class cls = [Person class];
void *kc = &cls;
[person saySomething]; // person + 0x10
[(__bridge id)kc saySomething];// cls + 0x10(16个字节因为新增了一个8字节的属性)
}
重新回来这个代码,可以得出压栈顺序:
所以[(__bridge id)kc saySomething]
打印的是ViewController