关于C指针
指针的本质,是存储变量的内存首地址,即一个Int类型的整数。读取内容的时候,将指针中的内存作为内存地址,去取该内存地址对应的内容,再根据指针的类型判断取多少个字节。
一块内存中,若存放的是其它变量的地址,即为指针,如果存放的是实际内容,则为变量。
例:int *p = 10; 代表的意思就是声明指针p,类型为int,指向一块存放了10的内存。
获取对象本身在内存中的起始地址可以使用一元运算符&,&p就是一个指向变量p的指针。
而访问指针所指向的对象则使用间接引用运算符 *。
本质编译器根据指针的类型,从指针指向的内存中连续取N个字节,然后根据指针的类型解析。
关于NSObject对象
OC中所有的类有一个共同的基类NSObject。
对象的本质是C的结构体,包含父类的结构体以及自身的成员变量。
一个NSObject对象所占用的内存大小在64位系统下为使用getInstanceSize看到占用了8个字节的内存,而malloc的时候由于内存对齐分配了16个字节的内存。稍微复杂一点的对象呢,拆解到最终的基本类型来看,全部所需的长度加起来,然后根据16对齐。
例:
class: person
property:name
method: - (void)print;
id cls = [person class];
void *obj = &cls;
[(__bridge id)obj print];
访问对象内存规则,访问类的成员变量会跳过前面isa占用的8个字节,寻找对应所占的内存地址;
栈空间内存排序顺序:从高位到低位;
所以调用将obj看作对象然后调用方法,类本身占用8个字节,跳过前8个字节,然后就根据person类的内存布局寻找name对应的内存,尝试给出对应位置代码结果。
对象isa指针,指向类class。调用对象方法的时候,通过isa找到类信息,然后遍历其方法列表,找到对象方法的实现。
类isa指针,指向元类meta-class,调用类方法的时候,通过isa找到元类信息,最后找到类方法的实现。
当一个实例对象尝试调用对象方法的时候,通过isa指针找到类class查询是否有实现方法,如果没有,再通过superclass找到父类class,再尝试调用父类的对象方法,直到找到基类。
Tips:实例对象方法调用顺序: isa->类->父类->...->基类
类方法调用顺序:isa->元类->父类的元类->...->基类的元类->基类
判断对象和类的关系:
isKindOfClass 判断是否是某个类或者其派生类的实例。
isSubClassOfClass 判断类是否是某类或其派生类。
isMemberOfClass 判断对象是否是特定某类的实例。(不能检测任何类是基于NSObject类。)
关于@property
在OC中,@property会根据之后括号内的限制声明set/get方法,然后交由@sythensize实现和生成带有下划线的变量。
runtime
isa
NSObject的数据结构,
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_list_t *methods;
property_list_t *properties;
const protocol_list_t *protocols;
Class firstSubclass;
Class nextSibilngClass;
char *demangledName;
}
/* OC对象 */
struct objc_object {
void *isa;
};
/* 类对象 */
struct objc_class: objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
public:
cllass_rw_t *data() {
return bits.data();
}
objc_class *metaClass() {
return (objc_class *)((long long) isa & ISA_MASK);
}
}
实例对象为objc_object结构体,隐藏属性只有一个isa,指向类对象。
类对象实为objc_class结构体,继承objc_object,isa指向元类。
superclass 指向 父类,cache为方法缓存列表,data中是被封装好的class_rw-t。
isa在64位之前为常规指针,指向class、metaclass对象的内存地址。
在64位之后,变成了union共同体,存储着对象各种信息标识,包括:是否为优化以后的指针、是否有关联对象、是否有C++析构函数、类与元类的内存地址信息、是否弱引用、是否正在释放、引用计数等等。
对象的isa指针同ISA_MASK进行一次 按位与操作运算,才能获取到真正的对象内存地址。
结构如下:
union isa_t
{
Class cls;
uintptr_t bits;
# if __arm64__ //arm64架构
# define ISA_MASK 0x00000000ffffffff8ULL //用来取出32位内存地址使用
# define ISA_MGCIC_MASK 0x0000003f000000001ULL
# define ISA_MAGIC_VALUE 0x0000001a000000001ULL
struct {
uintptr_t nonpointer : 1; //0:代表普通指针,1:代表优化过的指针,第0位
uintptr_t has_assoc : 1; //是否设置过关联对象,如果没有设置过的话,释放会更快,第1位
uintptr_t has_cxx_dtor : 1; //是否有C++的析构函数,第2位
uintptr_t shiftcls : 33; //用来存储类指针的内存地址值,从3到35,共33位
uintptr_t magic : 6; //用于调试时判断对象是否未完成初始化,从36-41位
uintptr_t weakly_referenced : 1; //标记对象是否指向或曾经指向一个ARC的弱引用对象。第42位
uintptr_t deallocating : 1; //标记对象是否正在释放内存。第43位
uintptr_t has_sidetable_t : 1; //当对象引用计数过大时,标记需要借助sidetable来存储。第44位
uintptr_t extra_rc : 19; //表示该对象的引用计数值,表示方法:引用计数值-1。若计数值>10,则需要借助has_sidetable_rc。45-63位
///...
};
}
Tips:extra_rc占用了19位,当引用计数超过2^19-1的时候,溢出。将溢出的值除以2,将一半存入extra_rc中,一半存入sidetable中。
method_t
对方法的封装
// 函数名字,编码(返回值类型、参数类型),以及指向函数内存地址的指针
struct method_t {
SEL name;
const char *types;
IMP imp;
}
SEL代表方法名,一般叫做选择器,底层结构与char*类似(也就是说@selector(test) 等同于 "test")
1、可以通过@selector()和sel_registerName()获得
2、可以通过sel_getName()和NSStringFromSelector()转化成字符串
3、不同类中相同名字的方法,所对应的方法选择器是相同的。
IMP本质是一个函数指针,指向方法实现。用于找到函数地址并执行函数。
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
每个方法都有两个隐藏的参数,方法最终都会转化成objc_msgSend(receiver, selector)。用来通过第一个参数获取isa,然后寻找函数列表,然后通过第二个参数找到对应函数。
而types则是包含了返回值参数编码的字符串
例: "i 24 @ 0 : 8 i 16 f 20"
意思就是返回值为int类型,从24字节开始往后就是返回值的内容,4个字节。
@ 代表第一个参数,id类型, 从0-8位
:代表第二个参数,SEL类型,从8-16位
i 代表第三个参数,int类型,从16-20
f 代表第四个参数,float类型,从20-24位
class_data_bits_t
仅包含一个uintptr_t类型的变量bits,存储类的属性、方法、协议,是一个复合指针,通过不同的MAS掩码获取所需值。bits就相当于class_rw_t结构体加上rr/allloc的flag标记。
class_rw_t
真正存储所有类的属性、方法、协议等信息,以二维数组形式存储,均可读写。
以及class_ro_t常量指针。在运行时可以动态添加成员变量、方法。
class_ro_t
常量修饰,在编译器就已确定类的ivar属性、成员变量、方法、遵循的协议,全部为一维数组。只读属性。
在编译器来确定类的内存大小。
在编译器的时候class_data_bits_t指向该指针。
cache_t
通过散列表形式缓存已调用过的类方法,以提升访问速度。
以selectot方法名为key,映射存储对应的IMP实现。
内部结构:
struct cache_t {
struct bucker_t * _buckets;
mask_t _mask;
mask_t _occupied;
}
struct bucker_t {
cache_key_t _key;
IMP _imp;
}
Tips:以对象方法为例查找
1、通过isa查找到指定的class
2、从cache中查找若存在缓存,则直接调用。
3、若缓存中不存在方法,则在自己的class里bits的class_rw_t中查找方法。
4、若找到该方法则调用,并将方法缓存至cache中。
5、若没有找到,则通过superclass找到父类,继续从父类class里bits的class_rw_t中查找方法。
6、若在父类中找到,则直接调用并缓存至自己的class中,若找不到,则一直向上查找,直到基类,然后走消息转发流程。
存储过程:
将SEL函数名与_mask进行位运算,将函数内存地址尝试放入计算得出的散列表中。若该位置已经存在其它函数内存地址,在arm64环境下尝试存储在当前位置的前一位,若一直找到第一位都不符合条件,会去最后一位继续尝试存储。如果当前散列表的空间大小不足以存储现有的缓存函数则先进行扩容,并将当前存储的所有缓存的函数重新运算以后再存储。
查找过程:
通过SEL函数名与_mask进行位运算,直接得出存储于散列表中的函数内存地址。