背景
- 我们说在整个苹果的底层有太多内容,跟黑洞似的,这时候我们拿出对象最熟悉的陌生人作为一个案例开始分析
iOS底层OC对象原理(上),但我们也不知道怎么分析,于是我们通过LLDB,汇编,符号断点,找到底层源码并LLDB调试,通过底层源码我们开始流程分析,得到alloc的流程分析图,后来我们通过探索alloc我们把注意力转移到了内存大小上,然后去验证对象的内存得出一些字节对齐的算法和一些原理,然而我们对对象真正的大小还一无所知。 - 对象类型内存多大?
- 对象实际内存大小和系统分配的内存大小是否一样?
- 内存是怎么对齐的?
- 对象的本质是什么?
isa是怎么和类关联的?
准备
- objc4源码
- malloc源码
内存对齐-alignStyle
案例
//ZMPerson声明了4个属性
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *nickName;
@property (nonatomic) int age;
@property (nonatomic) double height;
ZMPerson *p1 = [[ZMPerson alloc] init];
ZMPerson *newP;
NSLog(@"p1:%@ - %lu - %lu - %lu", p1, sizeof(p1), class_getInstanceSize([p1 class]), malloc_size((__bridge const void*)(p1)));
NSLog(@"newP:%@ - %lu - %lu - %lu", newP, sizeof(newP), class_getInstanceSize([newP class]), malloc_size((__bridge const void*)(newP)));
输出结果:
p1:<ZMPerson: 0x6000012e0f00> - 8 - 40 - 48
newP:(null) - 8 - 0 - 0
结果分析:
对象类型的内存大小(sizeof操作符,计算传进来的数据类型大小)p1和newP一样的都是8字节,这在编译期就已经确定了,因为它们本质上是结构体指针。- newP的内存地址为null,实际内存大小和系统分配的内存大小都是0,因为newP只是声明一个变量,并没有开辟内存
- p1开辟了内存,但
实际内存大小(class_getInstanceSize由类的成员变量大小决定)和系统分配大小(malloc_size按16字节对齐方式)却不一样
引申扩展:
class_getInstanceSize通过源码查找核心代码为:
define WORD_MASK 7UL//字节面具
//字节对齐算法
static inline size_t word_align(size_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
看核心代码实际内存是计算实际大小后并进行8字节对齐 案例中ZMPerson类定义了4个属性,再加上通过看核心代码4*8(4个属性变量)+8(对象类型)=40
malloc_size获取系统分配的内存大小,上篇文章iOS底层-OC对象原理(上)我们知道了在instanceSize计算好需要的内存后向系统calloc申请内存,首先在malloc库下载libSystem_malloc编译运行源码,通过探索malloc源码我们得到这样的逻辑图:
在上篇文章iOS底层-OC对象原理(上)我们以9为例子探索了16进制对齐算法(&~MASK算法)
这里我们探索一下位移算法
为什么需要内存对齐:
空间换取时间
CPU在读写数据时,是以块为单位,不是以字节为单位。频繁读写未对齐的数据,给CPU极大压力,降低CPU性能。字节对齐后,减少CPU的读写次数,降低开销。
提升安全、效率
CPU以块为单位的读写方式,操作未对齐的数据可能开始在上一个内存块,结束在另一个内存块。这样中间可能要经过复杂运算在合并在一起,降低了CPU效率还不安全。字节对齐后,提过CPU访问速率。
内存对齐三大原则:
1. 数据成员对⻬,以元素的整数倍为开始
结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如char为1字节,min(当前开始位置为9)则要从9开始存储并存储在9。这时候后面又有个int为4字节,min(这时当前开始的位置为10)但这个int不能从10开始,而是从12(即4的整数倍)开始[12 13 14 15]
2. 结构体作为成员,以最大元素为倍数
如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
3. 以最大成员整数倍收尾
结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补⻬
常用类型内存所占字节表
内存对齐分析
结构体struct
- 结构体(struct)中所有变量是“共存”的
- 优点是“
有容乃大”,全面; - 缺点是struct内存空间的分配是
粗放的,不管用不用,全分配
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"------%lu--%lu--%lu",sizeof(ZMS1),sizeof(ZMS2),sizeof(ZMS3));
}
return 0;
}
打印结果:
------16--24--40
图例:
内存优化
案例
//ZMPerson定义4个属性
@property (nonatomic) NSString *name
@property (nonatomic) NSString *nickName
@property (nonatomic) int age;
@property (nonatomic) int height;
//实现
ZMPerson *p1 = [[ZMPerson alloc] init];
p1.name = @"moon";
p1.nickName = @"jdt";
p1.age = 19;
p1.height = 179;
NSLog(@"p1:%@ - %lu", p1, class_getInstanceSize(p1.class));
结果
内存优化分析
理论上定义4个属性,在计算内存时应该是 4 * 8 + 8 = 40字节,为什么实际打印却只有32字节,说明内存对齐又制定了一套规则,目的是提高cpu的存取效率和安全的访问。字节对齐可能浪费了部分内存,但是同时进行内存优化尽可能的降低了内存的浪费,即保证了存取的速率,又减少了内存的浪费
对象的本质
联合体-union
- 各变量是“
互斥”的; - 修改联合体中某个变量会
覆盖其他变量的值 - 缺点是
不够“包容”; - 优点是内存使用更为
精细灵活,也节省了内存空间
示例代码
//定义一个联合体
union ZMPerson {
int a; //4
short b;//2
char c; //1
};
//实现
int main(int argc, const char * argv[]) {
@autoreleasepool {
union ZMPerson p;
p.a = 18;
NSLog(@"a:%d - b:%d - c:%c", p.a, p.b, p.c);
p.c = 'f';
NSLog(@"a:%d - b:%d - c:%c", p.a, p.b, p.c);
p.b = 3;
NSLog(@"a:%d - b:%d - c:%c", p.a, p.b, p.c);
NSLog(@"%lu---%lu",sizeof(p),sizeof(union ZMPerson));
}
return 0;
}
结果分析
a:18 - b:18 - c:
a:102 - b:102 - c:f
a:3 - b:3 - c:
4---4
- 修改c的值,覆盖了a和b
- 修改b的值,覆盖了a和c
- 联合体的内存大小由其中
最大的成员的大小决定 - 所有变量
公用一块内存,变量之间互斥
位域-Bit field
- 优化内存,减少内存空间浪费
示例代码
//普通结构体
struct ZMStruct {
BOOL top;
BOOL bottom;
BOOL left;
BOOL right;
};
//结构体位域
struct ZMBitfStruct {
BOOL top : 1;
BOOL bottom : 1;
BOOL left : 1;
BOOL right : 1
};
//实现
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct ZMStruct zms;
struct ZMBitfStruct zmbitfs;
NSLog(@"-- %lu - %lu", sizeof(zms), sizeof(zmbitfs));
}
return 0;
}
结果分析
从上面我们的常用类型内存所占字节表知道bool类型占一个字节
结构体
ZMStruct里分配4字节内存,但ZMBitfStruct发现内存只占1个字节,1字节=8bit(位),ZMBitfStruct在1字节里按位存储,因为macOS为小端模式,bit里从右往左依次为 0000 1111:top、bottom、left、right
clang(main.cpp)
clang知识集
-
Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。 -
Clang是一个
由Apple主导编写,基于LLVM的C/C++/Objective-C编译器 -
2013年4月,Clang已经全面支持C++11标准,并开始实现C++1y特性(也就是C++14,这是 C++的下一个小更新版本)。Clang将支持其普通lambda表达式、返回类型的简化处理以及更 好的处理constexpr关键字。
-
Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/ Objective-C++编译器。它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过__attribute__((overloadable))来修饰函数),其目标(之一)就是超越GCC。
-
在这里我们探索的主题:用
Clang把源文件编译成底层文件,比如把main.m文件编译成main.cpp(也可编译成main.o或者可执行文件)。便于观察底层的逻辑结构,便于我们探究底层。 终端找到工程main函数位置并执行
clang -rewrite-objc main.m -o main.cpp
案例
@interface ZMPersons : NSObject
{
NSString *height;
}
@property (nonatomic) NSString *name;
//@property (nonatomic) NSString *nickName;
@property (nonatomic) int age;
//@property (nonatomic) int height;
@end
@implementation ZMPersons
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here
// appDelegateClassName = NSStringFromClass([AppDelegate class])
}
return 0;
// return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
在main.cpp文件的相关代码:
分析
- 从main.cpp文件里我们看到ZMPersons底层就是个
结构体 - 我们自定义了height、_age、_name,再加上NSObject_IVARS(
就是isa),总4个变量 - NSObject_IMPL里的
Class是个指针类型 - 代码中我们自定义了一个局部变量
height,但是底层代码仅添加了一个变量。而定义的属性,底层自动添加了带_变量以及get和set方法的实现。
总结
对象本质就是结构体对象的isa是继承NSObject的isaNSObjct只有一个成员变量就是isa
isa探索
- 通过iOS底层-OC对象原理(上)我们引入了
isa是为了与类关联 - 在
calloc系统分配内存时多了8字节的内存也是isa指针 - 而在上面
对象的本质时,我们看到对象底层唯一的成员变量也是isa
源码案例
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls)
} else
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa())
isa_t newisa(0);
#if SUPPORT_INDEXED_IS
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE
// isa.magic is part of ISA_MAGIC_VALU
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index tabl
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}
- 代码中
newisa(0)对bits进行赋值,bits里面所有的变量都是0 - 进到
isa_t:
源码分析
isa_t是联合体,并且有两个变量 一个是bits,一个是cls。- 通过上面分析
联合体是互斥的,那就意味着初始化isa有两种方式 bits被赋值,cls没有值或者被覆盖cls被赋值,bits没有值或者被覆盖isa_t中还有一个宏定义的结构体成员变量ISA_BITFIELD。
ISA_BITFIELDisa的位域信息
各变量的含义:
nonpointer:表示是否对isa指针进行优化,0表示纯指针,1表示不止是类对象的地址,isa中包含了类信息、对象、引用计数等has_assoc:关联对象标志位,0表示未关联,1表示关联has_cxx_dtor:该对象是否C ++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,没有,则释放对象shiftcls:储存类指针的值,开启指针优化的情况下,在arm64架构中有33位用来存储类指针,x86_64架构中占44位magic:用于调试器判断当前对象是真的对象还是没有初始化的空间weakly_referenced:指对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放deallocating:标志对象是否正在释放has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位hextra_rc和extra_rc:表示该对象的引用计数值,实际上引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc为9,如果大于10,就需要用到上面的has_sidetable_rc
isa位域我们举例macOS端,如下图:
- 由于macOS的小端模式,在读取时要从后往前读
- ULL表示类型,NULL
isa与类关联(obj->initInstanceIsa)
- 通过iOS底层-OC对象原理(上)的
alloc流程alloc-->...-->_class_createInstanceFromZone,断点到obj->initInstanceIsa,进入obj->initInstanceIsa->initIsa
图中显示
newisa.bits = ISA_MAGIC_VALUE 是一个宏 ISA_MAGIC_VALUE = 0x001d800000000001,变量值改变的有bits = 8303511812964353 ,cls = 0x001d800000000001,nonpointer = 1,margic = 59
cls=0x001d800000000001是因为给bits赋值的时候覆盖了cls,isa_t是联合体- 第
0位的1等于nonpointer=1 margic的值按位计算从47位开始,长度是6位结果就是111011,从第0个位置开始算111011的10进制就是59
继续走newisa.setClass到setClass里面
做了一次右移>>3
shiftcls=536875162与上面的算法shiftcls=(uintptr_t)newCls >> 3得到的结果是一样的。LGPerson的类地址>>3进行10进制转换赋值给shiftcls。此时isa已经关联LGPerson类 ,cls变量被覆盖,cls=LGPerson。
(uintptr_t)newCls >> 3 为什么需要右移3位
MACH_VM_MAX_ADDRESS表示虚拟内存最大寻址空间,在arm64中MACH_VM_MAX_ADDRESS =0x1000000000虚拟内存最大寻址空间是36位。在x86_64中MACH_VM_MAX_ADDRESS=0x7fffffe00000虚拟内存最大寻址空间是47位。字节对齐是8字节对齐,也就是说指针的地址只能是8的倍数,那么指针地址的后3位只能是0,比如0x8,0x18,0x30转换成二进制后3位都是0`。
为了节省内存空间,把后3位是0抹去。在__arm64__中shiftcls占33位,__x86_64__中shiftcls占44位。因此需要将类地址右移3位,即(uintptr_t)newCls >> 3。可以说isa做到了极致内存优化。
总结
-
isa是 (union + bitfield)的方式存储信息,这样可节省大量内存空间。 -
一切皆对象,对象就有isa,isa占用很大内存,union联合体的互斥特性可使公用一块儿内存节省内存,而bitfield位域更是在节省的基础上,让isa指针的内存得到充分使用 -
isa与cls关联,实质是isa指针中的shiftcls储了类信息,访问的时候经过偏移量访问 -
让内卷来得再猛烈一些吧
进阶
isa核心
shiftcls=(uintptr_t)newCls >> 3isa&ISA_MASKisa位算法(字节对齐算法)
使用gdb查看内存表
格式: x /nfu <addr>
说明:
- x 是 examine 的缩写
- n 表示要显示的内存单元的个数
- f 表示显示方式
- u 表示一个地址单元的长度 |显示方式(f)|注释| | --- | --- | |x |按十六进制格式显示变量。| |d |按十进制格式显示变量。| |u |按十进制格式显示无符号整型。| |o |按八进制格式显示变量。| |t |按二进制格式显示变量。| |a |按十六进制格式显示变量。| |i |指令地址格式| |c |按字符格式显示变量。| |f |按浮点数格式显示变量。|
| 地址单元长度(u) | 注释 |
|---|---|
| b | 表示单字节 |
| h | 表示双字节 |
| w | 表示四字节 |
| g | 表示八字节 |