OC基础之底层系列
# 内存对齐的原因
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器。这需要做很多工作。 现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。
# 内存对齐的原理
内存在进行IO的时候,一次操作取的就是64个bit。
所以,内存对齐最最底层的原因是内存的IO是以64bit为单位进行的。 对于64位数据宽度的内存,假如cpu也是64位的cpu(现在的计算机基本都是这样的),每次内存IO获取数据都是从同行同列的8个chip中各自读取一个字节拼起来的。从内存的0地址开始,0-63bit的数据可以一次IO读取出来,64-127bit的数据也可以一次读取出来。CPU和内存IO的硬件限制导致没办法一次跨在两个数据宽度中间进行IO。
假如对于一个c的程序员,如果把一个bigint(64位)地址写到的0x0001开始,而不是0x0000开始,那么数据并没有存在同一行列地址上。因此cpu必须得让内存工作两次才能取到完整的数据。效率自然就很低。这下你有没有彻底理解了内存对齐?
扩展1:如果不强制对地址进行操作,仅仅只是简单用c定义一个结构体,编译和链接器会自动替开发者对齐内存的。尽量帮你保证一个变量不跨列寻址。
扩展2:其实在内存硬件层上,还有操作系统层。操作系统还管理了CPU的一级、二级、三级缓存。实际中不一定每次IO都从内存出,如果你的数据局部性足够好,那么很有可能只需要少量的内存IO,大部分都是更为高效的高速缓存IO。但是高速缓存和内存一样,也是要考虑对齐的。
# 内存对齐规则
- 基本类型的对齐值就是其sizeof值;
- 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行;
- 结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行;
点击查看
//2020.05.12 公众号:C语言与CPP编程
#include<stdio.h>
struct
{
int i;
char c1;
char c2;
}Test1;
struct{
char c1;
int i;
char c2;
}Test2;
struct{
char c1;
char c2;
int i;
}Test3;
int main()
{
printf("%d\n",sizeof(Test1)); // 输出8
printf("%d\n",sizeof(Test2)); // 输出12
printf("%d\n",sizeof(Test3)); // 输出8
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
默认#pragma pack(4),且结构体中最长的数据类型为4个字节,所以有效对齐单位为4字节,下面根据上面所说的规则以第二个结构体来分析其内存布局:首先使用规则1,对成员变量进行对齐:
sizeof(c1) = 1 <= 4(有效对齐位),按照1字节对齐,占用第0单元; sizeof(i) = 4 <= 4(有效对齐位),相对于结构体首地址的偏移要为4的倍数,占用第4,5,6,7单元; sizeof(c2) = 1 <= 4(有效对齐位),相对于结构体首地址的偏移要为1的倍数,占用第8单元; 然后使用规则2,对结构体整体进行对齐:
第二个结构体中变量i占用内存最大占4字节,而有效对齐单位也为4字节,两者较小值就是4字节。因此整体也是按照4字节对齐。由规则1得到s2占9个字节,此处再按照规则2进行整体的4字节对齐,所以整个结构体占用12个字节。
根据上面的分析,不难得出上面例子三个结构体的内存布局如下:
## 一个OC对象占用多少内存
系统分配了16个字节给NSObject对象(通过malloc_size函数获得) 但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)
# OC对象的分类
OC对象 可以分为3种:
- instance对象 (实例对象)
- class对象 (类对象)
- meta-class对象 (元类对象)
# instance对象 (实例对象)
instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象
instance对象在内存中存储的信息包括 -isa指针 -其他成员变量
# Class对象 (类对象)
我们平时说的类,其实也是对象,称为类对象, 每个类在内存中有且只有一个class对象
# cache_t结构
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
...
};
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
public:
inline cache_key_t key() const { return _key; }
inline IMP imp() const { return (IMP)_imp; }
inline void setKey(cache_key_t newKey) { _key = newKey; }
inline void setImp(IMP newImp) { _imp = newImp; }
void set(cache_key_t newKey, IMP newImp);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
以上bucket_t的属性和方法中可以看出它应该与imp有联系——事实上bucket_t作为一个桶,里面是用来装imp方法实现以及它的key
LRU算法
也就是最近最少使用策略——这个策略的核心思想就是先淘汰最近最少使用的内容,在方法缓存中也用到了这种算法
# meta-Class 元类对象
每个类在内存中有且只有一个meta-class对象
// 将类对象当做参数传入,获得元类对象 Class objectMetaClass = object_getClass(objectClass5);
objectMetaClass是NSObject的meta-class对象(元类对象)
每个类在内存中有且只有一个meta-class对象
meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括
- isa指针
- superclass指针
- 类的类方法信息(class method)
# 为什么要设计metaclass?
先说结论: 为了更好的复用传递消息.metaclass只是需要实现复用消息传递为目的工具.而Objective-C所有的类默认都是同一个MetaClass(通过isa指针最终指向metaclass). 因为Objective-C的特性基本上是照搬的Smalltalk,Smalltalk中的MetaClass的设计是Smalltalk-80加入的.所以Objective-C也就有了metaclass的设计.

# isa指针
instance的isa指向class 当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用
class的isa指向meta-class 当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用
# superClass 指针
class对象的superclass指针
@interface Student: Person
@interfce Person: NSObject
当Student的instance对象要调用Person的对象方法时,会先通过isa找到Student的class, 然后通过superclass找到Person的class,最后找到对象方法的实现进行调用
meta-class对象的superclass指针
当Student的class要调用Person的类方法时,会先通过isa找到Student的meta-class, 然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用
提示
类对象存储实例方法列表等信息
元类对象存储类方法列表等信息
# 对像方法
实例对象(instance)要调用对象方法1.通过isa指针 -> 找到自己所属的类对象 -> 查找并调用方法
如果在自己的类对象没有找到方法,通过类对象的superclass指针 -> 找到父类的类对象 ->查找并调用方法
如果还没有找到,就通过superclass指针一直通过继承关系往上找,直到基类的类对象,如果还是没有找到,抛出异常
# 类方法
类对象通过isa指针 -> 找到自己的元类对象 -> 查找并调用方法
如果没有找到,通过元类对象的superclass指针 -> 找到父类的元类对象 -> 查找并且调用方法
如果还是没有找到,根据继承体系,通过元类对象的superclass指针一直找到基类的元类对象,查找并调用方法
如果基类的元类对象也找不到该类方法,会通过基类元类对象的superclass指针找到基类的类对象,查找有没有同名的对象方法,找到就调用,没有就抛出异常
isa的走向有以下几点说明:
实例对象(Instance of Subclass)的 isa 指向 类(class)
类对象(class) isa 指向 元类(Meta class)
元类(Meta class)的isa 指向 根元类(Root metal class)
根元类(Root metal class) 的isa 指向它自己本身,形成闭环,这里的根元类就是NSObject
# superclass走位
superclass(即继承关系)的走向也有以下几点说明:
类 之间 的继承关系:
类(subClass) 继承自 父类(superClass)
父类(superClass) 继承自 根类(RootClass),此时的根类是指NSObject
根类 继承自 nil,所以根类即NSObject可以理解为万物起源,即无中生有
元类也存在继承,元类之间的继承关系如下:
子类的元类(metal SubClass) 继承自 父类的元类(metal SuperClass)
父类的元类(metal SuperClass) 继承自 根元类(Root metal Class
根元类(Root metal Class) 继承于 根类(Root class),此时的根类是指NSObject
举例
NyanCat *cat = [[NyanCat alloc] init]; [cat nyan1];
向cat (instance) 发送消息nyan1时,运行时会通过isa指针查找到NyanCat(Class),这里保存着本类中定义的实例方法的指针。
[NyanCat nyan2];
向NyanCat(Class)发送消息nyan2时,运行时会通过isa查找到NyanCat(meta-class),这里保存着本类中定义的类方法的指针。
类的继承
在_class_t里面,第二个成员是superclass,很明显这个指针指向了它的父类。运行时可以通过isa和superclass获取一个类在继承树上的完整信息。
为了说明方便,这里把上面的例子稍微改一下:NyanCat : Cat : NSObject 这样一个继承树,画出图来就是这样子的
