本系列接《Effective Objective-C 2.0》一书中的系列文章。git
接下来,会有如下系列的主题的文章,以实践探索Objective-C 2.0的一些特性,参考部分博文、官方文档、以及MJ小码哥的系列课程—很是推荐:框架
Objective-C(七)对象内存分析iphone
本文主要针对几个类来窥探实例对象在内存中的存储,咱们从成员变量和属性入手,本文相关代码在这儿。
咱们平时编写的Objective-C代码,底层实现其实都是C\C++代码
因此Objective-C的面向对象都是基于C\C++的数据结构实现的
Objective-C的对象、类主要是基于C\C++的什么数据结构实现的——结构体。
NSObject *obj = [[NSObject alloc] init];
// 得到NSObject实例对象的成员变量所占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
// 得到obj指针所指向内存的大小 >> 16
NSLog(@"%zu", malloc_size((__bridge const void *)obj));
复制代码
上面有两个函数:
建立一个实例对象,至少须要多少内存?
# import <objc/runtime.h>
class_getInstanceSize([NSObject class]);
建立一个实例对象,实际上分配了多少内存?
#import <malloc/malloc.h>
malloc_size((__bridge const void *)obj);
将Objective-C代码转换为C\C++代码
//若是须要连接其余框架,使用-framework参数。好比-framework UIKit
// xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
复制代码
其中,咱们能够发现NSObject转换为C++的底层结构体为:
//main.cpp
struct NSObject_IMPL {
Class isa; //typedef struct objc_class *Class;
};
复制代码
咱们直接经过断点调试也能够发现,obj的确只有一个isa成员变量。
下面咱们经过查看obj对应的内存,来观察:
从上图中能够看到,从0x1029000a0地址开始的8个字节,是有数据的,后面8个字节,都是0。
根据最开始打印的:
// 得到NSObject实例对象的成员变量所占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
// 得到obj指针所指向内存的大小 >> 16
NSLog(@"%zu", malloc_size((__bridge const void *)obj));
复制代码
咱们猜想,前8个字节就是obj中isa
占用的内存空间,后8个字节,是为了内存对齐而分配的填充字节。
为了验证这个猜想,咱们将obj对象转换为对应的结构体:
struct NSObject_IMPL {
Class isa;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
NSLog(@"%zu", malloc_size((__bridge const void *)(obj)));
struct NSObject_IMPL *objImpl = (__bridge struct NSObject_IMPL *)obj;
NSLog(@"obj address: %p", obj);
NSLog(@"objImpl address: %p, objImpl isa: %p", objImpl, objImpl->isa);
NSLog(@"-----");
}
return 0;
}
复制代码
再次运行,输出结果以下:
从上图咱们能够看出:
NSObject_IMPL
结构体,地址一致;isa
的值为0x1dffffa4575141,与上一次运行一致,且只占8个字节。isa
,8个字节为内存对齐的填充字节。在这里,为何是16个字节,须要说明一下,iOS系统会给对象至少分配16*n字节的大小。
@interface BFPerson : NSObject
{
@public
int _age;
int _male;
}
@property (nonatomic, assign) double height;
@end
@implementation BFPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
BFPerson *jack = [[BFPerson alloc] init];
jack->_age = 24;
jack->_male = 1;
jack.height = 185;
NSLog(@"jack age is %d, male: %d, height: %f", jack->_age, jack->_male, jack.height);
BFPerson *rose = [[BFPerson alloc] init];
rose->_age = 21;
rose->_male = 0;
rose.height = 165;
NSLog(@"rose age is %d, male: %d, height: %f", rose->_age, rose->_male, rose.height);
NSLog(@"%zd", class_getInstanceSize([BFPerson class]));
NSLog(@"%zd", malloc_size((__bridge const void *)jack));
}
return 0;
}
复制代码
这一次,对象更复杂,并且继承了NSObject,那么其中实例对象中成员变量分配了多少字节,实际占用了多少本身呢?
最后输出:
经过将上面代码转换为C++代码,咱们能够获得BFPerson的结构:
其中,BFPerson_IMPL
包含了NSObject_IMPL
结构体,因此最后能够规整为:
能够看到,第一个变量仍是isa指针,后面跟着咱们定义的两个成员变量,及定义的一个属性。
咱们知道属性最后会转换一个对应的成员变量,因此总共有三个成员变量。
其中isa,咱们知道是一个占用8字节,因此获得下面的各个成员变量的占用字节数:
这和咱们打印该实例对象占用的为24个字节相符,可是系统仍是给它分配了32个字节。
咱们经过两种方式来查看内存中的实例对象,是否是按咱们预想的方式存储这些成员变量。
以下;
isa
值;isa
占8个字节,但结构体最后占用24字节,已是其整数倍。咱们经过前面转换C++代码结构体的分析,将jack转换为咱们自定义的结构体。
struct NSObject_IMPL {
Class isa;
};
struct BFPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _male;
double _height;
};
struct BFPerson_IMPL *jackImpl = (__bridge struct BFPerson_IMPL *)jack;
NSLog(@"jack age is %d, male: %d, height: %f", jackImpl->_age, jackImpl->_male, jackImpl->_height);
复制代码
输出以下,能够看到结果彻底一致,因此咱们的符合咱们的猜测。
继承关系以下:
@interface BFPerson : NSObject
{
@public
int _age;
int _male;
}
@property (nonatomic, assign) double height;
@end
@implementation BFPerson
@end
@interface BFProgrammer : BFPerson
{
@public
char *company;
}
@end
@implementation BFProgrammer
@end
复制代码
测试代码以下;
BFPerson *jack = [[BFPerson alloc] init];
jack->_age = 24;
jack->_male = 1;
jack.height = 185;
NSLog(@"jack age is %d, male: %d, height: %f", jack->_age, jack->_male, jack.height);
NSLog(@"jack instance size: %zd", class_getInstanceSize([BFPerson class]));
NSLog(@"jack malloc size: %zd", malloc_size((__bridge const void *)jack));
BFProgrammer *tony = [[BFProgrammer alloc] init];
tony->_age = 28;
tony->_male = 1;
tony.height = 178;
tony->company = "Google";
NSLog(@"tony age is %d, male: %d, height: %f, company: %s", tony->_age, tony->_male, tony.height, tony->company);
NSLog(@"tony instance size:%zd", class_getInstanceSize([BFProgrammer class]));
NSLog(@"tony malloc size: %zd", malloc_size((__bridge const void *)tony));
复制代码
对应的结果:
下面咱们分析下结果:
对于tony这个程序员:
其成员变量大小为32字节,相对于jack这个BFPerson,多了8字节的内存变量。那么这8个字节,用于存放char *company的指针。
咱们如今更进一步,从内存中直接读取tony的公司名称:Google。
Google是个C语言字符串常量,其存储在内存中,采用ASCII字符编码,其最后的结构为:
从上面图中,咱们发现tony所在地址为0x103300700,根据其结构体,咱们能够知道company所在地址为:
compyan地址 = 0x103300700 + isa+ _age + _male + _height
= 0x103300700 + 8 + 4 + 4 + 8
= 0x103300718
复制代码
对应的指令以下:
(lldb) x/2wx 0x103300718
0x103300718: 0x00000f52 0x00000001
(lldb) x/4wx 0x0000000100000f52
0x100000f52: 0x676f6f47 0x7400656c 0x20796e6f 0x20656761
(lldb) x/4wx 0x103300700
0x103300700: 0x00001391 0x001d8001 0x0000001c 0x00000001
复制代码
Google
字符串,Google占7个字节,注意是7个字节,最后一个字符为'\0';下面咱们分析上面咱们经常使用的打印语句:
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
NSLog(@"%zu", malloc_size((__bridge const void *)(obj)));
复制代码
alloc
以及class_getInstanceSize
源码z在Apple souce objc4库中。
**[NSObject alloc]**代码调用流程:
instanceSize
获取的是alignedInstanceSize
。
alignedInstanceSize
小于16字节,会补齐为16字节。
alignedInstanceSize
大于16字节,直接返回alignedInstanceSize
能够看到,其中当上图最后一步中的alignedInstanceSize,即通过结构体字节对齐后字节仍小于16字节,就会补齐为16字节。
为何须要补齐16字节呢?
代码文档有一行注释:
CF requires all objects be at least 16 bytes.
或者咱们能够理解为OC对象为了提升系统分配及查找地址的效率,而作的一个这样的规定。
这也是上面第一个实例NSObject分析中,为何实例对象实际占用8字节,会分配16字节的缘由。alignedInstanceSize实际实际返回8字节,但calloc中的时候,size为16字节。
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
复制代码
能够看出,class_getInstanceSize
最后返回的就是上面所说的,通过字节对齐,可是在alloc
中calloc
以前的大小。
因此咱们看到其实际返回的是成员变量实际须要的空间大小。
calloc
代码在另一个库中——libmalloc。
其中源码比较难以理解,因此直接给出结论。
calloc
返回的是系统实际分配的内存,最后返回的大小必定是16的倍数;
因此BFPerson实例中,成员变量占24字节,但最后通过calloc返回的是32字节(16*2);
malloc_size返回的是对象指针所指向的大小,就是calloc实际分配的内存大小;
extern size_t malloc_size(const void *ptr);
/* Returns size of given ptr */
LLDB的使用请参考:待补--iOS调试(二)LLDB
Debug -> Debug Workfllow -> View Memory (Shift + Command + M)