iOS对象的底层探索(上)

679 阅读5分钟

每天开发最常敲的代码就是alloc、init来进行对象的创建,那对象到底是怎么创建的呢?今天开始,研究底层第一步,探索一下alloc在底层的具体步骤。

创建一个demo

    WTPerson *p1 = [WTPerson alloc];
    WTPerson *p2 = [p1 init];
    WTPerson *p3 = [p1 init];
        
    NSLog(@"%@-%p-%p",p1,p1,&p1);
    NSLog(@"%@-%p-%p",p2,p2,&p2);
    NSLog(@"%@-%p-%p",p3,p3,&p3);

代码运行结果如下:

   Demo[16164:372529] <WTPerson: 0x600001160160>-0x600001160160-0x7ffee05391b8
   Demo[16164:372529] <WTPerson: 0x600001160160>-0x600001160160-0x7ffee05391b0
   Demo[16164:372529] <WTPerson: 0x600001160160>-0x600001160160-0x7ffee05391a8

可以看出三个对象指向的内存地址是一样的,但是指针地址是不同的,我们可以得出是alloc开辟的内存空间,init只是实例化了一个内存指针指向该内存空间。

最简单的探索方法就是在alloc之前打个断点,使用xcode中的debug - debug workflow - Always Show Disassembly,查看断点后的汇编窗口,发现汇编中的下一个方法不是alloc,而是objc_alloc。 alloc1@2x.png 使用symbolic breakpoint断点objc_alloc,发现其调用的是_objc_rootAllocWithZone, 同时下面使用objc_msgSend发一个alloc消息。 alloc2.png 断点_objc_rootAllocWithZone,其内部调用了calloc, 继续后并没有运行init,而是又调用了一次_objc_rootAllocWithZone->calloc,此时calloc内部调用_malloc_zone_calloc,由此可知alloc内部其实调用了两次calloc,第二次调用的时候,系统才进行对象的创建和内存的分配。 all3.png

all4.png 断点调试只能获取这些信息,想要进一步进行了解,需要使用iOS的底层源码传送门, 在开源源码中全局搜索objc_alloc或者 alloc { 进行查看源码。 在objc_runtime_new中fixupMessageRef这个方法内有一行代码 if (msg->sel == @selector(alloc)) { msg->imp = (IMP)&objc_alloc; } ,这行代码说明了为什么我们的alloc在断点的时候是objc_alloc, 在NSObject.m中找到alloc方法,一路跳转到callAlloc,最终跳转到的是objc_runtime_new中_class_createInstanceFromZone,在这个方法中进行了内存的分配。 12222.png 其中instanceSize()获取类的大小(参数中是传入额外字节的大小),其使用的所有代码调用如下

#ifdef __LP64__
#define WORD_SHIFT 3UL
#define WORD_MASK 7UL
#define WORD_BITS 64
#else
#define WORD_SHIFT 2UL
#define WORD_MASK 3UL
#define WORD_BITS 32
#endif
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t align16(size_t x) {
    returnx +size_t(15)) & ~size_t(15);
}
uint32_t unalignedInstanceSize() **const** {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
}
uint32_t alignedInstanceSize() **const** {
    return word_align(unalignedInstanceSize());
}
inline size_t instanceSize(size_textraBytes) const {
    if(fastpath(cache.hasFastInstanceSize(extraBytes))) {
        return cache.fastInstanceSize(extraBytes);
    }
    size_t size =alignedInstanceSize() + extraBytes;
    if(size <16) size =16;
    return size;
}
#if FAST_CACHE_ALLOC_MASK
#define FAST_CACHE_ALLOC_MASK        0x1ff8
#define FAST_CACHE_ALLOC_MASK16      0x1ff0
#define FAST_CACHE_ALLOC_DELTA16      0x0008
bool hasFastInstanceSize(size_t extra) const {
    if(__builtin_constant_p(extra) && extra ==0) {
        return _flags &FAST_CACHE_ALLOC_MASK16;
    }
    return _flags & FAST_CACHE_ALLOC_MASK;
}
size_t fastInstanceSize(size_t extra) const {
    ASSERT(hasFastInstanceSize(extra));
    if(__builtin_constant_p(extra) && extra ==0) {
        return _flags &FAST_CACHE_ALLOC_MASK16;
    }else{
        size_tsize = _flags &FAST_CACHE_ALLOC_MASK;
        return align16(size + extra -FAST_CACHE_ALLOC_DELTA16);
    }
}
void setFastInstanceSize(size_t newSize) {
    uint16_tnewBits = _flags & ~FAST_CACHE_ALLOC_MASK;
    uint16_tsizeBits;
    sizeBits =word_align(newSize) +FAST_CACHE_ALLOC_DELTA16;
    sizeBits &=FAST_CACHE_ALLOC_MASK;
    if(newSize <= sizeBits) {
        newBits |= sizeBits;
    }
    _flags = newBits;
}
#else

其中unalignedInstanceSize是获取这个类所有属性内存的大小。这里只有继承NSObject的一个属性isa——返回8字节,word_align就是字节对齐——64位系统下,对象大小采用8字节对齐,if (size < 16) size = 16;下次调用走fastInstanceSize,我们发现这个方法采用的是16字节对齐。 为什么至少16字节呢?我们创建对象都是id类型,id到底是什么类型呢?在objc.h文件中有如下代码

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;

所以对象其实就是objc_object,他拥有默认成员变量isa,isa指针占有8字节,加上其他成员变量的字节数内存一定大于8字节,毕竟开发需要创建其他的成员变量,所以开辟空间16字节。

那结构体是怎么进行的内存对齐呢

//1、定义两个结构体
struct Mystruct1 {
    char a;     //1字节
    double b;   //8字节
    int c;      //4字节
    short d;    //2字节
} Mystruct1;
struct Mystruct2 {
    double b;   //8字节
    int c;      //4字节
    short d;    //2字节
    char a;     //1字节
} Mystruct2;
//计算 结构体占用的内存大小
NSLog(@"%lu-%lu",sizeof(Mystruct1),sizeof(Mystruct2));
输出 24-16

从打印结果看出一个问题,结构体中变量相同,只是顺序不同,结果影响结构体占用内存大小,内存对齐规则如下:
1.数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。 min(当前开始的位置m,n) m=9 n=4 9 10 11 12
2.结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
3.收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补⻬。

以上面的为例
第一个结构体内存对齐方式:

struct Mystruct1{
    char a;     //1字节 从 0开始 1结束  min(0,1)
    double b;   //8字节 从 1开始 1不能整除8 从8开始 8到15 min(8,8)
    int c;      //4字节 从 16开始 能整除4 16到19
    short d;    //2字节 从 20开始 20能整除2 20到21
}Mystruct1;

最后 Mystruct1 中变量最大字节是8 Mystruct1 0-2122字节  22向上取整8的倍数 = 24, sizeof(Mystruct1)=24

第二个结构体对齐方式:

struct Mystruct2 {
    double b;   //8字节 从 0开始 0到7
    int c;      //4字节 从 8开始 8到11 
    short d;    //2字节 从 12开始 12到13
    char a;     //1字节 从 14开始 15结束
} Mystruct2;

最后 Mystruct2 中变量最大字节是8 Mystruct2 0-1515字节  15向上取整8的倍数 = 16, sizeof(Mystruct2)=16