第2课-OC对象原理上-2
1.4.2.4 向系统开辟内存,返回地址指针
1.4.2.4.1 查看malloc源码
系统底层是怎么分配内存的呢,我们先看以下例子:
有一个ZBPerson对象如下
@interface ZBPerson : NSObject
@property (nonatomic, copy) NSString *name; ///< 名字
@property (nonatomic, copy) NSString *nickName; // 昵称
@property (nonatomic, assign) int age; ///< 年龄
@property (nonatomic, assign) double height; ///< 身高
@end
main函数调用:
看一下打印结果
- sizeof(person)返回8,是因为person是一个指针,指针占8字节,所以返回8
我们分析一下ZBPerson对象
- name属性,字符串指针类型,占用8字节
- nickName属性,字符串指针类型,占用8字节
- age属性,int类型,占用4字节
- height属性,double类型,占用8字节
分析到这里,一共28字节,内存对齐后应该是32字节,所以class_getInstanceSize([ZBPerson class]
应该返回32啊,为什么是40呢?
注意我们在计算的时候前往不要忘记了隐藏成员变量isa,结构体指针类型,占用8字节,所以 ZBPerson对象实际占用36字节,内存对齐之后40字节,所以打印40
那么为什么 malloc_size((__bridge const void *)(person))
函数返回48呢?
接下来我们分析一下
我们直接点击malloc_size函数,发现只能看到函数声明,无法看到具体的源码实现
接下来我们把源码下载下来编译
下载成功之后,就是编译解决各类报错问题,具体可以参照https://www.jianshu.com/p/cb1b573a0297
1.4.2.4.2 堆中为对象开辟内存通过16字节对齐算法计算对象的大小。
编译成功之后,我们在main函数中执行
void *p = calloc(1, 40);
NSLog(@"%lu",malloc_size(p));
代码calloc(1, 40)
表示在内存空间分配40个字节的内存,与上面例子person对象的内存分配是一样的。 最后的结果确是打印48
我们分析一下:
- callloc 函数原型
void *calloc(size_t num_items, size_t size)
,在内存的动态存储区中分配num个长度为size的连续空间;num:对象个数,size:对象占据的内存字节数,相较于malloc函数,calloc函数会自动将内存初始化为0; - malloc_size(p)用于计算分配内存的大小
接下来我们跟踪calloc的执行过程 calloc-->_malloc_zone_calloc-->default_zone_calloc-->nano_calloc-->_nano_malloc_check_clear
注意当执行到zone_calloc时,无法再往下一层点击,此时我们可以通过lldb打印一下,然后确认一下接下来执行的函数 执行到zone->clloc函数时,再次通过lldb打印一下,然后确认一下接下来执行的函数是nano_calloc
这里的_nano_malloc_check_clear
就是关键代码 接下来我们就找到了最关键的函数segregated_size_to_fit
注意:
- #define SHIFT_NANO_QUANTUM 4
- #define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
也就是上面算法等价于
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = 16;
}
k = (size + 16 - 1) >> 4;
slot_bytes = k << 4;
*pKey = k - 1;
return slot_bytes;
}
而我们之前讲过 (x + WORD_MASK) & ~WORD_MASK
=== (x + WORD_MASK) >> A << A
其中 2^A - 1 = WORD_MASK 字节对齐算法,很明显这里使用了16字节对齐算法。
所以我们开辟一块40字节的内存,通过16字节对齐之后就变成了48字节。
通过上面的分析我们得出以下结论:
- 在对象的内部,成员变量之间通过8字节对齐算法计算实例变量的大小。
- 而在堆中为对象开辟内存的时候,通过16字节对齐算法计算对象的大小。
这里有一个问题为什么对象以16字节对齐开辟内存而不是8字节呢?
原因1为对象留有冗余空间,更加的安全 我们假设有2个对象A,B,以8字节对齐开辟内存,而对象内部的成员变量也是以8字节对齐的,所以对象内部的内存是与成员变量分配的内存重合的,没有冗余空间。 A对象:内存 [0x0001, 0x0007] B对象:内存 [0x0008, 0x00015] 那么当A对象越界访问的时候是不是访问到B对象的数据,所以为了安全,适当的给对象加一些冗余空间就能解决这个问题。 如果我们以8字节对齐分配对象内存 对象可能分配的内存 对象成员变量可能的内存
8 8 16 16 24 24 32 32 48 48
如果我们以16字节对齐分配对象内存 对象可能分配的内存 对象成员变量可能的内存
16 8 32 16 48 24 64 32 16 48
也就只有16 32 48发生了重合,重合的概率大大降低了,发生越界访问的情况也就低了
那么你可能会问为什么不用32字节对齐呢,因为太浪费内存了,比如一个33字节的对象,如果使用32字节对齐算法,那么它的大小就是64字节,后面的31字节都是空的,太浪费了。
原因2,每个对象内部都默认有一个isa成员变量,它已经是8字节了,我们通常还会有其他成员变量,也就是对象实例的大小最小是8字节,通常都是>8字节的,所以给对象开辟内存的时候就最小是16字节了。
1.4.2.5 关联isa指针到对应的类
isa指针关联到相应的类过程
- zone不存在时、且canAllocNonpointer为true时。走 obj->initInstanceIsa(cls, hasCxxDtor)
- zone存在时、走 obj->initIsa(cls) 方法
- 最终initIsa方法做了些赋值操作
1.4.2.5.1 知识点1:isa_t是一个联合体,里面包含位域
isa_t是一个联合体,代码如下:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
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_t是一个联合体,有3个属性Class cls; 和uintptr_t bits;和一个匿名结位域,这3个属性时互斥的,该联合体占用8个字节内存空间。
- Class cls: 非nonpointer isa,没有对指针进行优化,直接指向类,
typedef struct objc_class *Class;
- uintptr_t bits: uintptr_t是
typedef unsigned long uintptr_t;
无符号长整型。bits使用了结构体位域,针对arm64架构和x86架构提供了不同的位域设置规则。 - struct {ISA_BITFIELD;}; 匿名位域
前两行代码是构造函数和析构函数,c的底层使用了C++的构造和析构函数
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
isa之所以设计成联合体位域就是为了优化存储空间,我们知道isa指针占用8字节,8字节只存储一个指针有点浪费,其实还是有很多空间可以优化的,所以Apple就利用联合体位域对这片空间做了优化。 我们用实际代码调试一下:
其中isa的地址为0x011d8001000080e9
转换成二进制为0b0000000100011101100000000000000100000000000000001000000011101001
我们可以发现地址的高位全是0,因为是64位系统,8字节的内存空间是很大的,所以往往高位基本都是空的,这就造成了很大的浪费,所以苹果将isa设置成联合体位域,以便能最大化的利用这片内存空间。
接下来我们看一下位域的定义
struct {
ISA_BITFIELD; // defined in isa.h
};
# 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)
# else
# error unknown architecture for packed isa
# endif
// SUPPORT_PACKED_ISA
#endif
注意:iOS测试分为模拟器测试和真机测试,处理器分为32位处理器,和64位处理器
- 模拟器32位处理器测试需要i386架构,(iphone5,iphone5s以下的模拟器)
- 模拟器64位处理器测试需要x86_64架构,(iphone6以上的模拟器),同样mac 64位机器也是x86_64架构
- 真机32位处理器需要armv7,或者armv7s架构,(iphone4真机/armv7, ipnone5,iphone5s真机/armv7s)
- 真机64位处理器需要arm64架构。(iphone6,iphone6p以上的真机)
接下来我们重点看真机arm64的位域
# 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
其中:
- nonpointer: 位域占1位(黑色),表示是否对isa指针开启了指针优化
- 0:纯isa指针
- 1:不止是类对象地址,isa中还包含了类信息、对象的引用计数等
- has_assoc: 位域占1位(蓝色),关联对象标志位,0没有,1存在
- associate [əˈsoʊsieɪt , əˈsoʊsiət] 联合,联合的
- has_cxx_dtor: 位域占1位,该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
- shiftcls: 位域占33位,存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针。
- 它的前面有22位,后面有3位
- magic: 位域占6位,⽤于调试器判断当前对象是真的对象还是没有初始化的空间
- weakly_referenced: 位域占1位,对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。
- unused 位域占1位,没有使用
- 在之前旧版本,该位置表示 deallocating: 标志对象是否正在释放内存
- has_sidetable_rc: 位域占1位,当对象引⽤计数⼤于 10 时,则需要借⽤该变量存储进位
- extra_rc:* 位域占19位,当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc。
同理我们也可以梳理出模拟器的位域分布,具体如下图
了解了isa的结构之后,接下来我们使用模拟器探索isa是怎么关联类的
1.4.2.5.2 知识点2:isa的初始化过程
接下来我们看一下isa的初始化过程
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
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;
}
newisa(0)初始化执行完毕,之后
- bits初始化为0
- cls=nil
- 匿名位域所有数据都为0
- 此时newisa 8字节内存对应的比特位信息为:
0b0000000000000000000000000000000000000000000000000000000000000000
64个比特位全是0
接着执行 newisa.bits = ISA_MAGIC_VALUE;
其中 # define ISA_MAGIC_VALUE 0x001d800000000001ULL
bits赋值之后这8字节的比特位信息如下: 因为newisa是一个联合体,一共占用8字节,所以内部变量共享这8字节,所以bits的值也就是cls和匿名位域的值,从上图可以得知,位域中nonpointer和magic已经有值
接着给has_cxx_dtor
赋值
接着执行最关键的setClass
函数 注意这里有一个选择分支:
#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
如果SUPPORT_INDEXED_ISA=1的话,那么直接将newCls赋值给cls,也就是说这种情况isa代表的就是普通类地址,并没有开启指针优化,也就是nonpointer=0
我们看一下SUPPORT_INDEXED_ISA的定义
// Define SUPPORT_INDEXED_ISA=1 on platforms that store the class in the isa
// field as an index into a class table.
// Note, keep this in sync with any .s files which also define it.
// Be sure to edit objc-abi.h as well.
#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
其中__arm64__ && !__LP64__
代表的意思是真机且不是64位系统 也就是说:
- 一般32位真机SUPPORT_INDEXED_ISA 为1
- 一般64位真机SUPPORT_INDEXED_ISA 为0
我们测试的机器就是64位的,newCls是一个类的地址指针,8字节,打印如下:
(lldb) p/t newCls
(Class) $1 = 0b0000000000000000000000000000000100000000000000001000100111000000
我们注意上面的地址会发现有如下特点:
- 该地址占8字节,低3位一般都为0,低3位一般都为0,低3位一般都为0
因为shiftcls属于位域变量,存储位置在3-47位,而newCls的前3位正好全是0,为了将newCls 3-47位的比特位信息存储给shiftcls,所以将newCls>>3之后再赋值给shiftcls,这样就将newCls与isa进行了关联。
到这里有同学可能会问,如果newCls低3位如果不是000,比如111,那么newCls不就丢失信息了吗。 我的理解是这样的,在64位系统中,由于地址占用8字节,一共64个比特位,那么低3位一般都是0,如果在32位系统中,地址占用4字节,一共32个比特位,那么低3位可能不会为0,但是这时候SUPPORT_INDEXED_ISA=1,那么就不会走这句代码了,直接走cls = newCls;
进行赋值了。
这里留个问题,64位系统中newCls为什么低3位都是0?
接着给extra_rc赋值 最后将newisa指针赋值给当前对象的isa,所以isa指针里面不仅存储了类的地址,还包含了是否有引用计数,是否有c++析构函数等其他信息。
至此isa的整个赋值过程结束
1.4.2.5.3 知识点3:isa关联当前类
平常获取对象的类会直接调用class方法,那么class方法内部实现是怎样的?见下面源码:
当前不是taggedPointer,而是nonpointor isa, 直接返回ISA()
最终走到了getClass方法里面的clsbits &= ISA_MASK;
, 注意这里的clsbits就是bits,也就是我们的isa 上面代码也就等价于: Class地址 = (Class)(对象isa & ISA_MASK) 其中:
- # define ISA_MASK 0x00007ffffffffff8ULL ISA_MASK转换成二进制也就是
0b0000000000000000011111111111111111111111111111111111111111111000
其中低3位和高17位都是0,其他44位是1,正好是isa中shiftcls位域
那么isa & ISA_MASK算法就等价于:
- 对象isa先右移3位,将前3位信息移除,此时shiftcls从第0位开始
- 在x86_64架构中,isa左移20位(因为shiftcls占44位,所以高位占20位);如果是arm64架构,isa左移31位(因为shiftcls占33位,所以高位占31位)
- 在x86_64架构中,isa再右移17位,进行复位。在arm64架构中,isa再右移28位,进行复位。
ISA_MASK 也即是ISA的一个面具!验证一下:
综上所述,我们结论如下: 1. 在对象内部,对象通过isa指针来获取当前类,当前类的指针=isa & ISA_MASK 2. 同样我们可以通过isa移位运算来获取当前类
- 如果是x86_64架构,那么当前类的指针=isa>>3<<20>>17
- 如果是arm64架构,那么当前类的指针=isa>>3<<31>>28
1.5 init主线流程
我们看一下的源码
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
执行流程就是init->_objc_rootInit 而且 _objc_rootInit 啥也没做,直接返回obj,为什么呢?
原因:就是为了提供接口,让子类去扩展,因为类里面唯一的成员变量isa在alloc的时候已经赋值了,所以如果有其他的成员变量,肯定属于子类,所以就留有init接口让子类去初始化。
1.6 new主线流程
我们接下来看一下new的源码
// Calls [cls new]
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));
}
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
所以执行流程就是:
- 执行 objc_opt_new, 通过objc_msgSend发送消息调用 +[NSObject new] 方法
- +[NSObject new] 方法直接调用callAlloc 和 init方法
所以说 [NSObject new] == [[NSObject alloc] init]