iOS底层原理实践--对象本质

前言

最近在学习底层原理相关知识,想着写点东西,一则加深理解,二则希望能和大家一起交流,扩展思路。😸但简书搜索 iOS对象本质,惊了,我这是有多过时,各种文章满天飞,瞬间不知如何下笔。但还是硬着头皮往下写....,毕竟别人的终归是别人的,做好自己就好,这是我的处女作,写的不好解释不恰当的,请大家见谅,并帮忙指出。

废话不多说,直接入正题(以下内容都基于arm64进行分析)

一、NSObject的本质是什么?让我们来解开它的神秘面纱
#import <Foundation/Foundation.h>

//创建一个动物类
@interface Animal : NSObject
@end

@implementation Animal
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *object = [[NSObject alloc] init];
    }
    return 0;
}
  • xcode新建项目,main文件中编写上图代码,打开终端,进入main.m所在的文件夹,通过如下命令

clang -rewrite-objc main.m -o main.cpp
或者
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
分析:
xcrun -sdk iphoneos
使用xcode命令行工具xcrun指定sdk类型为iphoneos
clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
使用clang编译器,指定编译的架构为arm64,-rewrite-objc重写main.m文件,-o 输出文件为main-arm64.cpp
未指定架构和sdk类型的,则会编译出所有sdk类型(例:macos,ipados)和所有架构(例:armv7 -i386)的代码,编译自然会慢一些

生成编译后的main-arm64.cpp文件(摘录部分代码)

//NSObject编译后的结构
struct NSObject_IMPL {
    Class isa;
};

//Class为结构体指针类型
typedef struct objc_class *Class;

//Animal类编译后结构
struct Animal_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSObject *object = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    }
    return 0;
}

从上面的代码可看出,NSObject编译后的结构为结构体类型,其中包含一个Class类型的isa指针,Animal编译后的结构也为结构体类型,其中包含一个NSObject_IMPL的结构体。分析到这里,我们可以确认,OC代码的底层实现是C/C++。

接下来让我们分析几个问题,来巩固我们上面说的知识

二、NSObject对象占多少内存?
//NSObject编译后的结构
struct NSObject_IMPL {
    Class isa;
};

//Class为结构体指针类型
typedef struct objc_class *Class;

NSObject *object = [[NSObject alloc] init];
// 获得object指针所指向内存的大小  16
NSLog(@"%zd",malloc_size((__bridge const void *)(object)));
 // 获得NSObject实例对象的成员变量所占用的大小   8
NSLog(@"%zd",class_getInstanceSize([NSObject class]));

先简单通过打印分析一下,按照刚才我们所知的NSObject编译后的结构体,可以知道他包含了一个指针(arm64架构下,指针所占的内存为8个字节)。class_getInstanceSize获取的是类成员变量所占用的大小,因此该结果应该为8个字节,上图malloc_size获取的是object指针所指向的内存大小,按结构分析应该也是8个字节才对,但是实际打印却是16,接下来让我们跟踪源码苹果官方源码来分析下为何class_getInstanceSize得到的大小为8,malloc_size得到的大小为16。

分析class_getInstanceSize

  • 通过查看class_getInstanceSize的头文件,路径在objc/runtime.h,由此可知该函数属于objc库中,源码库中对应为objc4的库。或者我们可以利用强大的搜索引擎进行搜索,如果存在源码,广大网友们会告知你去下载哪个开源库的。从以下源码可知,class_getInstanceSize获取的是类成员变量字节对齐后所占用的大小,而NSObject中只有一个指针,指针在arm64系统下为8个字节
// May be unaligned depending on class's ivars.
//获取(unaligned)字节对齐前类的(ivars)成员变量的大小
uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;  
 }
// Class's ivar size rounded up to a pointer-size boundary.
//获取类的(ivar)成员变量字节对齐后的大小
uint32_t alignedInstanceSize() const {
     return word_align(unalignedInstanceSize());
}
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

分析malloc_size

  • 通过查看malloc_size头文件路径malloc/malloc.h文件中,在源码库中查找malloc,我查找到了libmalloc库,但是很遗憾,只能看到表层,里层代码是闭源的,无法探知其原理。
  • 然后我从开辟空间的角度去探究,毕竟malloc_size仅仅是获取内存大小,实际开辟内存的是alloc方法,alloc通过头文件可知在objc库中,下载objc4-781,接下来看我找到的代码段:
//大家可以下载源码自己跟踪,alloc调用的是allocWithZone,然后即可接着定位到如下函数
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);
    //从此处可知,size的大小通过Class的instanceSize获取
    if (outAllocatedSize) *outAllocatedSize = size;
    //后续代码不重要,省略
    .....
}

size_t instanceSize(size_t extraBytes) const {
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
         return cache.fastInstanceSize(extraBytes);
     }
     size_t size = alignedInstanceSize() + extraBytes;
     // CF requires all objects be at least 16 bytes.
     if (size < 16) size = 16;
     return size;
}
  • 根据instanceSize函数,可知alloc开辟空间的大小,首先获取类成员变量字节对齐后的大小,然后再根据系统本身约束来开辟空间,此处由于size为8,因此if(size < 16)会把size强制设置为16,所以alloc开辟的空间至少为16字节,因此malloc_size获取NSObject对象所占内存时,得到的结果为16。

根据上面的源码追踪,我相信都能理解了,也自然也能准确的回答NSObject所占的内存了。

NSObject对象占多少内存?
我的答案:在arm64架构下,NSObject所占内存大小为16个字节。其中8个字节为NSObject对象中的isa指针所占用的字节,另外8个字节是系统为了更优的访问内存额外开辟的空间,不存放任何数据。

三、NSObject对象占用多少内存我们已经知道了,那下面几个对象呢,让我们一起来分析一下

@interface Animal : NSObject
{
    int age;//年龄
    int weight;//重量
}
@end

@implementation Animal

@end

@interface Dog : Animal
{
    int no;//编号
}
@end

@implementation Dog

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Animal *animal = [[Animal alloc] init];
        Dog *dog = [[Dog alloc] init];      

        NSLog(@"%zd\n",class_getInstanceSize([Animal class]));
        NSLog(@"%zd\n",malloc_size((__bridge const void *)(animal)));

        NSLog(@"%zd\n",class_getInstanceSize([Dog class]));
        NSLog(@"%zd\n",malloc_size((__bridge const void *)(dog)));
    }
    return 0;
}
  • Animal 和Dog编译后的结构如下,int在arm64系统下为4个字节
struct Animal_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int age;
    int weight;
};

struct Dog_IMPL {
    struct Animal_IMPL Animal_IVARS;
    int no;
};
  • Animal通过class_getInstanceSize得到的大小为16,malloc_size得到的大小也为16。这时候也许会有疑问,NSObject对象都16了,为何加了两字段还是16。其实NSObject对象虽然占16字节,但其后面8个字节并未存放任何数据,因此系统会去将这两个int值填充进去,而不是new更多内存。
  • Dog通过class_getInstanceSize得到的大小为20?还是24呢?因为class_getInstanceSize获取的是字节对齐后成员变量所占内存大小,因此结果为24,结构体字节对齐大家可以百度去了解一下。那通过malloc_size得到的大小是多少呢?有点意外,不是字节对齐后的24,而是32,这里涉及到一个新的知识点,操作系统内存对齐的概念,为了访问内存最优,操作系统也需要对开辟内存空间有要求,由于这个知识点涉及的知识有点多,大家可以自行搜索相关文章。libmalloc中对于操作系统字节对齐,有一个这样的宏,也就是说iOS操作系统在对内存分配的时候以16的倍数进行内存分配,而Dog实际占用字节为24,靠近32,因此系统会分配32个字节。
#define NANO_MAX_SIZE           256 /* Buckets sized {16, 32, 48, ..., 256} */

总结:
iOS对象本质,讲到这里就结束了。之所以把我自己思考问题的过程写在文章里,是希望能够帮助大家在了解其他知识的时候,多一个思考的方向,也希望各位能提出疑问,我尽量查漏补缺。

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