iOS 对象内存结构本质

前言

我们平时写的iOS代码底层其实都是C/C++代码 ,编译器LLVM会把OC代码层层转换成机器语言
iOS 对象内存结构本质_第1张图片

对象的本质

NSObject * obj = [[NSObject alloc] init];对象占用多少内存?我们针对这个问题往下看

通过Xcode的MacOS创建一个工程,cd到main.m文件目录,然后我们通过编译器前端工具clang来生成cpp代码

clang -rewrite-objc main.m

这个时候会生成一个main.cpp的文件,里面就是通用cpp代码,代码非常的多,10w行大概,3.7m

以上方法编译器把OC代码转成C++代码,最好需要知道哪个平台,Windows,Mac还是iOS上,因此我们需要指定平台还有对用的架构

模拟器 i386
32bit armv7
64bit arm64

下面让我们生成的cpp代码支持iOS平台,cmd如下

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

意思通过Xcode指定平台,iphoneos,然后编译器指定输出架构arm64,然后转成对应平台架构的cpp代码,现在就只有3w多行代码了,1.5m,精简了很多。


我们看到NSObject对象在Xcode中API暴露出来的以及再刚才的cpp中搜索NSObject,可以相互对应,因此可以看到转换后的结构其实就是对应C++的结构体,可以猜测大概的内存布局

iOS 对象内存结构本质_第2张图片
根据上述的猜测,NSObject对象实例化出一个C++结构体,如果只有一个isa指针,isa指针的地址就是对象结构体的地址,在64bit上就占八个字节

但是实际上确并非如此,我们看看对应的Runtime库api打印出来的实际大小
通过Runtime和Malloc两个库打印一下内存分配情况

#import 
#import 

struct NSObject_IMPL {
    Class isa;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSObject *obj = [[NSObject alloc] init];
        
        // 分配8个字节
        NSLog(@"%zd",class_getInstanceSize(obj.class));
        
        // 实际分配16个字节
        NSLog(@"%zd",malloc_size((__bridge const void *)(obj)));
    }
    return 0;
}

通过打印可以看到上面Runtime打印的是8个字节,下面Malloc打印的16个字节,我们通过Apple开源的Runtime库查看到以下

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
}

Class's ivar size rounded up to a pointer-size boundary.实际Runtime返回的不是实际分配的内存,而是结构中成员变量的指针大小,可以推断出,实际上NSObject对象分配了16个字节的内存,但是实际上使用的只有8个字节,用来存放isa指针,剩下八个字节预留下来了。那么下面继续看下源码,怎么证明NSObject对象是否真的分配了16个字节的内存。

源码地址:opensource
一般在浏览器输入opensource.apple.com/tarballs,然后搜一下即可

首先我们找到对应的alloc初始化函数,在NSObject.mm

id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
    id obj;

#if __OBJC2__
    // allocWithZone under __OBJC2__ ignores the zone parameter
    (void)zone;
    obj = class_createInstance(cls, 0);
#else
    if (!zone) {
        obj = class_createInstance(cls, 0);
    }
    else {
        obj = class_createInstanceFromZone(cls, 0, zone);
    }
#endif

    if (slowpath(!obj)) obj = callBadAllocHandler(cls);
    return obj;
}

然后往后切入

id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

再往后切入

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

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    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;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

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

    return obj;
}

捕捉到一个创建的c函数


size_t size = cls->instanceSize(extraBytes);

....

obj = (id)calloc(1, size);

然后再切入,原谅我无限切入,我保证是最后一次源码切入。。。。
iOS 对象内存结构本质_第3张图片

// Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

    size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

看到这里,能看到NSObject到底分配了多少内存了吧,16个字节,alignedInstanceSize看到这个函数没,就是class_getinstancesize就是获取的这个值,8个字节,上面有打印,但是CF框架下,已经在源码上规定了NSObject最少是16个字节。


总结NSObject对象内存分配

NSObject对象分配了多少内存?
1.系统分配了16个字节给NSObject对象(通过malloc_size函数查看,源码_objc_rootAllocWithZone分配16字节)
2.但是实际使用了8个字节,isa指针就占用八个字节,如果继承的话其实也就8个字节,上面的16个字节是框架内约束,规定最少16个字节(64bit环境下可以通过class_getinstanceSize查看实际使用)

引出相关的iOS内存相关面试题

NSArray *array = @[@"Hello",@"World"];
NSLog(@"%@",array);

如上面所示,在这两行代码之前,插入一行代码,使得输出为@[@"Hello",@"iOS"],这里涉及到两个知识点大小端问题lldb的memroy read相关指令

大小端问题

	int a = 0x12345678;
	unsigned char *p = (unsigned char *)&a;
	if (0x78 == *p)
	{
		printf("little end\n");
	}
	else
	{
		printf("big end\n");
	}

大端:高位存在低地址,低位存在高地址;
小端:高位存在高地址,低位存在低地址;(intel的x86,ARM普遍都是属于小端)

常用lldb memory read指令


打印
p/po:这两个就不用说了打印对象


读取内存
memory read/数量格式字节数:内存地址
x/数量格式字节数:内存地址
格式:
x16进制,f浮点,d10进制
字节大小:
b:byte 1字节 h:half word 2字节
w:word 4字节 g:giant word 8字节

修改内存
memory write 内存地址 数值
memory write 0x1008611 100086


现在来看下题目,修改数组中的值,由于这里是NSArray我们直接修改,下面就绕过oc的机制通过内存地址来修改

(lldb) po array
<__NSArrayI 0x1006016f0>(
Hello,
World
)

(lldb) x array
0x1006016f0: 41 f1 0b 8a ff ff 1d 01 02 00 00 00 00 00 00 00  A...............
0x100601700: 80 10 00 00 01 00 00 00 a0 10 00 00 01 00 00 00  ................

上面就是数组的内存地址起始值0x1006016f0,通过x指令打印出后续的内存,可以根据数量格式字节数打印一下

(lldb) x/6xg array
0x1006016f0: 0x011dffff8a0bf141 0x0000000000000002
0x100601700: 0x0000000100001080 0x00000001000010a0
0x100601710: 0x7000000010060163 0x400000001006003c

其中第一个x代表memory read,第二个6代表打印6份,第三个x代表以16进制打印,第四个g代表每一份8个字节输出

iOS是小端的模式输出,比如前八个字节实际内存是41 f1 0b 8a ff ff 1d 01,16进制表示出来的小端就是把41高地址放在高位,低地址01放在低位,因此就有了0x011dffff8a0bf141

根据我们上面获取到NSObjec的内存地址来看,默认最少是16个字节,其中一个isa指针占用八个字节,框架内部规定最少16个字节,因此NSObject实际有16个字节,但是其他继承NSObject的类就不是了,他们会有自己的成员变量,本质是结构体,内存占用需要计算所有成员变量,这里第一个字节是isa,第二个字节打印出来是2,如果数组是3的话,打印就是3,可以猜测,这个数组的第二个字节是cout用来存储数组个数,实际值是从偏移2个单位16个字节开始的

(lldb) po 0x0000000100001080
Hello

(lldb) po 0x00000001000010a0
World

(lldb) po 0x011dffff8a0bf141
2060332890

(lldb) po 0x0000000000000002
2

可以看到前16个字节存放的东西不知道,但是肯定包含一个isa,这里猜测就是一个isa指针和其他数据占了最少的16个字节,然后在16-32之间存放了Hello World那么现在就很明确,我们拿到数组内存地址,然后拿到指向堆内存的指针地址,然后这个指针往后偏移2个单位(64bit上一个单位是8个字节),就是Hello,3个单位就是World

解法如下

NSArray *array = @[@"Hello",@"World"];
// 一行代码,把数组改成@[@"Goodbye",@"world"]
*((void **)(__bridge void *)array + 3) = @"Mikejing";
NSLog(@"%@",array);

__bridge void *就是把对象id类型强转成c的void *,这里的array获取到的还是堆C++结构体的首地址,然后我们继续取void **(指向id类型的指针,也就是我们平时用到的指针值)取到栈中NSArrayt *array指针,里面存放的就是@[@"Hello",@"World"]的首地址,然后根据上面分析,往后偏移3个单位,取到World的地址,然后取*进行赋值即可。

这里理解有问题的话各位大佬记得留言回复我。

自定义对象内存结构分析

@interface People : NSObject
{
    @public
    int _phone;
    int _age;
}

@end

@implementation People
@end

通过上面的clang指令查看c++代码

struct People_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _phone;
    int _age;
};

struct NSObject_IMPL {
    Class isa;
};

以上就是我们继承于NSObject对象的的类,我们来分析下该自定义类的内存结构。
首先根据上面NSObject和面试题NSArray的分析,我们可以猜测,对象有两个成员变量,int类型占4个字节,加上继承的isa指针,那么应该有16个字节,我们来验证下

People *p = [[People alloc] init];
p->_phone = 123;
p->_age = 100;
        
NSLog(@"%zd",class_getInstanceSize(p.class));
NSLog(@"%zd",malloc_size((__bridge const void *)(p)));

打印都是16

再次通过lldb来查看内存值

(lldb) x p
0x1006103f0: a9 11 00 00 01 80 1d 00 7b 00 00 00 64 00 00 00  ........{...d...
0x100610400: d0 04 61 00 01 00 00 00 10 07 61 00 01 00 00 00  ..a.......a.....
(lldb) x/4xg p
0x1006103f0: 0x001d8001000011a9 0x000000640000007b
0x100610400: 0x00000001006104d0 0x0000000100610710

根据上面Malloc和Runtime打印都是16,因此0x001d8001000011a9这个八个字节是isa指针,那么0x000000640000007b剩下八个就是两个int类型的变量,可以看到64就是100,7b就是123,iOS 64bit系统都是小端模式,可以理解为挨个字节高地址倒序读取。

我们可以试着修改内存中的值,也就是修改int变量的值。

People内存首地址 0x1006103f0
(lldb) memory write 0x1006103f8 50
(lldb) memory write 0x1006103f8 bb
(lldb) memory write 0x1006103f8 8

People对象内存首地址是0x1006103f0,可以看到变量存储的位置是在第八个字节,比如7b,对应八个字节的便宜就是0x1006103f8,然后我们就可以看到值被修改,前提是我们断点段在那里或者编辑个断点操作一下,就能输出被我们修改内存后的值。

整体如图所示:

iOS 对象内存结构本质_第4张图片

多重继承对象

@interface People : NSObject
{
    @public;
    int _age;
}
@end
@implementation People
@end
@interface Man : People
{
    @public;
    int _number;
}
@end
@implementation Man
@end

可以看下打印结果

		People *p = [[People alloc] init];
        p->_age = 100;
        
        Man *m = [[Man alloc] init];
        m->_number = 200;
        
        NSLog(@"%zd",class_getInstanceSize(p.class));
        NSLog(@"%zd",malloc_size((__bridge const void *)p));
        
        NSLog(@"%zd",class_getInstanceSize(m.class));
        NSLog(@"%zd",malloc_size((__bridge const void *)m));

都是16,首先People对象isa8个字节,int成员变量4个字节,但是由于框架内源码写了最少16个字节,因此malloc_size打印是16,从另一个角度来看,结构体内存对齐问题,结构体的大小必须是最大成员变量大小的倍数,这里8个字节的倍数最少也是16,因此这个没毛病。但是class_getInstanceSize应该返回的是实际使用的内存,应该是12才对啊,看看源码

uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

可以看到返回的是word_align意思是内存对齐后大小,参数就是未对齐的值,比如这个穿进去12,出来对齐16.

那么继承出来的Man对象,打印也是16字节,通过x m指令进行内存查看,确实是两个int类型在同一个64位空间下 。

再深入看下

@interface People : NSObject
{
    @public;
    int _age;
    long _number;
}
@end

@implementation People

@end

上面这种结构,你可以打印一下看看,正常按照我们的推算来说,isa 8个字节,age 4个字节,number 8个字节,结构体字节对齐,应该是24个字节才对,我们打印一下

		People *p = [[People alloc] init];
        p->_age = 20;
        p->_number = 12300;
        NSLog(@"%zd,%zd",class_getInstanceSize(p.class),malloc_size((__bridge void *)p));

但是打印出来是24和32,可以根据上面的opensource打开apple的源码查看libmalloc里面的实现,代码比较复杂。结论就是结构体字节对齐是根据最大内存数的字段进行倍数扩充,但是Apple的iOS操作系统需要优化,内部是以最小16字节为单位进行扩充的,因此,我们结构体isa(8的倍数),算出来是24,也就是实际使用class_getinstancesize的大小,但是传入iOS的malloc.c根据操作系统的内存分配规则,就是16的倍数,因此会分配32个字节给这个对象使用。

两个函数总结:
class_getInstanceSizesizeof一样,返回的是最少需要多少内存,一个是对象,一个是结构体(结构体字节对齐后的内存)
malloc_size返回到是操作系统优化后分配的最终实际内存(操作系统会有自己的字节对齐优化访问速度)

你可能感兴趣的:(基础知识)