IOS 底层原理 对象的本质--(1)

1,464 阅读9分钟

对象的本质

探寻OC对象的本质,我们平时编写的Objective-C代码,底层实现其实都是C\C++代码。 那么一个OC对象占用多少内存呢?看完这篇文章你将了解OC对象的内存布局和内存分配机制。

使用的代码下载 要用的工具:

首先我们使用最基本的代码验证对象是什么?

int main(int argc, const char * argv[]) {
	@autoreleasepool {
	    // insert code here...
		NSObject *obj=[[NSObject alloc]init];
	    NSLog(@"Hello, World!");
	}
	return 0;
}

使用clang编译器编译成cpp, 执行clang -rewrite-objc main.m -o main.cpp之后生成的cpp,这个生成的cpp我们不知道是跑在哪个平台的,现在我们指定iphoeosarm64重新编译一下。xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main64.cpp,将main64.cpp拖拽到Xcode中并打开。

clang 编译器
xcrun 命令
sdk 指定编译的平台
arch arm64架构
-rewrite-objc 重写
main.m 重写的文件
main64.cpp 导出的文件
-o 导出

command + F查找int main,找到关键代码,这就是main函数的转化成c/c++的代码:

int main(int argc, const char * argv[]) {
 /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

  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"));

     NSLog((NSString *)&__NSConstantStringImpl__var_folders_c0_7nm4_r7s4xd0mbs67ljb_b8m0000gn_T_main_1b47c1_mi_0);
 }
 return 0;
}

然后搜索

struct NSObject_IMPL {
	Class isa;
};

那么这个结构体是什么呢? 其实我们Object-C编译之后对象会编译成结构体,如图所示:

那么isa是什么吗?通过查看源码得知:

typedef struct objc_class *Class; 

class其实是一个指向结构体的指针,然后com+点击class得到:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

class是一个指针,那么占用多少内存呢?大家都知道指针在32位是4字节,在64位是8字节。

NSObject *obj=[[NSObject alloc]init];

可以理解成实例对象是一个指针,指针占用8或者4字节,那么暂时假设机器是64位,记为对象占用8字节。 obj就是指向结构体class的一个指针。 那么我们来验证一下:

int main(int argc, const char * argv[]) {
	@autoreleasepool {
	    // insert code here...
		NSObject *obj=[[NSObject alloc]init];
		//获得NSobject对象实例大小
		size_t size = class_getInstanceSize(obj.class);
		//获取NSObjet指针的指向的内存大小
		//需要导入:#import <malloc/malloc.h>
		size_t size2 = malloc_size((__bridge const void *)(obj));
		NSLog(@"size:%zu size2:%zu",size,size2);
	}
	return 0;
}

得出结果是:

size:8 size2:16

结论是:指针是8字节,指针指向的的内存大小为16字节。 查看源码得知[[NSObject alloc]init]的函数运行顺序是:

class_createInstance
    -_class_createInstanceFromZone
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;
    **
    size_t size = cls->instanceSize(extraBytes);
    **
    return obj;
}

这个函数前边后边省略,取出关键代码,其实sizecls->instanceSize(extraBytes)执行的结果。那么我们再看下cls->instanceSize的源码:

//成员变量大小 8bytes
    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;
    }

可以通过源码注释得知:CF要求所有的objects 最小是16bytes。

class_getInstanceSize函数的内部执行顺序是class_getInstanceSize->cls->alignedInstanceSize() 查阅源码:

//成员变量大小 8bytes
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

所以最终结论是:对象指针实际大小为8bytes,内存分配为16bytes,其实是空出了8bytes

验证: 在刚才 的代码打断点和设置Debug->Debug Workflow->View Memory,然后运行程序,

点击obj->view *objc得到上图所示的内存布局,从address看出和obj内存一样,左上角是16字节,8个字节有数据,8个字节是空的,默认是0.

使用lldb命令memory read 0x100601f30输出内存布局,如下图:

或者使用x/4xg 0x100601f30输出:

x/4xg 0x100601f304是输出4个数据,x 是16进制,后边g是8字节为单位。可以验证刚才的出的结论。

那么我们再使用复杂的一个对象来验证:

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

使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main64.cpp编译之后对应的源码是:

struct NSObject_IMPL {
 Class isa;
};
struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;// 8 bytes
	int _age;//4 bytes
	int _no;//4 bytes
};

Person——IMPL结构体占用16bytes

Person *obj=[[Person alloc]init];
		obj->_age = 15;
		obj->_no = 14;

使用代码验证:

		Person *obj=[[Person alloc]init];
		obj->_age = 15;
		obj->_no = 14;
		
		struct Person_IMPL *p =(__bridge struct Person_IMPL*)obj;
		NSLog(@"age:%d no:%d",p->_age,p->_no);
		
		//age:15 no:14

使用内存布局验证:

以十进制输出每个4字节
使用内存布局查看数据验证,Person占用16 bytes。

下边是一个直观的内存布局图:

再看一下更复杂的继承关系的内存布局:

@interface Person : NSObject
{
	@public
	int _age;//4bytes 
}
@end
@implementation Person
@end

//Student
@interface Student : Person
{
@public
	int _no;//4bytes
}
@end
@implementation Student
@end

那小伙伴可能要说这一定是32字节,因为Person上边已经证明是16字节,Student又多了个成员变量_no,由于内存对齐,一定是16的整倍数,那就是16+16=32字节。 其实不然,Person是内存分配16字节,其实占用了8+4=12字节,剩余4字节位子空着而已,Student是一个对象,不可能在成员变量和指针中间有内存对齐的,参数和指针是对象指针+偏移量得出来的,多个不同的对象才会存在内存对齐。所以Student是占用了16字节。

那么我们来证明一下:

Student *obj=[[Student alloc]init];
		obj->_age = 6;
		obj->_no = 7;
		
		//获得NSobject对象实例成员变量占用的大小 ->8
		size_t size = class_getInstanceSize(obj.class);
		//获取NSObjet指针的指向的内存大小 ->16
		size_t size2 = malloc_size((__bridge const void *)(obj));
		NSLog(@"size:%zu size2:%zu",size,size2);
		//size:16 size2:16
		
		

再看一下LLDB查看的内存布局:

(lldb) x/8xw 0x10071ae30
0x10071ae30: 0x00001299 0x001d8001 0x00000006 0x00000007
0x10071ae40: 0xa0090000 0x00000007 0x8735e0b0 0x00007fff

(lldb) memory read 0x10071ae30
0x10071ae30: 99 12 00 00 01 80 1d 00 06 00 00 00 07 00 00 00  ................
0x10071ae40: 00 00 09 a0 07 00 00 00 b0 e0 35 87 ff 7f 00 00  ..........5.....

(lldb) x/4xg 0x10071ae30
0x10071ae30: 0x001d800100001299 0x0000000700000006
0x10071ae40: 0x00000007a0090000 0x00007fff8735e0b0

可以看出来0x000000060x00000007就是两个成员变量的值,占用内存是16字节。

我们将Student新增一个成员变量:

//Student
@interface Student : Person
{
@public
	int _no;//4bytes
	int _no2;//4bytes
}
@end
@implementation Student
@end

然后查看内存布局:

(lldb) x/8xg 0x102825db0
0x102825db0: 0x001d8001000012c1 0x0000000700000006
0x102825dc0: 0x0000000000000000 0x0000000000000000
0x102825dd0: 0x001dffff8736ae71 0x0000000100001f80
0x102825de0: 0x0000000102825c60 0x0000000102825890

LLDB可以看出来,内存变成了32字节。(0x102825dd0-0x102825db0=0x20)

我们再增加一个属性看下:

@interface Person : NSObject
{
	@public
	int _age;//4bytes 
}
@property (nonatomic,assign) int level; //4字节
@end
@implementation Person
@end

//InstanceSize:16 malloc_size:16 

为什么新增了一个属性,内存还是和没有新增的时候一样呢? 因为property=setter+getter+ivar,method是存在类对象中的,所以实例Person占用的内存还是_age,_level和一个指向类的指针,最后结果是4+4+8=16bytes

再看下成员变量是3个的时候是多少呢?看结果之前先猜测一下:三个int成员变量是12,一个指针是8,最后是20,由于内存是8的倍数,所以是24。

@interface Person : NSObject
{
	@public
	int _age;//4bytes
	int _level;//4bytes
	int _code;//4bytes
}
@end
@implementation Person
@end

Person *obj=[[Person alloc]init];
		obj->_age = 6;
		
		
		//获得NSobject对象实例成员变量占用的大小 ->24
		Class ocl = obj.class;
		size_t size = class_getInstanceSize(ocl);
		//获取NSObjet指针的指向的内存大小 ->32
		size_t size2 = malloc_size((__bridge const void *)(obj));
		printf("InstanceSize:%zu malloc_size:%zu \n",size,size2);
		
InstanceSize:24 malloc_size:32

为什么和我们猜测的不一样呢? 那么我们再探究一下: 实例对象占用多少内存,当然是在申请内存的时候创建的,则查找源码NSObject.mm 2306行得到创建对象函数调用顺序allocWithZone->_objc_rootAllocWithZone->_objc_rootAllocWithZone->class_createInstance->_class_createInstanceFromZone->_class_createInstanceFromZone最后查看下_class_createInstanceFromZone的源码,其他已省略,只留关键代码:

id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;
**
    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;
    **
    obj = (id)calloc(1, size);
   **
    return obj;
}

那么我们在看一下instanceSize中的实现:

	//对象指针的大小
    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;
    }

最后调用的obj = (id)calloc(1, size);传进去的值是24,但是结果是申请了32字节的内存,这又是为什么呢? 因为这是c函数,我们去苹果开源官网下载源码看下,可以找到这句代码:

#define NANO_MAX_SIZE			256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */

看来NANO_MAX_SIZE在申请空间的时候做完优化就是16的倍数,并且最大是256。所以size = 24 ;obj = (id)calloc(1, size);申请的结果是32字节。 然后再看下Linux空间申请的机制是什么? 下载gnu资料, 得到:

#ifndef _I386_MALLOC_ALIGNMENT_H
#define _I386_MALLOC_ALIGNMENT_H

#define MALLOC_ALIGNMENT 16

#endif /* !defined(_I386_MALLOC_ALIGNMENT_H) */


/* MALLOC_ALIGNMENT is the minimum alignment for malloc'ed chunks.  It
   must be a power of two at least 2 * SIZE_SZ, even on machines for
   which smaller alignments would suffice. It may be defined as larger
   than this though. Note however that code and data structures are
   optimized for the case of 8-byte alignment.  */
   //最少是2倍的SIZE_SZ 或者是__alignof__(long double)
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
			  ? __alignof__ (long double) : 2 * SIZE_SZ)
			  
			  
/* The corresponding word size.  */
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))

#ifndef INTERNAL_SIZE_T
# define INTERNAL_SIZE_T size_t
#endif

在i386中是16,在其他系统中按照宏定义计算, __alignof__ (long double)在iOS中是16,size_t是8,则上面的代码简写为#define MALLOC_ALIGNMENT (2*8 < 16 ? 16:2*8)最终是16字节。

总结:

实例对象其实是结构体,占用的内存是16的倍数,最少是16,由于内存对齐,实际使用的内存为M,则实际分配内存为(M%16+M/16)*16。实例对象的大小不受方法影响,受实例变量影响。