我们平时写的iOS代码底层其实都是C/C++代码 ,编译器LLVM会把OC代码层层转换成机器语言
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对象实例化出一个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);
然后再切入,原谅我无限切入,我保证是最后一次源码切入。。。。
// 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对象分配了多少内存?
1.系统分配了16个字节给NSObject对象(通过malloc_size函数查看,源码_objc_rootAllocWithZone分配16字节)
2.但是实际使用了8个字节,isa指针就占用八个字节,如果继承的话其实也就8个字节,上面的16个字节是框架内约束,规定最少16个字节(64bit环境下可以通过class_getinstanceSize查看实际使用)
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/数量格式字节数
:内存地址
格式:
x
16进制,f
浮点,d
10进制
字节大小:
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
,然后我们就可以看到值被修改,前提是我们断点段在那里或者编辑个断点操作一下,就能输出被我们修改内存后的值。
整体如图所示:
@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_getInstanceSize
和sizeof
一样,返回的是最少需要多少内存,一个是对象,一个是结构体(结构体字节对齐后的内存)
malloc_size
返回到是操作系统优化后分配的最终实际内存(操作系统会有自己的字节对齐优化访问速度)