OC底层原理 - alloc

295 阅读3分钟

探寻底层原理的三种方法

1.符号断点

先在创建对象方法处添加断点,当程序走到断点位置时,按住control键,点击step into,程序走到了objc_alloc。再依次添加objc_alloc_objc_rootAllocWithZone符号断点,查看调用过程

企业微信截图_86ee66ba-0e26-4d45-b2cb-792cf34f48ca.png

2.汇编

打开 Xcode -> Debug -> Debug Workflow -> Always Show Disassembly 开关, 断点会直接进入汇编,快速定位到objc_alloc方法,再结合符号断点调试

3.objc源码分析

查看苹果源码 Apple Open SourceSource Browser (在这两个网站中搜索objc即可)

企业微信截图_12c7f8f8-f231-44ff-9736-b76de2619804.png

下载下来的源码是无法直接编译的,这里有KC老师配置好的 可编译的objc源码 ,打开最新的objc4-818.2工程,运行 KCObjcBuid,轻松调试。

源码分析alloc方法调用过程

从创建对象的alloc点进去可以看到alloc方法的实现

id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

+ (id)alloc {
    return _objc_rootAlloc(self);
}

但是结合断点,发现其实是先调用了objc_alloc方法,再调用的alloc方法

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

而关键方法callAlloc调用了两次,第一次走的是objc_msgSend,第二次走_objc_rootAllocWithZone

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

注释:
slowpath(x):一般就是基本流程,不被优化的,执行效率会慢一些
fastpath(x):在编译器优化,更容易被执行

为何会这样呢,搜索源码发现,原来苹果在底层llvmallocallocWithZone、等关键方法进行了一次拦截处理。

static void 
fixupMessageRef(message_ref_t *msg)
{    
    msg->sel = sel_registerName((const char *)msg->sel);
    if (msg->imp == &objc_msgSend_fixup) { 
        if (msg->sel == @selector(alloc)) {
            msg->imp = (IMP)&objc_alloc;
        } else if (msg->sel == @selector(allocWithZone:)) {
            msg->imp = (IMP)&objc_allocWithZone;
        } else if (msg->sel == @selector(retain)) {
            msg->imp = (IMP)&objc_retain;
        } else if (msg->sel == @selector(release)) {
            msg->imp = (IMP)&objc_release;
        } else if (msg->sel == @selector(autorelease)) {
            msg->imp = (IMP)&objc_autorelease;
        } else {
            msg->imp = &objc_msgSend_fixedup;
        }
    } 
    ...
 }

注释:
sel:方法编号
imp:方法实现首地址

由此,可以总结出alloc底层调用过程

alloc流程-3.jpg

下面再来具体研究核心方法_class_createInstanceFromZone

_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);
}

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); //1.计算对象所需内存大小
    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); //2.申请开辟内存
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }
    if (!zone && fast) { 
        obj->initInstanceIsa(cls, hasCxxDtor);   //3.初始化isa,关联cls
    } 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);
}

重点来看 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;
}

可以看到,计算内存大小分为有无缓存两种情况,下面以无缓存情况为例,研究下计算内存大小的算法

uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

static inline uint32_t word_align(uint32_t x) { //WORD_MASK = 7
    return (x + WORD_MASK) & ~WORD_MASK;
}

// May be unaligned depending on class's ivars. //内存大小由ivars决定
uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
}
字节对齐算法
(x + WORD_MASK) & ~WORD_MASK -> 8+7 & ~7
0000 1111      15
0000 0111       7
1111 1000      ~7

0000 1000    15 & ~7  -> 8
相当于        15 >> 3 << 3

总结:对象内存大小由成员变量决定,以8字节对齐(取8的整数倍),最少为16字节

isa(结构体指针类型)占8字节

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

为什么最少是16
没有任何成员变量时,isa已经占了8字节,所以额外开辟空间,以做容错处理

为什么是8字节对齐?
8字节的对象是最多的,CPU以8字节固定长度读取,以空间换时间

BOOL、int、char 等存储8字节浪费,是否可以合并到一起来读取呢?
移步下一篇博客