前言
上篇文章说了iOS中alloc方法是怎么创建对象的,以及对象的本质是结构体。接下来继续探究对象的内存分布,以及对象的isa是个什么样的结构体,存储了哪些信息。
对象内存分布
已知系统给对象分配内存是16字节对齐的;
@interface FFGoods : NSObject
@property (nonatomic ,copy) NSString *goodsName; // 0-8
@property (nonatomic ,copy) NSString *type; // 9-15
@property (nonatomic ,assign) int goodsId; // 16-19
@property (nonatomic ,assign) double weight; // 24-31
@property (nonatomic ,assign) short number; // 32-33
// 33经过16字节对齐后 -> 48
@end
那么以上实例应该是48字节:
如果加一个类方法和实例方法,这个对象占用的内存大小会不会发生变化呢?
说明没有影响。对于实例对象,内存分配是由isa + 成员变量的值,这是之前的结论。
既然成员变量的值存储在对象里面,会不会受到属性的书写顺序影响?
打印goods
实例的内存地址前,先解释一下将会出现的lldb
指令:
p
能够知道引用类型,po
只能打印值;
p/x
表示按照16字节打印,还有p/o
是8字节 , p/t 是2字节, p/f
是浮点数
x/6gx
: 第一个x代表打印对象的内存地址,/
后面是参数;4g
代表连续4个8字节的地址,最后个x
代表16进制;
goodsName
为什么没有紧跟在isa后面?
isa
指针后面的8字节:0x0000007b00000038
,尝试分为 0x0000007b
和 0x00000038
2段打印:
发现4字节的int
和2字节的short
会放在一个8字节的内存空间里。因为iOS会自动重排属性的顺序,达到内存优化。
再增加一个char属性(int + short + char < 8字节),
@property (nonatomic ,assign) char abc;
发现还是排在同个8字节空间里:因为对象内部是8字节对齐的。
再看一个例子:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FFTestObject : NSObject
{
@public
int count;
NSObject *subObject1;
NSObject *subObject2;
}
@end
@interface FFTestSubObject : FFTestObject
{
@public
int subCount;
}
@end
NS_ASSUME_NONNULL_END
打印:
然后我调整父类 int 属性的顺序,再运行看到 subObject
实际占用变成32。这说明成员变量的顺序还是会对内存造成微小影响的。
父类在内存中是一块连续的内存空间,子类无法修改和插入。也就是iOS对子类的成员变量,不会跟其父类混合重排在一起。图中int放在尾部时,子类可以跟在父类后面,填补空白的内存。
平时开发时不用特别在意,相差几个字节对内存影响太小。
至于iOS的自动重排,还得从位域说起。
位域
C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称**“位域”( bit field)** 。
定义2个结构体:
C可以在定义结构体的同时定义结构体变量,不过OC在.h文件中定义需要使用typedef修饰。
struct Student{
// …
} student1,student2;
struct FFStructA {
char a;
char b;
char c;
char d;
}structA;
struct FFStructB {
char a : 1;
char b : 1;
char c : 1;
char d : 1;
}structB;
打印内存大小,发现structB
为什么只占1个字节?
这就是位域的作用,a:1
表明只需要1个位域(二进制的1个比特位),位域的特点是不能超过数据类型的最大长度。
当前字节剩余位域不够用的时候,会直接从下个字节开始存放。修改位域,再打印发现内存占用变成2字节:
不过位域节省的空间实在有限,只有系统级别才会用到。对于开发者不需要节约,而是不要去浪费。
联合体
新建一个结构体和联合体:
struct FFGoods {
char *name;
int number;
double height;
}goodsStruct;
union UnionStudent {
char *name;
int number;
double height;
}goodsUnion;
在没赋值前,打印出联合体与结构体一样都是初始值;赋值一个属性后就不一样了,其他属性会是个乱七八糟的值;
union就是属性共用一块内存空间,系统会分配一块足够大的内存空间;成员变量就相当于开辟了几种去访问这一整块内存的途径。
上图为什么会出现乱七八糟的值?一开始的时候赋值字符,那么其他成员的数据类型指针去读取字符串的时候自然是错误的。
联合体的作用也是能节省一定的内存空间,所占内存取决于最大成员变量,例子中就是double
类型占8字节。
同时还得满足最大基本数据类型的整数倍:
union UnionStudent {
char *name;
int number;
double height; // 8字节
char a[9]; // 1 * 9 = 9 字节;但是要满足最大基本数据类型(这里是double)的整数倍
}goodsUnion;
这跟结构体内存对齐道理一样:
与结构体的区别:
结构体是共存,内存开辟比较粗放,只要写了内存变量就会去开辟内存空间;
联合体是互斥的,省内存空间;
对象的isa
那么联合体在objc底层是怎么使用的?
来到objc源码的_class_createInstanceFromZone
方法,上回只说到calloc,接着看后面的方法。代码中实例化的时候加一个断点,看看会发生什么。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello World!");
FFPhone *phone = [FFPhone alloc]; // 这行加断点
logPhone(phone);
}
return 0;
}
目前为止,还只是id类型,跟自己创建的类还没有什么关系。通过calloc开辟的内存空间如何与类产生关联?
运行到下一个断点,po
一下,看到已经是我们的类了。实际上就是通过前面的initisa
函数进行关联的。
注意initInstanceIsa
这个分支,代码里最终还会调用initisa
:
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
来到 initIsa
看看具体做什么:
Tips:简单提一下,第328行
isTaggedPointer
是指针优化第355行,
extra_rc
代表引用计数的值;通过alloc方法创建出来的对象,引用计数就是1,并且存在于isa指针里。
可以看到isa_t
查看发现,破案了!iOS的isa指针结构居然是联合体。
这里对代码加一些注释:有2个构造器和3个数据成员。通过94行得出,当有几东西只能同时存在一个的时候,确实适合用联合体;
#include "isa.h"
union isa_t {
// 2个构造器
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
// 数据成员1,64位内存占用
uintptr_t bits;
// 数据成员2,Class类型指针
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
// 数据成员3
struct {
ISA_BITFIELD; // defined in isa.h
};
// 是否被释放了?引用计数+散列表里的计数
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
// 释放对象
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
既然是联合体,那么3个数据成员只会同时存在一个。看到这个判断条件:
#if defined(ISA_BITFIELD)
如果没定义ISA_BITFIELD
,那么isa_t
就是Class类型的结构体指针;既然给了64位内存,只存放Class指针肯定是不划算的。那么iOS根据情况切换成其他数据成员。
如果有定义ISA_BITFIELD
,就变成另一种结构体,ISA_BITFIELD
本身还代表这些结构体成员有哪些:
看到 elif __x86_64__
,原来这个数据成员还区分架构的。我的是intel芯片的mac系统,架构是__x86_64__
;
以下是arm64
模拟器和真机:(__has_feature
: 是否支持指针的身份验证;A12芯片后引出的;)
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
nonPointerIsa
注意到isa结构体成员中的uintptr_t nonpointer : 1
,当nonpointer=1的时候,代表开启指针优化,这时候的isa指针也叫做nonPointerIsa
指针。关于指针的内容今后会单独讲一篇,这里稍微提一下。
单独整理一下不同架构的isa结构和里面指针的含义。
arm64 (模拟器):
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t weakly_referenced : 1;
uintptr_t shiftcls_and_sig : 52;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8
arm64 (真机):
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t unused : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19
x86_64:
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t unused : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8
指针含义:
指针 | 含义 |
---|---|
nonpointer | 是否对 isa 指针开启指针优化。0:纯isa指针,1:不止是类对象地址,isa还包含了类信息、对象的引用计数等 |
has_assoc | 是否有关联对象 |
has_cxx_dtor | 该对象是否有 C++ 或者 Objc 的析构器。如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象 |
shiftcls | 存储类指针的值。开启指针优化的情况下,来存储类指针,在不同架构中位数不同。 |
magic | 用于调试器判断当前对象是真的对象,还是没有初始化的内存空间 |
weakly_referenced | 对象是否被指向或者曾经指向一个 ARC 的弱变量, 没有弱引用的对象可以更快释放 |
deallocating | 标志对象是否正在释放内存 |
has_sidetable_rc | 是否有使用 sidetable 散列表来存储引用计数,通常是引用计数extra_rc 存满了 |
extra_rc | 表示该对象的引用计数值 |
回到刚才的demo;打印对象内存地址后怎么通过地址来获取内部这些指针呢?
获取isa里的指针
例如__x86_64__
架构ISA_BITFIELD
的这个uintptr_t shiftcls : 44;
指针,要单独取出这中间的44位,就意味着将左右位数据都变成0。通过位运算,左移右移能达到效果。这里就是右移3位到最右边,左移20(右3+左17)位回到最左边,再右移17位移回原来的位置。
为什么前三个在右边?内存地址右边是低位,属性又是从低位开始存。
lldb通过 p/x address >> 3 << 20 >> 17;
得到后的地址就是该实例对象的类对象地址空间。验证如下:
接下来尝试获取extra_rc,因为在高8位,直接右移 64-8 = 56
位就行了;前面会补0 ;并且为了测试再加一个指针引用:
结论:引用计数的上限取决于cpu架构。并且次数上限 c o u n t = 2 n − 1 count = 2^n - 1 count=2n−1,n为位数。
我就要问了,要是存储就是超过了怎么办?借位,存到散列表里;并用
has_sidetable_rc
标识;
以上位移操作获取类对象地址不用这么麻烦,系统有 ISA_MASK
掩码帮你完成了;
这与之前介绍的对象内部8字节对齐时,WORD_MASK
是一样的:
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
这是ISA_MASK
的定义:define ISA_MASK 0x00007ffffffffff8ULL
,直接进行与运算就能得到类对象地址。
用计算器查看这个掩码的位:本质是就是你需要保留的位都置1,其余0。与运算后0位都是0。
结尾的ULL = unsigned long long
总结
影响对象内存的因素
isa通过位域的形式,将对象相关的信息存储起来在8字节里面。所以影响对象内存的只有成员变量(属性会自动生成带下划线的成员变量)。
对象的内存分布
在对象的内部是以8字节进行对⻬的。苹果会自动重成员变量的顺序,将占用不足 8 字节的成员挨在一起,凑满 8 字节,以达到优化内存的目的。
联合体(union)
联合体的成员变量就相当于为这块内存空间开辟了几个访问途径,他们共享这一块内存。
联合体的大小规则:
- 必须能容纳联合体中最大的成员变量
- 必须是数据成员中最大的基本数据类型大小的整数倍
联合体和结构体的区别 :
- 结构体中所有变量是“共存”的,而联合体(union)中是各变量是“互斥”的。
- 一个完整,一个灵活。
位域
- 位域的宽度不能超过声明的数据类型的最大⻓度。比如int占4个字节也就是32位,那
:
号后面的数字就不
能超过32。 - 如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放
该位域。 - 位域能够节省一定的内存空间。
nonPointerIsa
-
nonPointerIsa
是内存优化的一种手段。isa
是一个Class
类型的结构体指针,占8个字节也就是64位。然而存储地址根本不需要这么大的内存空间。而且每个对象都有个isa指针,这样就浪费了内存。所以iOS就把和对象相关的信息,存在了这块内存空间里面。这种isa指针就叫nonPointerIsa。 -
通过isa的位运算得到类对象。借助系统提供的掩码
ISA_MASK
就能轻松得到,使用时注意区分架构。同时还可以通过平移得到其他对象信息。
对象的结构体关系图