预备知识
xcrun
一种从命令行调试或者定位 Xcode 的工具链。在命令行输入man xcrun
可以看完整文档。
它可以用来查询工具的路径,比如寻找 clang 的文件路径:xcrun -f clang
。
输入 xcrun --help
可以看到 xcrun 支持的全部命令:
Usage: xcrun [options] <tool name> ... arguments ...
Find and execute the named command line tool from the active developer
directory.
The active developer directory can be set using `xcode-select`, or via the
DEVELOPER_DIR environment variable. See the xcrun and xcode-select manual
pages for more information.
Options:
-h, --help show this help message and exit
--version show the xcrun version
-v, --verbose show verbose logging output
--sdk <sdk name> find the tool for the given SDK name
--toolchain <name> find the tool for the given toolchain
-l, --log show commands to be executed (with --run)
-f, --find only find and print the tool path
-r, --run find and execute the tool (the default behavior)
-n, --no-cache do not use the lookup cache
-k, --kill-cache invalidate all existing cache entries
--show-sdk-path show selected SDK install path
--show-sdk-version show selected SDK version
--show-sdk-build-version show selected SDK build version
--show-sdk-platform-path show selected SDK platform path
--show-sdk-platform-version show selected SDK platform version
复制代码
Clang
Clang是一个C++编写、基于LLVM发布于LLVM BSD许可证下的C/C++/Objective-C编译器。可以使用它来将 OC 代码编译成 C++ 代码。
clang -rewrite-objc main.m -o main.cpp
:将 main.m 编译成 main.cpp。
LLDB
LLDB 全称 Low Level Debugger,是一款轻量级的高性能调试器,默认内置于Xcode中。
常用命令 p、po。
更多的使用可参见这篇文章。
术语解释
- 大端模式:低位字节存放内存中的高地址端,高位字节存放内存中的低地址端;小端模式则低位字节存放内存中的低地址端,高位字节存放内存中的高地址端。
- 内存对齐:计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。
了解了上述工具及术语解释后,下面来看一下 NSObject 的底层是如何实现的。
NSObject 底层实现
首先在 main 函数中创建一个 NSObject 对象:NSObject *obj = [NSObject new];
,接下来通过 xcrun 和 clang 来编译成 C++ 代码。
xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
。
xcrun --sdk iphoneos 表示将 SDK 指定为 iphoneos;clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 表示指定 arm64 架构,将 main.m 的OC 代码编译成 C++代码并输出到 main-arm64.cpp 中。
在 main-arm64.cpp 中我们可以看到 NSObject 是由一个 NSObject_IMPL
的结构体实现:
struct NSObject_IMPL {
Class isa;
};
复制代码
通过上面的结构体可以看到 NSObject_IMPL 中只包含一个 Class 类型的 isa 字段。通过代码中可查询得知 Class 是一个指针类型:
typedef struct objc_class *Class;
复制代码
NSObject 只包含一个指针类型的 isa 字段,所以它实际需要的内存是 8 bytes, 那 obj 系统分配的内存是 8 bytes 吗?它实际占用的内存又是多少呢?
在解答该问题前先明确两个概念:实际需要内存和系统分配内存。实际需要内存指的是对象所需要的内存,而系统分配的内存则值得是系统分配给该对象的内存,系统分配内存 >= 实际需要内存。
这就好比高铁的容客量(系统分配内存)和实际乘客量(实际需要内存)。假设今天高铁只有 64 个乘客,但高铁上的座位该是多少还是多少,它不会因为乘客的变化而变化。
首先我们通过malloc_size()
函数来看一下 obj 分配了多少内存空间:
#import <malloc/malloc.h>
NSObject *obj = [NSObject new];
NSLog(@"%zd", malloc_size(( **__bridge** **const** **void** *)(obj))); // print 16
复制代码
通过打印可以得知 obj 虽然只需要 8 bytes,但系统还是分配了 16 bytes 给它。
系统分配了 16 bytes ,那 obj 到底使用了多少字节呢?我们可以通过 size_t class_getInstanceSize(Class cls);
来得知,
#import <objc/runtime.h>
NSLog(@"%zd", class_getInstanceSize([obj class])); // print 8
复制代码
由打印可以看出,obj 使用的和它实际需要的都是 8 bytes ,那为什么系统会给它分配 16 bytes呢?带着这个疑问来查看一下 objc4 的源码版本:objc4-818.2。
allocWithZone:
上图可以看出 alloc 底层调用的 allocWithZone:
,打开下载的 objc4 源码可以在 NSObject.mm 文件找到其实现:
- allocWithZone: 代码实现:
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
复制代码
- _objc_rootAllocWithZone 代码实现:
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused) {
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
复制代码
- _class_createInstanceFromZone 代码实现:
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);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
复制代码
通过上述的 size = cls->instanceSize(extraBytes);
这句代码可以看出 size 取的是 instanceSize 函数的值。
- instanceSize 代码实现
inline 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;
}
复制代码
上述代码 if (size < 16) size = 16;
可以得知对象内存占用最小为 16 bytes。该代码上面的注释也写得很清楚,CF 要求所有的对象内存最小为 16 bytes。
因此,虽然 obj 实际需要的是 8 bytes,但系统会分配给它 16 bytes。
下图为 allocWithZone 调用流程:
分析实际中的例子
下述代码中 good 的实际占用内存和系统分配内存分别为多少?
@interface Goods : NSObject
{
int _count;
NSString *_name;
}
@end
Goods *good = [Goods alloc];
NSLog(@"%zd", malloc_size((__bridge const void *)(good)));
NSLog(@"%zd", class_getInstanceSize([good class]));
复制代码
计算一下可以得出 good 各字段需要的内存:isa(8) + _count(4) + _name(8) = 20 bytes。按道理讲 class_getInstanceSize 返回的应该是 20 bytes,但实际上确实 24 bytes,这是为什么呢?这是因为内存对齐的原因。
结构体的内存为各字段占内存最大的倍数,而 good 的 isa 和 name 都是 8 bytes,所以它应该是 8 的倍数 24 bytes。
内存对齐
打开项目搜索 class_getInstanceSize
,可以在 objc-class.mm 文件中找到其实现:
size_t class_getInstanceSize(Class cls) {
if (!cls) return 0;
return cls->alignedInstanceSize();
}
复制代码
通过观看 alignedInstanceSize 函数的名字也可以看出,它的作用就是用来内存对齐的。
alignedInstanceSize
代码实现:
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
复制代码
通过 unalignedInstanceSize 获取未对齐的大小,通过 word_align 函数内存对齐。
unalignedInstanceSize
代码实现:
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
复制代码
word_align
代码实现:
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
static inline size_t word_align(size_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
复制代码
假设 x 为上述计算的 20 bytes,我们来推导一下 24 bytes 是如何计算出来的:
1)(x + WORD_MASK):20 + 7 = 27(二进制:0001 1011)
2)~WORD_MASK:对 7 进行取反为 1111 1000
3)1111 1000 & 0001 1011 = 0001 1000(24)
计算过程:
如果 x 为 24 bytes 的话,计算出来的结果还是 24 bytes,感兴趣的小伙伴可以自己计算一下。
- class_getInstanceSize 调用流程图:
那 malloc_size 返回的是多少呢?答案是 32 bytes。具体原因是 Apple 系统中的 malloc 函数分配内存空间时,内存是根据一个 bucket 的大小来分配的。bucket的大小是16,32,48,64,80 等,可以看出系统是按16的倍数来分配对象的内存大小的。
64 位编译器下基本类型所占字节数
char: 1个字节
char*(即指针变量): 8个字节
short int: 2个字节
int: 4个字节
unsigned int: 4个字节
long: 8个字节
long long: 8个字节
unsigned long long: 8个字节
float: 4个字节
double: 8个字节
复制代码
总结
- NSObject 的底层由 NSObject_IMPL 实现。
- 对象实际占用的内存与系统分配的内存并不总是一致的。
- 内存对齐。