-
知识准备
一些lldb指令
- p/x
以十六进制打印数据 - p/o
以八进制打印数据 - p/t
以二进制打印数据 - p/f
以浮点形式打印数据 - x
输出对象的内存地址,x/4gx中4代表输出4个,g代表每一个是8字节大小,x代表以16进制打印
位域
C语言允许在一个结构体中以位为单位来指定其成员所占内存,但指定的内存大小不能超过该成员类型所占的最大内存大小。
一个正常的结构体,它所占的内存空间由它的数据结构决定,如下列结构体,四个char类型占四个字节。
struct MyStruct {
char a;
char b;
char c;
char d;
}MyStruct; // 4字节
但我们可以为每个成员指定存储所用的比特位,假如我们为MyStruct所有的成员都指定占1个比特位,那这个结构体总共只占4个比特位,最终这个结构体只占1个字节的内存空间。
struct MyStruct {
char a : 1;
char b : 1;
char c : 1;
char d : 1;
}MyStruct; // 1字节
联合体
几种不同类型的变量存放到同一段内存单元中,几个变量互相覆盖。
union LGTeacher2 {
char *name;
int age;
int height;
}t2;
给联合体的成员赋值时,会将上一次所赋值的成员的值覆盖掉,并且所有的成员变量都是指向同一块内存空间。
内存大小的计算规则
- 联合体必须能够容纳最大的成员变量
- 通过1计算出来的大小必须是其最大成员变量(基本数据类型)的整数倍
union LGTeacher3 {
char a[7];
int b;
}t3;
联合体LGTeacher3中最大的成员变量是char a[7],占7个字节,但因其不是基本数据类型,而int是,所以LGTeacher3必须是int类型大小的整数倍,并且要够容纳a数组,所以该联合体占8个字节。
联合体和结构体的区别
结构体(struct)中所有变量是“共存”的,⽽联合体(union)中是各变量是“互斥”的,只能存在⼀个。struct内存空间的分配是粗放的,不管⽤不⽤,全部分配。这样带来的⼀个坏处就是对于内存的消耗要⼤⼀些。但是结构体⾥⾯的数据是完整的。联合体⾥⾯的数据只能存在⼀个,但优点是内存使⽤更为精细灵活,也节省了内存空间。
对象在内存中
分布
一个对象在内存中占用多少字节呢?我们以LGPerson为例来探索一下。
@interface LGPerson : NSObject
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,copy) NSString *hobby;
@property (nonatomic ,assign) int age;
@property (nonatomic ,assign) double hight;
@property (nonatomic ,assign) short number;
@end
int main(int argc, char * argv[]) {
LGPerson *p = [LGPerson new];
// 输出结果为48
NSLog(@"%lu",malloc_size((__bridge const void *)(p)));
}
在对象中,都有isa指针,指针占8字节,name和hobby都是字符串指针,所以也是8位,int占4位,double占8位,short占2位,char占1位,LGPerson的属性所占的字节数应该为8(isa)+8(name)+8(hobby)+4(age)+8(double)+2(short)+1(char)=39。但根据之前的探索,我们可知实际开辟内存空间要以16字节对齐,所以LGPerson占用48字节。 当我们执行main中的代码后,发现输出结果为48,与我们的分析相符合。
接下来我们将所有的属性赋值,然后在main中打个断点,通过终端输出来看看属性的值在内存中是怎么存储的。
LGPerson *p = [LGPerson new];
p.name = @"iOS";
p.hobby = @"code";
p.hight = 1.80;
p.age = 18;
p.number = 8;
通过终端的输出,我们发现,int和short类型被放到了一个8字节的内存空间里,NSString类型自己占了8个字节,double同样也占了8字节。并且int和short在书写的时候虽然被放在了NSString后面,但在内存中并没有按这个顺序存储,而是先存储了int和short,然后才存储了NSString类型。
那么有哪些因素会影响对象在内存中的占用呢?
影响因素
交换属性的书写顺序
@property (nonatomic ,assign) double hight;
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,copy) NSString *hobby;
@property (nonatomic ,assign) short number;
@property (nonatomic ,assign) int age;
再次查看内存,发现short和int类型,就算写在最后,最后也是放在了同一个8字节的内存空间中,并且这个内存空间就紧挨着isa指针。另外,虽然修改了double和NSString的顺序确实改变了它们在内存中间的位置,但由于它们都是8字节,没有优化的空间,所以苹果就按其编写的顺序存储到了内存中。
结论:对象在内存中是以8字节对齐的,当有多个属性可以存放在同一个8字节的内存中时,不管其书写的顺序,编译器都会尽可能帮我们进行优化。
添加属性
我们尝试在LGPerson中添加一个char类型的属性
@property (nonatomic ,assign) double hight;
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,assign) char a; // 添加char类型的属性
@property (nonatomic ,copy) NSString *hobby;
@property (nonatomic ,assign) short number;
@property (nonatomic ,assign) int age;
发现新添加char类型的数据,被放在了与int和short同一个8字节的内存中,其中int占了前4个字节,当只添加一个char类型的属性时,short和char各占了两个字节,当添加两个char类型时,short占了前2个字节,两个char类型属性各占1个字节。
结论:不管有多少个属性,编译器将尽可能多的属性放在8字节的内存空间中,从而达到节省内存空间的目的。
继承
@interface Father : NSObject
{
isa
int age1;
NSString *name;
NSString *job;
}
@end
@interface Son : Father
{
isa
int age2;
}
Son *s = [Son new];
NSLog(@"对象所占空间:%zd", class_getInstanceSize([Son class]));
NSLog(@"系统开辟的空间:%zd", malloc_size((__bridge const void *)s));
Father中有一个int类型的成员变量,两个NSString类型的成员变量,而Son中只有一个int类型的成员变量。理论上,Father尝试打印对象所占空间发现,编译器并没有为我们进行优化。
接下来我们将Father中的int类型的成员变量换个位置,放到最后。
@interface Father : NSObject
{
NSString *name;
NSString *job;
int age1;
}
@end
我们惊奇地发现,编译器为我们做了内存优化。
当存在继承关系时,父类是存储在一个连续的内存空间中,子类无法改变父类数据结构。但如果子类的头部数据和父类的尾部数据能够存在同一个8字节内存中,编译器就会把它们放在一起达到优化的目的。(但使用属性而非成员变量时,即使在这种情况下编译器也没有为我们进行优化,原因暂不得而知。)
nonPointerIsa
继续探索 _class_createInstanceFromZone
在对象的底层探索(上)中,我们探索到了objc源码中的_class_createInstanceFromZone函数,得知了其中调用instanceSize来计算对象所占用的内存空间,calloc来开辟对象所需的空间。接下来,我们继续查看_class_createInstanceFromZone函数中剩余的代码。
static ALWAYS_INLINE id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
//计算需要的大小
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
首先在if (slowpath(!obj))和if (fastpath(!hasCxxCtor))各下一个断点,在代码走到这两个地方时,分别打印obj。由控制台输出的信息发现,在前者处obj还只是一个id类型,但在后者处,obj已经变成了LGPerson的类型,说明此时开辟的内存空间已经与类完成了绑定,并且对象的isa指针,就是指向实例对象所属的类对象。
在这两个断点中间,无论if (!zone && fast)判断结果如何,最终都会执行到initIsa函数,那就从initIsa中一探究竟吧。
initIsa
inline void objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
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_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
// extra_rc就是对象的引用计数,而这个引用计数,正是存储在对象的isa指针中。
newisa.extra_rc = 1; // 引用计数
}
// 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 table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
initIsa中创建了一个initIsa类型的结构体对象,查看isa_t,发现它是一个联合体,并且其中有一个结构体,结构体的内容是宏定义ISA_BITFIELD,查看这个宏定义。
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
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)
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);
};
// ISA_BITFIELD根据架构的不同,其成员的值也不相同。
# 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
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# 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 : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t unused : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
我们发现这个结构体中有不少成员,而这些成员变量的作用,则由下图列出。
利用isa得到类对象
根据表中的内容,shiftcls是存储类对象的指针,那我们要怎么从isa中获取到类对象的指针地址呢?
通过移位
shiftcls存储的是类指针的值,那把isa指针中其他的值都去掉,就能获得类对象的指针。以x86_64架构为例,ISA_BITFIELD一共存储了64位的数据,首页我们将这段数据左移3位,再右移3位,那么前面3位nonpointer、has_assoc、has_cxx_dtor的值都变成了0,那么我们接着向右移动17位,同理右边的数据也都变成了0,最后在左移17位回到原位,那么ISA_BITFIELD中就只剩下了shiftcls的值。这个移动的过程,我们可以优化成先左移3位去除左边3位的数据,右移20位直接去除右边的数据,再左移17位回到原来的位置。
那么我们现在通过控制台打印来验证一下。先通过x/4gxp命令获取到对象的isa指针,然后将指针移动完毕后得到的shiftcls的值,接着我们查看LGPerson类对象的地址,发现二者一致,证明了shiftcls就是LGPerson类对象的地址,并且我们可以通过对象的isa指针获取到对象所属类对象的地址。
通过掩码
除了通过移位,苹果还为我们提供了一个掩码,可以通过这个掩码,快速地获取到类对象的地址。
我们可以直接将isa指针的地址&上define ISA_MASK 0x00007ffffffffff8ULL,就可以得出类对象的指针。&上掩码的操作,其实就相当于把前后的数据都去掉,只留下中间类对象地址的值。
setClass
接着查看initIsa函数,发现其最后有一行代码shiftcls = (uintptr_t)newCls >> 3;。这行将传入的Class对象的右移三位以后赋值给了shiftcls,这也验证了shiftcls就是存放类对象的地址(右移3位是因为前面3位存储其他的内容,写死右移3位是因为不管任何架构,shiftcls前面都是只有3位)。
inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
// Match the conditional in isa.h.
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE
// No signing, just use the raw pointer.
uintptr_t signedCls = (uintptr_t)newCls;
# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT
// We're only signing Swift classes. Non-Swift classes just use
// the raw pointer
uintptr_t signedCls = (uintptr_t)newCls;
if (newCls->isSwiftStable())
signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL
// We're signing everything
uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
# else
# error Unknown isa signing mode.
# endif
shiftcls_and_sig = signedCls >> 3;
#elif SUPPORT_INDEXED_ISA
// Indexed isa only uses this method to set a raw pointer class.
// Setting an indexed class is handled separately.
cls = newCls;
#else // Nonpointer isa, no ptrauth
shiftcls = (uintptr_t)newCls >> 3;
#endif
}
new方法
我们时常会看到,有些人的代码里创建对象使用alloc和init方法,而有些人则使用new方法。那二者又有什么区别呢?
我们先给LGPerson添加一个属性name,并且重写init方法来对该属性进行赋值,然后分别通过两种方式创建对象,并打印name的值,那两个name的值分别是什么呢?
@interface LGPerson : NSObject
@property (nonatomic ,copy) NSString *name;
@end
@implementation LGPerson
-(instancetype)init {
if (self = [super init]) {
self.name = @"LG";
}
return self;
}
@end
int main(int argc, char * argv[]) {
LGPerson *p1 = [[LGPerson alloc] init];
LGPerson *p2 = [LGPerson new];
NSLog(@"alloc init的name:%@\n new的name:%@",p1.name,p2.name);
// 输出的结果为:
// alloc init的name:LG
// new的name:LG
return 0;
}
通过运行代码我们发现,二者输出的name值均为LG。打开objc源码,在NSObject.m中找到new方法的实现。
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
通过源码我们发现,new方法调用callAlloc函数后调用了init方法,根据之前的内容,alloc方法调用的也是callAlloc函数,所以alloc和init方法和new方法是等价的。
我们也可以通过汇编来验证一下。首先在调用new方法的地方打个断点,同时打开汇编,发现调用了objc_opt_new。
我们继续在objc源码中查找这个函数,发现其同样调用callAlloc和init,这也进一步验证了我们的结论。
id
objc_opt_new(Class cls)
{
#if __OBJC2__
if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
return [callAlloc(cls, false/*checkNil*/) init];
}
#endif
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
}
对象的总结
alloc方法在底层的调用顺序
alloc核心方法
为什么要字节对齐
字节是内存的容量单位。但是,CPU在读取内存的时候,却不是以字节为单位来读取的,⽽是以“块”为单位读取的,所以⼤家也经常听到⼀块内存,“块”的⼤⼩也就是内存存取的⼒度。如果不对⻬的话,在我们频繁的存取内存的时候,CPU就需要花费⼤量的精⼒去分辨你要读取多少字节,这就会造成CPU的效率低下,如果想要CPU能够⾼效读取数据,那就需要找⼀个规范,这个规范就是字节对⻬。
为什么对象内部的成员变量是以8字节对⻬,系统实际分配的内存以16字节对⻬?
以空间换时间。苹果采取16字节对⻬,是因为OC的对象中,第⼀位叫isa指针,它是必然存在的,⽽且它就占了8位字节,就算对象中没有其他的属性了,也⼀定有⼀个isa,那对象就⾄少要占⽤8位字节。如果以8位字节对⻬的话,如果连续的两块内存都是没有属性的对象,那么它们的内存空间就会完全的挨在⼀起,是容易混乱的。以16字节为⼀块,这就保证了CPU在读取的时候,按照块读取就可以,效率更⾼,同时还不容易混乱。
影响对象内存的因素
对象⾥⾯存储了isa指针 + 成员变量的值,isa指针是固定的,占8个字节,所以影响对象内存的只有成员变量(属性会⾃动⽣成带下划线的成员变量)。
对象的内存分布
在对象的内部是以8字节进⾏对⻬的。苹果会⾃动重排成员变量的顺序,将占⽤不⾜ 8 字节的成员挨在⼀起,凑满8字节,以达到优化内存的⽬的。
nonPointerIsa
nonPointerIsa是内存优化的⼀种⼿段。isa是⼀个Class类型的结构体指针,占8个字节,主要是⽤来存内存地址的。但是8个字节意味着它就有8*8=64位。存储地址根本不需要这么多的内存空间。⽽且每个对象都有个isa指针,这样就浪费了内存。所以苹果就把和对象⼀些息息相关的东⻄,存在了这块内存空间⾥⾯。这种isa指针就叫nonPointerIsa。