Objective-C 对象探究

本文将分析 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 位二进制,每一位都代表特定的信息:

  • nonpointer: 表示是否对 isa 指针开启指针优化。0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等。

  • has_assoc:关联对象标志位,0没有,1存在

  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。

  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。

  • magic:用于调试器判断当前对象是真的对象还是没有初始化的空间。

  • weakly_referenced:志对象是否被指向或者曾经指向一个 ARC 的弱变量,
    没有弱引用的对象可以更快释放。

  • deallocating:标志对象是否正在释放内存

  • has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位

  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。

其中需重点理解 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 中,类以及属性、方法的本质又是什么呢?类是怎么存储属性和方法的呢?类的缓存又是什么呢?在下一篇文章,将为大家揭晓。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容