Objective-C 对象探究

2021-02-06  本文已影响0人  pengquanhua

本文将分析 OC 对象的本质,对象的内存布局,已经如何为对象分配内存。分析的源码来自 objc-812

对象的本质

打开 objc-812 runtime 的源码可以找到对象的定义:

typedef struct objc_object *id;

struct objc_object {
private:
    isa_t isa;
}

id 被类型定义为 objc_object *,也就是说对象本质上一个 objc_object 结构体。其唯一的变量 isa 的类型为 isa_t:

#define ISA_MAGIC_MASK  0x001f800000000001ULL
#define ISA_MAGIC_VALUE 0x001d800000000001ULL
#define RC_ONE   (1ULL<<56)
#define RC_HALF  (1ULL<<7)

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

    struct {
        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 deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
    };
};

isa_t 是一个联合体,可简单理解为 64 位二进制,每一位都代表特定的信息:

其中需重点理解 nonpointershiftcls,举个例子:假如 isa 的值为
0x011d800100008b1d,转为二进制:

nonpinter = 1 时,第 3-47 位为 shiftcls,即类的指针,这是什么呢?后面会分析。
为了方便取出 shiftcls,可以使用 isa & ISA_MASK

define ISA_MASK        0x00007ffffffffff8ULL // 3-47 位为 1

对象的内存布局

在 OC 中,一切对象都是以 objc_object 为基础,那如果一个类声明了多个属性,它的对象在内存中布局是怎样的呢?

@interface MYObject : NSObject

@property(nonatomic, strong) NSString *property1;
@property(nonatomic, strong) NSString *property2;
@property(nonatomic, assign) BOOL bool1;
@property(nonatomic, assign) NSInteger int10;
@property(nonatomic, strong) NSString *property3;

- (void)instanceMethod1;

@end

@implementation MYObject

- (void)instanceMethod1 {
    
}

@end

int main(int argc, const char * argv[]) {
    
    MYObject *myObject = [MYObject alloc];
    myObject.property1 = @"property1";
    myObject.property2 = @"property2";
    myObject.bool1 = YES;
    myObject.int10 = 10;
    myObject.property3 = @"property3";
    return 0;
}

return 0; 打个断点,运行程序,然后在 lldb 中输入 x/8gx myObject 将 myObject 对象内存打印出来。 我们已经知道对象的第一个变量为 isa,并且 isa 中的 3-47 位对应类的指针:


接着打印其他数据:

可以看到对象的内存布局不一定和变量声明的顺序是一样的。由于字节对齐和节省内存,在编译时编译器会进行重排。

对象的内存分配

上面我们已经知道了,内存的布局情况。那么在创建一个对象时,是如何为它分配内存的呢?
OC 的所有对象都是通过 alloc 方法来分配内存,研究 alloc 的内部实现,需要下载可以编译的 runtime 源码。在 [MYObject alloc]; 打个断点,此时就可以跳进 alloc 源码里研究它的流程了。大致如下:

[MYObject alloc]; 
-> _objc_rootAlloc(self); 
-> callAlloc(cls, false, true);
-> _objc_rootAllocWithZone(cls, nil); 
-> _class_createInstanceFromZone(cls, 0, nil,OBJECT_CONSTRUCT_CALL_BADALLOC);

最后的函数 _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);
}

通过设置断点,可以忽略无效的条件判断,可以得到核心的过程为:

size = cls->instanceSize(extraBytes); // 计算对象内存大小
obj = (id)calloc(1, size); // 分配内存
obj->initInstanceIsa(cls, hasCxxDtor);    // 初始化 isa,即把类指针关联到对象

所以分配对象内存过程经过了三个步骤:

  1. 计算内存大小
  2. 分配内存
  3. 对象关联类指针

计算内存大小

通过设置断点,可以忽略无效的条件判断,size_t instanceSize(size_t extraBytes) 过程为:

alignedInstanceSize()
-> cache.fastInstanceSize(extraBytes); 
-> align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
  1. fastInstanceSize 获取对象的内存大小:
size_t fastInstanceSize(size_t extra) const
    {
        ASSERT(hasFastInstanceSize(extra));

        if (__builtin_constant_p(extra) && extra == 0) {
            return _flags & FAST_CACHE_ALLOC_MASK16;
        } else {
            size_t size = _flags & FAST_CACHE_ALLOC_MASK;
            // remove the FAST_CACHE_ALLOC_DELTA16 that was added
            // by setFastInstanceSize
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }

这里暂时不展开,后续写到类的缓存 cache 时,会补充。现在只需知道 这个函数获取对象大小。

  1. align16 字节对齐
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

OC 的对象是以 16 位进行对齐。

打个断点,输出 MYObject 的对象大小为 48 个字节。

来验证一下,isa 占 8 个字节,property1/property2/property3 各占 8 个,bool1 占 1 个字节,int10 占 8 个字节,根据 C++ 结构体内存对齐原则,加起来占 48 个字节。

对象关联类指针

obj->initInstanceIsa(cls, hasCxxDtor);; 对象关联类指针

calloc 已经为对象分配好了内存,但此时这块内存还是空的,所以需要类信息关联到这个对象上,也就是为对象的 isa 赋值。

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
    isa_t newisa(0);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.setClass(cls, this);
        newisa.extra_rc = 1;
    }
    isa = newisa;
}

上面代码是删了无效信息后的核心代码,先判断 nonpointer 是否有效:

  1. nonpointerfalse,只为 isa 设置类指针。
  2. nonpointertrue,为 isa 设置类指针,并且设置 isa 的其他位。
    再来看看是如何设置类指针的:
inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
    shiftcls = (uintptr_t)newCls >> 3;
}

将类指针向右移 3 位后赋值给 shiftcls,这和在分析 isa_t 时讲的是一致的,但为什么要向右移 3 位呢,前面说到 OC 对象是以 16 进行内存对齐,而 OC 的类指针是以 8 字节进行对齐的,也就是地址后面 3 位都是 0,也就没必要进行存储了。

验证
obj->initInstanceIsa(cls, hasCxxDtor); 打个断点
输入 x/4gx 打印对象的内存上的内容:

此时,isa 为空的,往下运行一步,输入 x/4gx

此时,就找到对象的类信息了。

至此,已经为对象分配好了内存,并且关联了 isa。

小结

文中分析了 OC 对象本质都是 objc_object,每个对象都有一个 isa_t 类型的变量 isa,其存储了类的信息。并分析了对象的内存布局情况,以及对象内存分配和关联 isa 的过程。
那么在 OC 中,类以及属性、方法的本质又是什么呢?类是怎么存储属性和方法的呢?类的缓存又是什么呢?在下一篇文章,将为大家揭晓。

上一篇下一篇

猜你喜欢

热点阅读