Objc4-818底层探索(一):alloc

首先先看个例子:

    TestObj *obj1 = [TestObj alloc];
    TestObj *obj2 = [obj1 init];
    TestObj *obj3 = [obj1 init];
       
    NSLog(@"%@ - %p - %p", obj1, obj1, &obj1);
    NSLog(@"%@ - %p - %p", obj2, obj2, &obj2);
    NSLog(@"%@ - %p - %p", obj3, obj3, &obj3);

他们的打印情况为

问题8-答案

首先了解下, 三个打印依次是打印 对象内容, 对象指针指向的内存地址, 指针地址
%@: 打印的是对象的内容
%p → &p1: 打印的是指向对象内存的指针地址
%p → p1: 打印的是指针地址

那么

  • 第一列: obj1, ob2, obj3 相等, 打印的是对象内容都是TestObj开辟的内存空间

  • 第二列: obj1, obj2, obj3 相等, 留意下init是不会对指针进行操作的, 而%p打印的是对象指针, obj1、obj2、 obj3, 三个都是指向同一片内存区域( [TestObj alloc]开辟的), 所以不变

  • 第三列: &obj1, &ob2, &obj3 不相等, 打印的是指针地址, obj1、obj2、 obj3三个不同指针, 地址不同

指向图

(例子详细内容可看 IOS面试题 --- 类相关中的问题8)

其实这里面就引出一个问题? alloc 究竟做了什么?

alloc探索

通常我们对alloc了解是, 它做了初始化, 开辟了一块内存空间, 那它底层究竟怎么做的呢? 首先我们需要一份objc-818源码:
编译好的objc4-818源码下载: https://github.com/Lv100-ShawnAlex/objc4-818.git
源码探索常用方法: https://www.jianshu.com/p/63988f940c90

源码有了, 先建立一个SATest类继承NSObject, 并做alloc初始化

建立一个SATest类

cmd + 点击查看alloc源码

SATest点击查看源码

依次点击进入

+ (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)
{
// OBJC有1.0版本2.0版本, 现在我们大部门都用2.0版本
#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);
    }
    // 发送 alloc 消息
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

走到这里是时候, 其实就会很迷茫, 因为我们不知道究竟走了哪个判断?那么可以加个断点运行一下

源码加断点运行

这块留意下因为系统很多对象alloc方法, 都会走这里, 所以要确保是我们建的类SATest进入

错误的例子
错误的例子
正确的例子

最好main中的对象先加断点, 等main中断点确定走到之后再加/重新打开对应方法的断点


main先加断点
正确的例子

po: lldb命令读出/输出值, 打印对象




当我们在这边打断点跟流程的时候, 可看到, 流程先走了return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));, 然后又来一遍走了
if (fastpath(!cls->ISA()->hasCustomAWZ()))这个判断里面的return _objc_rootAllocWithZone(cls, nil);

为什么会走2次呢? 我们这里可以打开汇编(Debug→Debug Workflow → Always Show Disassembly)看一下

打开汇编

走了objc_alloc

可看到, 他并没有走alloc, 而走了objc_alloc, 这个objc_alloc是什么呢? 我们先看下源码, 在NSObjec.mm中可以找到调用了callAlloc方法

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

那么我们 cmd + 点击进入的方法走没走呢?

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

}

我们依次(id)objc_alloc(Class cls), + (id)alloc, callAlloc(Class cls, bool checkNil, bool allocWithZone=false)分别加个断点, 判断下是这里代码走入情况

objc_alloc断点

alloc断点
callAlloc断点

可以看到依次走入顺序

objc_alloccallAllocobjc_msgSendalloc_objc_rootAlloccallAlloc_objc_rootAllocWithZone

alloc流程图

接下来介绍下slowpathfastpath(后面也会用到)

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

其中__builtin_expect指令是由编译器gcc引入的。 __builtin_expect(EXP, N), 表示 EXP==N的概率很大。
目的 : 编译器可以对代码进行优化, 以减少指令跳转带来的性能下降。(性能优化)
作用 : 允许程序员将最有可能执行的分支告诉编译器
写法 : __builtin_expect(EXP, N)。表示 EXP==N 的概率很大。

  • fastpath: __builtin_expect(bool(x), 1)。表示x值为真的概率很大如果方法放在if判断中, 执行真的if的几率很大。

  • slowpath: __builtin_expect(bool(x), 0)。表示x值为假的概率很大如果方法放在if判断中, 执行假的else的几率很大

在日常的开发中, 也可以通过设置来优化编译器 , 达到性能优化的目的, 设置位置 Build SettingsOptimization Level

Optimization Level



看下这里走2次原因, 涉及知识点比较多, 此处仅仅是探索猜测, 后续补完 (这块我也是在探索, 可直接跳到下面alloc 三大核心)

fastpath(!cls->ISA()->hasCustomAWZ()
    bool hasCustomAWZ() const {
        return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
    }
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
// 类或父类有默认 alloc/allocWithZone 实现
// 注意,它存储在元类中。
#define FAST_CACHE_HAS_DEFAULT_AWZ    (1<<14)

SATest 继承NSObject, NSObject有个默认的isa, 但此时isa并没有做任何初始化(objc_object::initIsa), 也可以理解成空白isa

  • 系统默认走了(这块需要看下llmv)objc_alloccallAlloc

  • 第一次进入:

    • cls有值if (slowpath(checkNil && !cls)) return nil;这个判断不会走

    • fastpath(!cls->ISA()->hasCustomAWZ(), 因为这个值FAST_CACHE_HAS_DEFAULT_AWZ存储在元类中, 此时父类链, 元类都没关联确认, 所以 cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ)false, !一下为true, 外层又!了一下还是为false, 所以首次不走这里。

    • allocWithZone传入的也是false, 所以走消息转发((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc))

  • 消息转发:
    • 快速查找: cache中并没有传入类的alloc方法, 所以直接进慢速查找流程
    • 慢速查找: 快速查找没有找到走 lookUpImpOrForward 慢速查找流程
      • 跟流程走这里 cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);

这块跟一下 realizeAndInitializeIfNeeded_locked源码

/***********************************************************************
* realizeAndInitializeIfNeeded_locked
* Realize the given class if not already realized, and initialize it if
* not already initialized.
* inst is an instance of cls or a subclass, or nil if none is known.
* cls is the class to initialize and realize.
* initializer is true to initialize the class, false to skip initialization.
**********************************************************************/
static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
{
    runtimeLock.assertLocked();
    // 判断当前类是否实现
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }
   
    // 判断当前类是否初始化
    if (slowpath(initialize && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // If sel == initialize, class_initialize will send +initialize and
        // then the messenger will send +initialize again after this
        // procedure finishes. Of course, if this is not being called
        // from the messenger then it won't happen. 2778172
    }
    return cls;
}
// Locking: caller must hold runtimeLock; this may drop and re-acquire it
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
    return initializeAndMaybeRelock(cls, obj, lock, true);
}

/***********************************************************************
* class_initialize.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
* inst is an instance of cls, or nil. Non-nil is better for performance.
* Returns the class pointer. If the class was unrealized then 
* it may be reallocated.
* Locking: 
*   runtimeLock must be held by the caller
*   This function may drop the lock.
*   On exit the lock is re-acquired or dropped as requested by leaveLocked.
**********************************************************************/
static Class initializeAndMaybeRelock(Class cls, id inst,
                                      mutex_t& lock, bool leaveLocked)
{
    lock.assertLocked();
    ASSERT(cls->isRealized());

    if (cls->isInitialized()) {
        if (!leaveLocked) lock.unlock();
        return cls;
    }

    // Find the non-meta class for cls, if it is not already one.
    // The +initialize message is sent to the non-meta class object.
    Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);

    // Realize the non-meta class if necessary.
    if (nonmeta->isRealized()) {
        // nonmeta is cls, which was already realized
        // OR nonmeta is distinct, but is already realized
        // - nothing else to do
        lock.unlock();
    } else {
        nonmeta = realizeClassMaybeSwiftAndUnlock(nonmeta, lock);
        // runtimeLock is now unlocked
        // fixme Swift can't relocate the class today,
        // but someday it will:
        cls = object_getClass(nonmeta);
    }

    // runtimeLock is now unlocked, for +initialize dispatch
    ASSERT(nonmeta->isRealized());
    initializeNonMetaClass(nonmeta);

    if (leaveLocked) runtimeLock.lock();
    return cls;
}

继续跟源码我们看到走了这里


realizeClassMaybeSwiftAndUnlock
static Class
realizeClassMaybeSwiftAndUnlock(Class cls, mutex_t& lock)
{
    return realizeClassMaybeSwiftMaybeRelock(cls, lock, false);
}

/***********************************************************************
* realizeClassMaybeSwift (MaybeRelock / AndUnlock / AndLeaveLocked)
* Realize a class that might be a Swift class.
* Returns the real class structure for the class. 
* Locking: 
*   runtimeLock must be held on entry
*   runtimeLock may be dropped during execution
*   ...AndUnlock function leaves runtimeLock unlocked on exit
*   ...AndLeaveLocked re-acquires runtimeLock if it was dropped
* This complication avoids repeated lock transitions in some cases.
**********************************************************************/
static Class
realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
{
    lock.assertLocked();

    if (!cls->isSwiftStable_ButAllowLegacyForNow()) {
        // Non-Swift class. Realize it now with the lock still held.
        // fixme wrong in the future for objc subclasses of swift classes
        realizeClassWithoutSwift(cls, nil);
        if (!leaveLocked) lock.unlock();
    } else {
        // Swift class. We need to drop locks and call the Swift
        // runtime to initialize it.
        lock.unlock();
        cls = realizeSwiftClass(cls);
        ASSERT(cls->isRealized());    // callback must have provoked realization
        if (leaveLocked) lock.lock();
    }

    return cls;
}

这里走了 if (!cls->isSwiftStable_ButAllowLegacyForNow())判断进入realizeClassWithoutSwift(cls, nil);

这里比较长, 我就截取部分图片, 可看到处理了一些rw, ro信息


处理rw, ro信息

往下可看到进入了这里针对于父类链, 元类做一些判断处理

处理父类链/元类

继续往下, 可看到进入这里initClassIsa

image.png

inline void 
objc_object::initClassIsa(Class cls)
{
    if (DisableNonpointerIsa  ||  cls->instancesRequireRawIsa()) {
        initIsa(cls, false/*not nonpointer*/, false);
    } else {
        initIsa(cls, true/*nonpointer*/, false);
    }
}
initisa

之后进入isa的核心代码initisa, 这里主要是对isa做一些操作处理

initisa

可看到这里的确对isa做了一些字段赋值

打印isa信息

之后进入的object_getClass

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

inline Class
objc_object::getIsa() 
{
    if (fastpath(!isTaggedPointer())) return ISA();

    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    cls = objc_tag_classes[slot];
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}

因为isTaggedPointer此时为0, 所以!为真, 直接走return ISA(), 后续把一些剩余代码走完, 然后走第二次进入cAlloc

return ISA()

  • 第二次进入:

第二次的calloc来自于alloc而不是objc_alloc, 由于消息发送的是alloc方法

  • cls有值if (slowpath(checkNil && !cls)) return nil;这个判断不会走

  • fastpath(!cls->ISA()->hasCustomAWZ(), 元类, 父类链确认完成, ISA中信息已更改所以FAST_CACHE_HAS_DEFAULT_AWZ cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ)true, !一下为false, 外层又!了一下还是为true, 第二次走了这里

两次打印isa bits也会发现bits不一样


两次打印isa

(此处仅仅是探索猜测, 后续补完)



alloc 三大核心

我们接着跟下源码

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

alloc核心代码_class_createInstanceFromZone

/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
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
    // hasCxxCtor: C++/OC的析构器(类似于dealloc)占1位
    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);
}

alloc核心代码: instanceSize:计算开辟内存大小

我们也是_class_createInstanceFromZone里打断点, 看一下源码流程

_class_createInstanceFromZone
  1. 第一步肯定是留意一下是否是我们定义的类进入

2.开始定义一些是否包含析构的 bool
hasCxxCtor: C++/OC的析构器(类似于dealloc)占1位

  • 第一个bool hasCxxCtorfalse: cxxConstruct 为传入进来的为true, cls->hasCxxCtor()是否有析构器false, 然后 & 一下为false

  • 第二个bool hasCxxDtorfalse:cls->hasCxxCtor()同上为false

  • 第三个bool fast:falsecls->canAllocNonpointer()判断当前类是否已经关联isa, 为此并没有 false

3.定义size主要用于储存后面的开辟空间大小值

instanceSize
  1. 核心代码之一: size = cls->instanceSize(extraBytes);, 计算开辟内存空间大小并赋给size(extraBytes: 是否需要额外空间大小, 为0)

首先我们了解下内存大小是由属性/成员变量决定, 这里可由data()->ro()->instanceSize源码知道

// May be unaligned depending on class's ivars.

    uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        // 其中 instanceSize 是实例变量大小, 并且是编译完的干净内存大小
        return data()->ro()->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.
        // CF要求所有对象至少为16字节。不足16补齐16
        if (size < 16) size = 16;
        return size;
    }

依次跟断点进入走了align16方法

传入参数 size = 16, extra == 0, #define FAST_CACHE_ALLOC_DELTA16 0x0008 = 8,

align16

其中__builtin_constant_pgcc的内建函数 用于判断一个值是否为编译时常数,如果参数的值是常数,函数返回 1,否则返回 0, 这里很显然传入的是个变量所以走了下面的判断。(如果会走这里的话, 只能是 extra != 0)

_flags
#define FAST_CACHE_ALLOC_MASK16       0x1ff0 = 8176 = 0001 1111 1111 0000

_flags系统会计算多次, 最后为32784 = 1000 0000 0001 0000
两种做下 &运算

0001 1111 1111 0000
&
1000 0000 0001 0000
=
0000 0000 0001 0000 = 16

cache.fastInstanceSize(extraBytes)往下走, 这里align16是一个向上取整的16字节对齐方法

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

~: 非操作, 0变1, 1变0
&: 与操作, 1 & 0 = 0; 1&1 = 1; 0&0 = 0

这个方法核心在于转成二进制按位与

例子1: x = 16 , 16二进制为 0001 0000, 15 二进制为0000 1111, ~15为1111 0000
16 + 15 = 31 = 0001 1111
那么有

0001 1111
&
1111 0000
=
0001 0000 = 16

例子2: x = 7 , 二进制为 0000 0111, 15 二进制为0000 1111, ~15为1111 0000
7 + 15 = 22 = 0001 0110
那么有

0001 0110
&
1111 0000
=
0001 0000 = 16, 不足16倍数的向上取整取到16倍数为止, 当然size_t(7)为8字节对齐。

所以上面传入16 + 0 - 8 = 8 计算完size = 16 返回, 继续往下走可看到走了第二个核心代码calloc

calloc

当然我们也看一下如果计算空间大小没走, 后面方法怎么走的

    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;
    }
// Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }
#   define WORD_MASK 7UL

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

这里可看出是个8字节对齐。这块我的理解是, obj对象实际占用是按8字节对齐,但之后系统还是会以16对齐分配大小,而快速创建一步到位, 直接按16字节对齐分配, 减少后面系统判断处理。

calloc我们放在下一篇详细讲, 总结下instanceSize流程图

instanceSize




为什么需要8字节/16字节存储, 直接按本身字节数存不就好了吗? 例如内存条这样存放

错误示范

存完之后读取, 假如第一个是4字节, 第二个是8字节, 第三个是8字节, 第四个是4字节...
当CPU开始读数据时候读完第一个4字节, 立马要变换读取长度8字节, 当读到第四个时候又要变化读取长度4字节读取, 这样每次都要做判断改变读取长度, 繁琐且会影响CPU速度。CPU意思: 大哥你别这样存了, 我难受!!!

因为通常存储指针特别多, 即8字节比较多, 就规定存储时候都是8字节为一段进行存储, 不足也为8, 以空间换取时间

接下来, 我们验证下是否真的是按我们想象的一样8字节对齐

不同类型字节占用

建立一个SATest类继承于NSObject, 并建立一些属性

@interface SATest : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, strong) NSString *hobby;
#import 
#import 
#import 
#import "SATest.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        SATest *test = [[SATest alloc] init];
        test.name = @"ShawnAlex";
        test.age = 18;
        test.height = 180;
        test.hobby = @"女";
        
        NSLog(@"%p", test);
    }
    return 0;
}

先介绍下我下面用到的一些lldb命令

  • x: 读取内存段
  • po: 读取一下内容
  • x/4gx: 以4个片段打印内存段
  • x/5gx: 以5个片段打印内存段
读内存段

可看到依次读出相应数据, 那么为什么没有hobby呢?

因为4gx只会按4个片段读取, 第五个需要5gx(大于4都可)

我们再增加一个bool b, 重新读取一下, 可看到还是5个片段

新增bool

那么bool去哪了呢?

B100BFC2-F629-4CE0-8D2F-B61E7084BB9D.png

其实这里, 系统帮我们优化了一下, 将intbool放在一起, 减少浪费



总结:

alloc流程图

alloc开辟内存方法

  • 底层顺序 objc_alloccallAllocobjc_msgSendalloc_objc_rootAlloccallAlloc_objc_rootAllocWithZone_class_createInstanceFromZone

  • _class_createInstanceFromZonealloc的核心代码

    • instanceSize: 计算开辟空间大小
    • calloc: 开辟内存
    • initInstanceIsa: 对象与指针isa绑定

你可能感兴趣的:(Objc4-818底层探索(一):alloc)