Loading...
墨滴

weak_PG

2021/09/29  阅读:22  主题:默认主题

iOS底层原理探索之alloc底层探究

寻找alloc方法底层的三种方式

我们直接上代码看下,创建一个OC工程,在ViewController里面实例化一个Person类,探究alloc的底层调用可以结合三种方式 1.符号断点,按住control+Step into(图中框起来处)

此时出现了一个方法objc_alloc方法,这是加一个符号断点

continue program execution,此时可以得到一个函数_objc_rootAllocWithZone

2.直接看汇编,这种方式要比上面更常用一些 Debug->Debug Workflow->Always Show Disassembly直接定位到了libobjc.A.dylib``objc_alloc这里

3.直接看objc源码苹果开源源码汇总 本文所有的源码示例来源于objc4-818,直接在搜索框定位关键字,可以得到alloc的方法流程图

alloc的主线流程

字节对齐

我们进入_class_createInstanceFromZone方法内部一探究竟

可以看到Person在断点1处的时候已经开辟了内存,到断点3的时候已经看到地址已经与类绑定上了,那么内存是如何开辟出来的呢?开辟的大小是多少呢?带着这个疑问,我们重新查看这个方法,发现了一个跟size有关的方法cls->instanceSize,进入该方法得到一个结论:所有的对象最少为16字节

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

影响size的有一个函数alignedInstanceSize,这个是字节对齐的算法,那么这个算法怎么回事呢?

字节对齐原理

接着上面,我们看下alignedInstanceSize的实现

 // 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) {
    // define WORD_MASK 7UL
    return (x + WORD_MASK) & ~WORD_MASK;
}

参数x是unalignedInstanceSize()那么这个大小是多少呢,定位到这个方法,我们看到了一行注释,depending on class's ivars,而我们知道Person的ivars只有一个继承NSObject的isa指针

  // May be unaligned depending on class's ivars.
    uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }

所以最后的的算法其实是:(8+7)& ~7 ==> 15 & ~7 =8,即8字节对齐,向上取整。我们知道Person的大小是由成员变量来决定的,那么在开辟内存的时候,碰到Int,Char类型的时候,我们都要无差别的开辟8个字节吗,这样不是造成了内存浪费嘛

对象的内存空间

学习本段需要了解小技巧: x p: 查看当前p的所有内存情况 0x10060de10首地址 x/4gx: x-16进制格式显示变量,4-显示内存单元的个数,g-8字节,也就是4个内存单元按照16进制格式显示变量 define ISA_MASK 0x00007ffffffffff8ULL

首先,我们先来试验一下,看下对象的内存情况。我们在控制台x一下当前的对象,来验证下首地址isa是不是指向当前的类。由于iOS系统是小端模式,所以内存的读取从右向左反过来读取。 99 82 00 00 01 80 1d 01 --- > 0x011d800100008299(isa) 那么为什么没有直接打印出来Person呢,是因为在__x86_64__系统下,isa的真实值需要&(ISA_MASK)

此时新增name和age两个属性,使用x/4gx可以更直观的看出内存的占用情况

新增一个BOOL类型,我们知道name占用8字节,age占用4字节,bool占用1个字节,那么此时这个bool是否单独占用8个字节的空间呢,还是有些内存优化,我们来试一下就知道了!

我们发现,新增的bool属性跟age共享了一个八字节,此时得到结论,在类的存储过程中,确实用到了内存优化。

LLVM优化alloc

打开我们的源码,我们发现这里alloc方法和objc_alloc同时存在,断点调试我们发现在Person alloc的过程中,首先调用的是objc_alloc,这里的结果跟刚开始符号断点定位的一样,那么此时我们是否有一个疑问:为什么alloc的IMP指向的是objc_alloc而不是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 == @selector(alloc)) {
            // hook 到了objc_alloc
            msg->imp = (IMP)&objc_alloc;
        } 
        ....
    } 
}

苹果设计alloc方法的时候比较特殊,会做底层的LLVM的拦截,hook到objc_alloc,该方法callAlloc时再次发送alloc方法((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)),这也是为什么callAlloc方法断点的时候会走两次的原因。

对象内存对齐原则

在计算InstanceSize的时候我们发现了一段注释size depending on class's ivars,下面我们来验证一下:

我们可以在Person里面加上一个方法,运行之后size不变,问题:Person的各个属性的字节相加为22,8字节对齐之后就是24 ​

struct/class/union内存对齐原则有四个:

  1. 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节, 则要从4的整数倍地址开始存储),基本类型不包括struct/class/uinon。
struct LGStruct1 {
    double a;       // 8    [0 7]
    char b;         // 1    [8]  -- 8是1的整数倍
    int c;          // 4    (9 10 11 [12 13 14 15] --12是4的整数倍
    short d;        // 2    [16 17] -- 16是2的整数倍,该结构体最大字节为8,所以需要是8的倍数,17->24
}struct1;

struct LGStruct2 {
    double a;       // 8    [0 7]
    int b;          // 4    [8 9 10 11] -- 8是4的整数倍
    char c;         // 1    [12]    -- 12是1的整数倍
    short d;        // 2    (13 [14 15] 16  -- 14是2的整数倍    15->16
}struct2;
  1. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)。
struct LGStruct3 {
    double a;               // 8    [0 7]
    int b;                  // 4    [8 9 10 11]
    char c;                 // 1    [12]
    short d;                // 2    [14 15]
    int e;                  // 4    [16 17 18 19]   19-> 24
    struct LGStruct1 str;   //24     24 + 24 = 48
}struct3;
  1. 收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的"最宽基本类型成员"的整数倍.不足的要补齐.(基本类型不包括struct/class/uinon)。
  2. sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址。

补充

  1. #define fastpath(x) (__builtin_expect(bool(x), 1)): 表示 x 的值为真的可能性更大, __builtin_expect() 是 GCC (version >= 2.96)提供给程序员使用的,目的是将“分支转移”的信息提给编译器,这样编译器可以对代码进行优化,以减少指令跳转带来的性能下降

  2. x/4gx: x-16进制格式显示变量,4-显示内存单元的个数,g-8字节

  3. malloc_size((__bridgeconstvoid *)(p)) = 32, malloc_size:成员变量结构体内部8字节对齐 对象是16字节对齐

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
 }
    // (24 + 15) >> 4 << 4   16字节对齐 --->  24--> 32
 k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
 slot_bytes = k << SHIFT_NANO_QUANTUM;       // multiply by power of two quanta size
 *pKey = k - 1;             // Zero-based!

 return slot_bytes;
}

weak_PG

2021/09/29  阅读:22  主题:默认主题

作者介绍

weak_PG