iOS - 关于 NSObject 的「本质」

NSObject,再熟悉不过,它可以指向任何 Objective-C 对象,也就是说它是一切 Objective-C 类的基类,这和 Java 中的 Object 类很像。

创建形式为 [[NSObject alloc] init],那么,通过这种形式创建的 NSObject 对象在内存中有多大?究其根源,就要知道一个 NSObject 类型的指针所指向的内存空间布局是怎样的便可得知 NSObject 对象的占用内存的大小。但前提是要首先弄懂,Objective-C 代码的本质。

Objective-C 代码的本质

我们所编写的 Objective-C 底层都是 C\C++ 的代码,然后编译器将 C\C++ 代码转换为汇编语言代码,然后转成汇编语言代码又转成只有 1 和 0 的机器码,最终运行在手机上。


image.png

这也可得出结论,Objective-C 的相面对象都是基于 C\C++ 的数据结构实现的。

Objective-C 的代码也能通过命令转为 .cpp 格式的代码,命令如下:

clang -rewrite-objc 目标文件.m -o 目标文件.cpp
clang Xcode 内置的 LLVM 编译器的前端
-rewrite-objc 表重写 Objective-C 代码
-o 表输出

运行后,目录下已多了一个 cpp 文件,打开这个文件查看内容会发现,即使内容很少的 Objetive-C 代码最终转成 C++ 代码也会高达数十万行,前面的 [[NSObject alloc] init] 最终也转成如下的形式:

NSObject* obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));

但假如要参考平台、架构来将 Objective-C 代码转成 C ++ 代码 可通过如下命令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 目标文件.m -o 目标文件.cpp 
xcrun Xcode 内置的 工具
-sdk iphoneos 表示最终的代码支持 iPhone 的硬件
-arch 表示不同的架构,如 arm64、armv7、i386

不同的平台架构,最终得到的代码也是不一样的。

运行后得到的 cpp 体积和代码量都减少了很多,这份代码便是能够运行在 iPhone 手机上的 arm64 架构的代码。

NSObject 的本质数据结构

在代码中,可发现一块有关 NSObject 的结构体,如下:

struct NSObject_IMPL {
    Class isa;
};

NSObject_IMPL 由名字可猜想这可能是 NSObject Implementation 的意思。
那么在查看 NSObject Definition 也能看到:

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

简化后(去掉协议):

@interface NSObject {
    Class isa;
}

对比 C++ 和 Objective-C 有关 NSObject 的两种定义也可再一次验证 Objective-C 类的底层通过结构体来实现。

Class 的定义为 typedef struct objc_class *Class,是指向结构体的指针。

既然是指针,在 64 位环境下占 8 个字节,32 位环境下占 4 个字节。那么可猜想,该结构体也可能只占 8 个字节,毕竟该结构体目前只有一个成员。

借助 runtime 函数验证猜想

在 runtime 中,通过 class_getInstanceSize(Class _Nullable __unsafe_unretained cls) 从字面意思上理解为可得到一个类的实例的大小。

此时打印:

NSLog(@"%zd", class_getInstanceSize([NSObject class]));

得到结果:

8

此时似乎进一步验证了 8 个字节的猜想,但事实并非如此。

借助 malloc_size 函数验证猜想

通过 malloc_size(const void *ptr) 函数可获得指针指向的内存地址的大小。

此时打印:

NSObject* obj = [[NSObject alloc] init];
NSLog(@"%zd", malloc_size((__bridge const void *)(obj)));

(__bridge const void *) 指将 Objective-C 指针转成 C 的指针。

打印结果:

16

借助 objc 源码

关于 Objective-C 某些底层的实现苹果已经开源,开源网址objc4 文件夹就是 Objective-C 部分底层源码。

image.png

列表中可找到最新的源码下载。

class_getInstanceSize

项目搜索 class_getInstanceSize 可发现在实现中,调用了 alignedInstanceSize() 方法,关于该方法,注释是这么写的:

Class's ivar size rounded up to a pointer-size boundary.
大概就是说返回类的成员变量所占据的大小。

image.png

那么也就意味着,class_getInstanceSize 方法返回的是实力对象的成员变量的大小。

image.png

由上图可知,系统为一个 NSObject 对象分配了 16 个字节,但是真正使用的,即 isa 占用的仅有 8 个字节。

alloc

一个 NSObject 对象的实例化都是通过 alloc -> init 实现, 在源码中搜索 alloc 相关可找到 NSObject 的 id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone) 方法,接下来的调用顺序如下:

image.png

由图可知,最终给对象分配内存还是调用 C 语言的 alloc 方法。值得注意的是 instanceSize 方法,整个方法体是这样的:

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    if (size < 16) size = 16;
    return size;
}

一目了然,这个函数中,假如创建的对象「体积」一旦小于 16,则直接分配 16 个字节。也就是说,一个 NSObject 对象,至少有 16 个字节的空间。

借助 Xcode

NSObject* objc = [[NSObject alloc] init] 后打上断点,运行到断点后可看到其内存地址,如下:

image.png

内存地址为 NSObject: 0x10065d6a0

然后 Debug -> Debug Workflow -> View Memory 可看到如下面板:

image.png

在最下方的 Address 输入 obj 的内存地址值,得到显示:

image.png

显示结果中,内存地址是以 16 进制显示,两个数表示一个字节(如 41、A1、B0...)

一个十六进制数表示 4 个二进制位,两个十六进制数表示 8 个二进制位,8 位表示一个字节,所以如 B0 这样的数表示一个字节。

上图中第一行即是 obj 的地址:


image.png

观察发现,刚好后八位都是 0 ,第二行开始的内存地址已然不是 obj 所属的空间了。41 A1 B0 A3 FF FF 1D 00 就是 isa 所占用的空间。

在终端借助 memeory read 内存地址 (或者 x 内存地址) LLDB 指令可读取一段内存空间,如下:

image.png

从 41 - 00 都是 obj 所占用的内存空间。
另一种读取内存的 LLDM 指令 x/数量格式字节大小 内存地址表示从该内存地址后的若干地址空间读取。

数量 表示要打印多少内存地址
格式 x 表示 16 进制,f 表浮点,d 表示 10 进制
字节大小 b 表示 1 字节,h 表示 2 字节,w 表示 4 字节,g 表示 8 字节

如键入 x/3xg 0x10065d6a0 表示从 0x10065d6a0 打印 3 个地址空间,格式为 16 进制,每段地址空间占 8 个字节,如下显示:

image.png

第一串为 obj 的内存地址,0x001dffffa3b0a141 位 isa 占用的空间。
第二串起就不再是 obj 的空间。

修改内存地址的 LLDB 指令为 memory write 内存地址

结论

NSObject 本质 C/C++ 数据结构为结构体,有一个 Class 类型的成员变量 isa,在 64 位的环境当中,系统为其对象分配了 16 个字节的空间,但是实际使用(isa 占用)的只有 8 个字节。

延伸

复杂对象的内存结构

那么,更复杂的对象的内存分布是如何的?在此,本人用自己的名字创建了一个类,包括年龄(int 型)和身高(int 型)两个成员变量。

@interface Valenti : NSObject
{
    @public
    int _age;
    int _height;
}
@end

转成 C++ 代码后可发现关于 Valenti 类其底层数据结构为下:

struct Valenti_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _height;
};

由于 struct NSObject_IMPL NSObject_IVARSNSObject_IMPL 只有一个成员 Class isa 则可转化为:

struct Valenti_IMPL {
    Class isa;
    int _age;
    int _height;
};

对其初始化并赋值:

Valenti* v = [[Valenti alloc] init];
v -> _age = 26;
v -> _height = 183;

其内存分配如下:


image.png

当我在代码中用重新定义的 Valenti_IMPL 结构体类型的指针指向 Valenti 类型对象并打印结构体中 _age 和 _height 两个成员的时候发现:

struct Valenti_IMPL* v_struct = (__bridge struct Valenti_IMPL *)(v);
NSLog(@"%d, %d", v_struct -> _age, v_struct->_height);

打印结果为:

26, 183

可得出结论 v 所指向的对象本质为 Valenti_IMPL 结构。

借助 class_getInstanceSizemalloc_size 函数打印大小:

 NSLog(@"%zd", class_getInstanceSize([Valenti class]));
 NSLog(@"%zd", malloc_size((__bridge struct Valenti_IMPL *)(v)));

打印结果为:

16
16

可知,v 这个对象在内存中所占用的空间为 16(isa + _age + _height),成员占用情况如下图:


image.png

若去掉成员 _height,此时理论上内存空间占用为 isa 8 个 + _age 4 个 = 12 个字节,但在 struct 中有内存对齐法则:结构体占用内存大小必须为最大成员的整数倍,所以在此借用两个函数打印大小结果还是 16(isa * 2)。

那么,假如再加一个成员变量 int 型的体重打印结果又会如何??

经过上面的分析,参考内存对齐,我相信答案一定脱口而出为 24(身高 4 + 年龄 4 + 体重 4 + isa 8 = 20,遵循内存对齐结果是 24)但实际打印结果:

class_getInstanceSize() 的结果为 24
malloc_size() 的结果为 32

这又是为什么?此时还得回到 allocWithZone 的源码一探究竟。由于前面没有贴过源代码,在这里贴一下 _class_createInstanceFromZone 的源码,因为 allocWithZone() 最终会走到这里并调用 calloc():

id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }
    return obj;
}

其中需要值得注意的是 size_t size = cls->instanceSize(extraBytes)obj = (id)calloc(1, size) 两句,其中 extraBytes 是 0,从 _objc_rootAllocWithZone() 内部一路走到这里来可以发现,extraBytes 参数的值一直都是 0,而 instanceSize() 最终调用的是 alignedInstanceSize() 也就是和 class_getInstanceSize() 一样,那么 size 的值就是 24,但到了 obj 这里,obj 的 size 就变成了 32, 那么 calloc() 就很值得考究了,calloc() 为 C 标准库函数。

开源网址中我们可找到有关内存的源码:

image.png

malloc.c 文件中找到 calloc() 函数,发现其内部调用的是 malloc_zone_calloc() ,其内部是现实:

void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

    void *ptr;
    if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
        internal_check();
    }

    ptr = zone->calloc(zone, num_items, size);
    
    if (malloc_logger) {
        malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
                (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
    }

    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
    return ptr;
}

这是系统的内存分配方式,也有一套内存对齐原则,这和前面的结构体 struct 的分配原则不同,但系统的内存分配原则也遵循着是谁的倍数的原则。内存中的堆空间可能是 16、32、48,最大是 256,源码中有最大空间的定义:

#define NANO_MAX_SIZE   256 /* Buckets sized {16, 32, 48, ..., 256} */

但在 iOS 系统中创建一个 Objective-C 对象,系统分配内存都是 16 的倍数,所以当 obj 的 size 为 24 的时候,系统会为其分配 32 的内存空间。

我们看到 libmalloc 源码中有很多 xxx_malloc.c 文件,这些内存分配都有着不同的原则,malloc.c 中的原则是通用的原则。

所以这里有个结论:

  • class_getInstanceSize() 返回的是一个对象至少需要多少内存空间,这和运算符 sizeof() 很像;
  • malloc_size() 返回得失系统为其分配多少内存空间。

更复杂的内存结构

首先声明 Person 类,只有一个 int 型年龄成员:

@interface Person : NSObject
{
    @public
    int _age;
    
}
@end

再声明 Valenti 类继承自 Person,并只有一个 int 型身高成员:

@interface Valenti : Person
{
    @public
    int _height;
}
@end

那么,一个 Valenti 实例占用多少内存空间?理论上为 Person 的 16 个字节 + _height 4个字节 = 20,再根据内存对齐法则可得占用空间为 16 的倍数 32,但当使用 class_getInstanceSizemalloc_size 函数打印大小结果为:

16
16

因为在 Person 的 16 个字节中,有 4 个是空余的,所以 Valenti 类中的 int 型身高刚好可以放到这 4 个空间内,所以最终结果为 16。

关于内存对齐

关于内存对齐,objc 源码中也有体现,上面提到过的 alignedInstanceSize() 中也是调用并返回了 word_align(unalignedInstanceSize()) 的结果, word_align() 便已经「对齐」了内存。

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,076评论 1 32
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,676评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,161评论 0 7
  • iOS底层原理总结 - 探寻OC对象的本质 对小码哥底层班视频学习的总结与记录。面试题部分,通过对面试题的分析探索...
    xx_cc阅读 21,149评论 31 177
  • Imaging Biomarkers and Computer-Aided Diagnosis Laborator...
    MrGiovanni阅读 1,334评论 1 2