第2课-OC对象原理上-2

本文详细剖析了Objective-C对象内存分配过程中isa指针的作用、内存对齐规则,以及如何通过查看malloc源码理解堆中对象大小计算的16字节对齐算法。还揭示了isa指针关联类的过程和初始化细节。
摘要由CSDN通过智能技术生成

第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字节。

通过上面的分析我们得出以下结论:

  1. 在对象的内部,成员变量之间通过8字节对齐算法计算实例变量的大小。
  2. 而在堆中为对象开辟内存的时候,通过16字节对齐算法计算对象的大小。

这里有一个问题为什么对象以16字节对齐开辟内存而不是8字节呢?

  1. 原因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. 原因2,每个对象内部都默认有一个isa成员变量,它已经是8字节了,我们通常还会有其他成员变量,也就是对象实例的大小最小是8字节,通常都是>8字节的,所以给对象开辟内存的时候就最小是16字节了。

1.4.2.5 关联isa指针到对应的类

isa指针关联到相应的类过程

  1. zone不存在时、且canAllocNonpointer为true时。走 obj->initInstanceIsa(cls, hasCxxDtor)
  2. zone存在时、走 obj->initIsa(cls) 方法
  3. 最终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算法就等价于:

  1. 对象isa先右移3位,将前3位信息移除,此时shiftcls从第0位开始
  2. 在x86_64架构中,isa左移20位(因为shiftcls占44位,所以高位占20位);如果是arm64架构,isa左移31位(因为shiftcls占33位,所以高位占31位)
  3. 在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];
}

所以执行流程就是:

  1. 执行 objc_opt_new, 通过objc_msgSend发送消息调用 +[NSObject new] 方法
  2. +[NSObject new] 方法直接调用callAllocinit方法

所以说 [NSObject new] == [[NSObject alloc] init]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值