对象的底层探索(下)
影响对象内存的因素
从原来的分析我们得出,对象的本质是isa
+ 成员变量的值,那什么因素会影响对象的内存呢?我们先从一段代码开始:
@interface LLPerson : NSObject
// isa --- 8
@property (nonatomic, strong) NSString *name; // 8
@property (nonatomic, strong) NSString *hobby; // 8
@property (nonatomic, assign) int age; // 4
@property (nonatomic, assign) double height; // 8
@property (nonatomic, assign) short number; // 2
@end
LLPerson *p = [LLPerson alloc];
p.name = @"LL";
p.hobby = @"boy";
p.height = 1.85;
p.age = 24;
p.number = 20;
NSLog(@"----------%lu", malloc_size((__bridge const void *)(p)));
在LLPerson
类中,isa
指针8
个字节,对象的内存大小打印可得48
(16
字节对齐)。当我们为LLPerson
类添加实例方法和类方法时:
@implementation LLPerson
- (void)test {
}
+ (void)test {
}
@end
打印还是48
字节,所以影响对象内存大小的因素是成员变量。那么,在成员变量的值在内存中是怎么排布的呢?是我们编写属性的顺序还是我们赋值的顺序呢?我们可以通过终端命令打印获取内存信息:
我们通过打印可以看出,内存的排布不是根据我们编写属性的顺序,也不是是我们赋值的顺序,而是系统会根据成员变量各个的大小,进行优化,比如将4
个字节的age
和2
个字节的number
放到一起。
对象是怎么关联类型的?
那么,对象是怎么关联上类型的?
我们在原来的的结论中知道,_class_createInstanceFromZone
中返回了obj
,所以我们可以在_class_createInstanceFromZone
方法中,查看是怎么关联类型的。
我们通过打断点,po obj
打印,可以得到对象的类型关联是在obj->initIsa(cls)
中的,那我们可以看一下initIsa
这个方法中是怎么做的。
// objc-object.h
#if !SUPPORT_INDEXED_ISA && !ISA_HAS_CXX_DTOR_BIT
#define UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT __attribute__((unused))
#else
#define UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT
#endif
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer()); // 是否优化
isa_t newisa(0); // 初始化 isa_t
if (!nonpointer) { // 没有优化过的isa 直接赋值class 原来的时候
newisa.setClass(cls, this);
} else { // 优化过的 现在基本上都是优化过的
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA // watch独有
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE isa.magic是ISA_MAGIC_VALUE的一部分
// isa.nonpointer is part of ISA_MAGIC_VALUE isa.nonpointer是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.magic是ISA_MAGIC_VALUE的一部分
// isa.nonpointer is part of ISA_MAGIC_VALUE isa.nonpointer是ISA_MAGIC_VALUE的一部分
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1; // 引用计数器 alloc时直接引用计数器 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 //固定使用原子在这里保证单存储和保证内存顺序W.R.T.类索引表
// ...but not too atomic because we don't want to hurt instantiation //但不要太原子化,因为我们不想破坏实例化
isa = newisa; // 赋值isa
}
从方法中,我们可以看到isa
指针就是isa_t
这个结构的,在方法中进行isa_t
的赋值,同时我们可以看到alloc
对象后,引用计数器为1
。
接下来我们研究isa_t
这个结构:
// objc-private.h
#include "isa.h"
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. //访问类需要自定义的ptrauth操作,因此通过将此设置为私有来强制客户端通过setClass/getClass。
Class cls;
public:
#if defined(ISA_BITFIELD) // isa的结构
struct {
ISA_BITFIELD; // defined in isa.h
};
bool isDeallocating() { //判断是都被在销毁时,判断的是引用计数器和sidetable都为0
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() { // 对象销毁时,引用计数器和sidetable都会置为0
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
从这个结构中我们可以看到,isa_t
是union
联合体来定义的,兼容是否是NonpointerIsa
,当然,我们只研究NonpointerIsa
,即ISA_BITFIELD
这个结构。
// isa.h
# if __arm64__ // arm64架构下
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e. // ARM64模拟器有更大的地址空间,所以即使模拟器是为ARM64-not-e构建的,也要使用ARM64e方案
// __has_feature(ptrauth_calls) 是判断编译器是否支持指针身份验证功能
// ptrauth_calls 指针身份验证,针对arm64e架构;使用Apple A12或更高版本A系列处理器的设备(如iPhone XS、iPhone XS Max和iPhone XR或更新的设备)支持arm64e架构
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR //arm64e 或者 模拟器
# define ISA_MASK 0x007ffffffffffff8ULL // 获取isa中指向class的指针掩码
# 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 // 获取isa中指向class的指针掩码
# 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__ // x86架构
# define ISA_MASK 0x00007ffffffffff8ULL // 获取isa中指向class的指针掩码
# 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)
# else
# error unknown architecture for packed isa
# endif
从结构中我们可以看出,ISA_BITFIELD
是使用位域的形式来保存信息的,isa
中8
字节,8 * 8 = 64
位来存储信息,并且是分平台架构的,下面我们值分析真机(iphone11)
下的结构即:
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; // 表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等 \
uintptr_t has_assoc : 1; // 关联对象标志位,0没有,1存在 \
uintptr_t weakly_referenced : 1; // 标志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。 \
uintptr_t shiftcls_and_sig : 52; // 存储类指针的值。开启指针优化的情况下,在 arm64e 架构中有 52 位用来存储类指针。 \
uintptr_t has_sidetable_rc : 1; // 是否需要使用 sidetable 来存储引用计数 \
uintptr_t extra_rc : 8 // 表示该对象的引用计数值
我们通过内存平移获得的地址和LLPerson
的地址是一致的,当然我们咳哟使用define ISA_MASK 0x007ffffffffffff8ULL
掩码来获取这个值,跟内存平移是一致的。
我们可以看到对象是通过isa
来关联类型的,同时在isa
中我们也可以看到对象的一些其他信息,比如引用计数器。
关于new方法
最后,我们看一下new
方法,可以从源码中搜索:
// NSObject.mm
// callAlloc 实际上是 alloc + init
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
其实new
方法就是alloc + init
。