研究OC底层原理,就应该从最基本和最熟悉的开始,那就是对象的创建alloc
底层实现。本文就我自己探索和学习到的alloc
实现进行总结,有问题请指出,大家一起交流探索。
几种源码探索方法(建议真机环境)
- 打断点,control + ↓
- 打符号断点(例如
alloc
) - 显示汇编语言,Debug -> Debug Workflow -> Always Show Disassembly
- 下载源码探索(本文源码下载地址objc-750/objc-756.2和配置教程)
以上方法前三种方法可以探索出简单的流程方法顺序,只有下载源码才能看到流程方法的具体实现。探索起来虽说比较枯燥无味,但是只要坚持下去,对于自己技术帮助还是很大的,当你探索出结果也会有很大的成就感。
alloc流程源码探索
流程
第一步
+ (id)alloc {
return _objc_rootAlloc(self);
}
第二步
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
第三步
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
//cls为空
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
//看下边官方注释 -->判断是否自定义alloc/allocWithZone,并且不是继承于NSObject/NSProxy
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
//canAllocFast通过点进去可以得出在arm64系统下,canAllocFast一直返回false
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
//第三步之后,进入这里
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
//此处打断点,输出一下obj,发现是LGTeacher
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
-
hasCustomAWZ()
是用来判断是否有alloc/allocWithZone
的imp
,以及是否有自定义allocWithZone
。 -
canAllocFast()
通过点进去可以得出在arm64系统下,一直返回false。 -
hasCxxDtor()
是否有c++
析构函数。
在if (slowpath(!obj)) return callBadAllocHandler(cls);
打断点,输出obj
对象,说明追踪正确,如下图:
第四步
id
class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
第五步
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
//cls为空
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
//判断class or superclass 是否有 .cxx_construct 方法实现
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
//判断是否需要优化的isa
bool fast = cls->canAllocNonpointer();
//instanceSize计算需要为对象开辟的空间
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
//第四步之后,进入这里
//calloc是开辟内存空间
obj = (id)calloc(1, size);
if (!obj) return nil;
//isa初始化
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.
//isa初始化
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
-
canAllocNonpointer()
判断是否需要优化的isa。 -
instanceSize(extraBytes)
计算需要开辟的空间大小,extraBytes
等于0。 -
calloc
->malloc_zone_calloc
开辟内存空间。
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
//对象内存地址至少分配16字节
//一个对象内存都会分配16字节,实际在64位系统下,只使用了8字节;32位系统下,只使用了4字节
if (size < 16) size = 16;
return size;
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
uint32_t unalignedInstanceSize() {
assert(isRealized());
return data()->ro->instanceSize;
}
-
if (size < 16) size = 16;
对象内存地址至少分配16字节。一个对象内存都会分配16字节,实际在64位系统下,只使用了8字节;32位系统下,只使用了4字节。 - 此处
word_align()
方法用到了一个内存的8字节对齐计算,保证返回的size大小是8的倍数。WORD_MASK
在arm64系统下等于7。假如传入word_align()
的x = 10,计算过程:
x = 10 ; x + WORD_MASK = 17
0000 0111 //WORD_MASK二进制(4 + 2 + 1)1111 1000 //~WORD_MASK二进制
0001 0001 //17的二进制:(16 + 1)
0001 0000 //(x + WORD_MASK) & ~WORD_MASK 二进制(16)
所以return 16; 8的倍数
calloc开辟内存空间
这里探索需要用到源码libmalloc
源码,打开源码直接调用calloc
方法,得出如下结果,接下来就一步一步探索下去,看看如何得出此结果。
calloc
方法
void *
calloc(size_t num_items, size_t size)
{
void *retval;
retval = malloc_zone_calloc(default_zone, num_items, size);
if (retval == NULL) {
errno = ENOMEM;
}
return retval;
}
进入malloc_zone_calloc
方法
void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
//malloc追踪,DBG_FUNC_START 开始
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
//malloc检查
if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
internal_check();
}
//调用calloc
ptr = zone->calloc(zone, num_items, size);
//malloc 记录器 三种类型
if (malloc_logger) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone, (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}
//malloc追踪,DBG_FUNC_END 结束
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
return ptr;
}
通过这段代码可以看出系统在这里开启了malloc
追踪、malloc
监测、malloc
记录(看我注释)。真正执行开辟内存的是ptr = zone->calloc(zone, num_items, size);
这句代码。zone
通过->
调用calloc
,那么和属性一样我们可以打印zone->calloc
,结果如下:
default_zone_calloc
,找到default_zone_calloc
的实现,如下:
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
zone = runtime_default_zone();
return zone->calloc(zone, num_items, size);
}
又碰到了zone->calloc
(头大),打断点正好还能走到这里,说明探索的过程是正确的。没办法接着打印zone->calloc
,得到nano_calloc
,搜到nano_calloc
的实现如下:
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
//异常情况
return NULL;
}
if (total_bytes <= NANO_MAX_SIZE) {
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
//异常情况-->失败会走helper zone
}
}
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}
可以看出没有异常情况,会进入_nano_malloc_check_clear
方法,打断点进入。
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
void *ptr;
size_t slot_key;
//加盐处理,size在此处使用
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
mag_index_t mag_index = nano_mag_index(nanozone);
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
if (ptr) {
//都是一些异常情况处理,代码没有粘贴
} else {
//正常情况
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
}
if (cleared_requested && ptr) {
memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
}
return ptr;
}
到了此处确实不知该如何进行下去(一脸懵逼)。我们既然要探索size
是如何开辟到内存的,那我们就跟踪size
,进入segregated_size_to_fit
方法。(我不会告诉你这都是老师教的)
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
//40 + 16 - 1 >> 4
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
// << 4
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
slot_bytes = k << SHIFT_NANO_QUANTUM;
这句代码翻译出来就是
k = (40 + 16 - 1)>>4 //右移4位
slot_bytes = 55 << 4 //再左移4位
我们试着计算一下。
k = 40 + 16 - 1 = 55
0011 0111 //k的二进制
右移4位,空的补0(>>4)
0000 0011
左移4位,空的补0(<<4)
0011 0000 //正好等于48
这里其实运用了16字节内存对齐,和上边的8字节对齐有着异曲同工之妙(把~WORD_MASK
换成>>3、<<3也可以实现8字节对齐)。上边的8字节对齐是为了保证每个对象占用8字节内存(64位),16字节对齐则是保证了每个对象开辟了16字节的内存。关于对象占用内存和开辟内存的关系可以看下 Cocoi老师的文章。
进行到这里我们大致也就验证出来了,那些没看懂可以以后慢慢探索,现在我们至少了解了对象占用内存和开辟内存苹果底层都是怎么实现的。
objc_alloc的探索
其实使用探索方法1和3的时候,我们都看到了objc_alloc
这个方法,那这个方法到底是干什么的呢。
全局搜索objc_alloc
,可以找到两个方法。
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
// Calls [cls allocWithZone:nil].
id
objc_allocWithZone(Class cls)
{
return callAlloc(cls, true/*checkNil*/, true/*allocWithZone*/);
}
可以看出objc_alloc
也会调用callAlloc
,只不过和_objc_rootAlloc
调用传入的参数不同。在objc_alloc
打断点调试得出objc_alloc
在_objc_rootAlloc
之前执行,进入callAlloc
执行[cls alloc]
,会调起alloc
。然后才是上边总结的流程。
那么objc_alloc
做了什么事情?全局搜索objc_alloc
,可以看到一个调用objc_alloc
的地方,方法如下:
static void
fixupMessageRef(message_ref_t *msg)
{
msg->sel = sel_registerName((const char *)msg->sel);
if (msg->imp == &objc_msgSend_fixup) {
if (msg->sel == SEL_alloc) {
msg->imp = (IMP)&objc_alloc;
} else if (msg->sel == SEL_allocWithZone) {
msg->imp = (IMP)&objc_allocWithZone;
} else if (msg->sel == SEL_retain) {
msg->imp = (IMP)&objc_retain;
} else if (msg->sel == SEL_release) {
msg->imp = (IMP)&objc_release;
} else if (msg->sel == SEL_autorelease) {
msg->imp = (IMP)&objc_autorelease;
} else {
msg->imp = &objc_msgSend_fixedup;
//下边代码省略
此处判断sel
如果是SEL_alloc
(即alloc
)就把objc_alloc
绑定为他的IMP。
我们接着寻找fixupMessageRef
方法,结果如下:
_read_images
是系统读取镜像文件,就是dyld进行符号绑定的时候,由此我们可以猜想msg->imp = (IMP)&objc_alloc;
就是一个符号绑定的过程。那我们验证一下,编译一下工程,把目录Products下的可执行文件用MachOView(链接:https://pan.baidu.com/s/10k5BFJueUpz_ccBS54DQHQ 密码:eadw)打开,可以看到下图:至此也就验证了我们的猜想。
init&new探索
init
init
流程方法
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.(事实上,很难依靠此方法)
// Many classes do not properly chain -init calls.(很多类并未实现此方法)
return obj;
}
可以看出init
源码直接返回obj
,其实init
方法更多的是为了开发者自定义类使用的,在实际中我们自定义类都会重写init,把自定义的内容写在init
方法内,这个时候init
才真正发挥了作用。创建对象时调用init
,是为了防止有自定义内容。
new
new
流程方法
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
new
方法是调用了callAlloc
&init
方法,其实和alloc
&init
是一样的。
总结
alloc流程图努力就有收获,想要在开发的路上走远,就要不断地更新自己的知识库!