iOS-1.对象alloc的流程分析

760 阅读13分钟

ios底层文章汇总

1.引入

创建对象LGPerson类,在ViewDidLoad中进行如下编码

     <LGPerson: 0x6000006543c0> - 0x6000006543c0 - 0x7ffee47290e8
     <LGPerson: 0x6000006543c0> - 0x6000006543c0 - 0x7ffee47290e0
     <LGPerson: 0x6000006543c0> - 0x6000006543c0 - 0x7ffee47290d8
     
     */
    LGPerson *p1 = [LGPerson alloc];
    LGPerson *p2 = [p1 init];
    LGPerson *p3 = [p1 init];
    LGNSLog(@"%@ - %p - %p",p1,p1,&p1);
    LGNSLog(@"%@ - %p - %p",p2,p2,&p2);
    LGNSLog(@"%@ - %p - %p",p3,p3,&p3);

从调试终端答应的结果可知:存储p1,p2,p3的地址是不一样的,但p1,p2,p3指向同一内存空间,栈内存连续,p1,p2,p3的地址相差8个字节(64位系统,指针存储需要8个字节)

思考: 对象的alloc底层究竟做了什么?

2.底层原理探索的三种方式

2.1 方法一 下符号断点的形式直接跟流程

  • 添加符号断点alloc
  • LGPerson *p1 = [LGPerson alloc];前添加普通断点
  • 运行代码,首先会被alloc断点断住,这是由于系统做了许多对象的内存分配,将alloc断点设置不进入,再全速运行,直到LGPerson *p1 = [LGPerson alloc];处断点断住,再打开alloc的断点,再全速运行
  • 进入[LGPerson alloc]的alloc断点,发现由于LGPerson没有实现alloc方法,是父类的alloc方法,libobjc.A.dylib' +[NSObject alloc]

  • 单步执行,step into, 进入libobjc.A.dylib'_objc_rootAlloc

2.2 方法二 通过摁住control - step into

  • LGPerson *p1 = [LGPerson alloc];前添加普通断点

  • 断点断住后摁住control - step into

  • 符号断点objc_alloc,查看源码出处

  • 单步执行,step into, 进入_objc_rootAlloc

2.3 方法三 汇编查看跟流程

  • 设置 Debug -> Debug Workflow -> Always Show Disassembly

  • control+step into 进入到objc_alloc

  • 设置符号断点objc_alloc,查看源码出处

  • 单步执行,step into, 进入_objc_rootAlloc

3.在obj781源码工程中查看alloc源码

3.1 苹果开源源码

https://opensource.apple.com
https://opensource.apple.com/tarballs

找到Source下得NSObject.mm中+(id)alloc的实现 全局搜索 alloc { 定位到NSObject.mm文件中

+ (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)
{
#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 & fastpath

其中关于slowpathfastpath这里需要简要说明下,这两个都是objc源码中定义的,其定义如下

**

//x很可能为真, fastpath 可以简称为 真值判断
#define fastpath(x) (__builtin_expect(bool(x), 1)) 
//x很可能为假,slowpath 可以简称为 假值判断
#define slowpath(x) (__builtin_expect(bool(x), 0)) 

其中的__builtin_expect指令是由gcc引入的,
1、目的:编译器可以对代码进行优化,以减少指令跳转带来的性能下降。即性能优化
2、作用:允许程序员将最有可能执行的分支告诉编译器
3、指令的写法为:__builtin_expect(EXP, N)。表示 EXP==N的概率很大
4、fastpath定义中__builtin_expect((x),1)表示 x 的值为真的可能性更大;即 执行if 里面语句的机会更大
5、slowpath定义中的__builtin_expect((x),0)表示 x 的值为假的可能性更大。即执行 else 里面语句的机会更大
6、在日常的开发中,也可以通过设置来优化编译器,达到性能优化的目的,设置的路径为:Build Setting --> Optimization Level --> Debug --> 将None 改为 fastest 或者 smallest

cls->ISA()->hasCustomAWZ()

其中fastpath中的 cls->ISA()->hasCustomAWZ() 表示判断一个类是否有自定义的 +allocWithZone 实现,这里通过断点调试,是没有自定义的实现,所以会执行到 if 里面的代码,即走到_objc_rootAllocWithZone

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


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)// alloc 源码 第五步
{
    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;

    //计算需要开辟的内存大小,传入的extraBytes 为 0
    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) {
        //将 cls类 与 obj指针(即isa) 关联
        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);
}

_class_createInstanceFromZone的源码实现,主要分为三部分:

  • cls->instanceSize:计算需要开辟的内存空间大小
  • calloc申请内存,返回地址指针
  • obj->initInstanceIsa:将 类 与 isa 关联

总结

其调用链是:[[HTPerson alloc]init] --> objc_alloc_init --> callAlloc --> ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)) --> +alloc --> _objc_rootAlloc --> callAlloc -->objc_rootAllocWithZone --> _class_createInstanceFromZone

[HTPerson alloc]-->objc_alloc-->callAlloc --> ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)) --> +alloc --> _objc_rootAlloc --> callAlloc -->objc_rootAllocWithZone --> _class_createInstanceFromZone

进入到_class_createInstanceFromZone后有分支处理,单纯从源码不能确定流程如何-->

  • LGPerson *p1 = [LGPerson alloc];前添加普通断点
  • 执行到断点处后,添加符号断点 callAlloc
  • 单步执行 跟踪程序流程

断点后 读寄存器 register read

register read x0 ; x0是调用函数时第一个参数存放处,函数返回时是返回值的存放位置

  • 分配多少内存 :8字节对齐,最小16字节

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. 最少16字节
        if (size < 16) size = 16;
        return size;
    }
    
    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_t size = _flags & FAST_CACHE_ALLOC_MASK;
            // remove the FAST_CACHE_ALLOC_DELTA16 that was added
            // by setFastInstanceSize  FAST_CACHE_ALLOC_DELTA16 = 0x0008
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }
    
    //16字节对齐,苹果早期是8字节对齐
  static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}        

image.png

3.2 NSObject *objc1 = [[NSObject alloc] init];

此时的alloc并没有走上述流程,查看汇编后发现:alloc在OC转汇编时,都是调用的objc_alloc

  • 打开Debug --> Debug Workflow --> 勾选 Always Show Disassemly,开启汇编调试

  • 关闭源码的断点,只留main中的断点,重新运行程序,然后通过下图的汇编可以发现NSObject走的objc_alloc

image.png

调用alloc时,什么时候转到objc_alloc的?

在llvm源码中搜索objc_alloc

image.png

搜索shouldUseRuntimeFunctionForCombinedAllocInit,表示版本控制

image.png

搜索tryEmitSpecializedAllocInit,非常著名的特殊消息发送,在这里也没有找到 objc_alloc

image.png

继续尝试,开启上帝视角,通过alloc字符串搜索,如果还找不到,还可以通过_alloc:找到tryGenerateSpecializedMessageSend,表示尝试生成特殊消息发送

llvm源码查看消息转发函数GeneratePossiblySpecializedMessageSend--->if判断条件中调用 tryGenerateSpecializedMessageSend --->"alloc "消息时,调用EmitObjCAlloc--> 对象在消息转发alloc时,转而转发objc_alloc消息 --> GeneratePossiblySpecializedMessageSend函数中的if判断

image.png

  • 当GeneratePossiblySpecializedMessageSend函数中的if判断tryGenerateSpecializedMessageSend返回false-->进行原有消息"alloc"的转发:GenerateMessageSend,eg:[HTPerson alloc]的调用流程

image.png

  • if判断tryGenerateSpecializedMessageSend返回true,就可以进入if分支返回,不再进行消息转发,eg:[NSObject alloc]的调用流程: NSObject中的alloc 会走到 objc_alloc,其实这部分是由系统级别的消息处理逻辑,所以NSObject的初始化是由系统完成的,因此也不会走到alloc的源码工程中

image.png

LGPerson中alloc 走两次

LGPerson 第一次的alloc会走到 objc_alloc --> callAlloc方法中最下方的objc_msgSend,向系统发送消息 继续执行代码,发现会走到 alloc --> _objc_rootAlloc-->callAlloc --> _objc_rootAllocWithZOne

3.3 对象内存分配的字节对齐

iOS使用16字节对齐内存分配方式,对象分配内存时需要分配地址的有:isa,对象的属性,将实际需要的地址传入函数align16,进行16字节的对齐返回

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}
cls->instanceSize:计算所需内存大小

计算需要开辟内存的大小的执行流程如下所示

instanceSize流程

  • 1、跳转至instanceSize的源码实现
size_t instanceSize(size_t extraBytes) const {
    //编译器快速计算内存大小
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        return cache.fastInstanceSize(extraBytes);
    }
    
    // 计算类中所有属性的大小 + 额外的字节数0
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    //如果size 小于 16,最小取16
    if (size < 16) size = 16;
    return size;
}

通过断点调试,会执行到cache.fastInstanceSize方法,快速计算内存大小

  • 2、跳转至fastInstanceSize的源码实现,通过断点调试,会执行到align16
size_t fastInstanceSize(size_t extra) const
{
    ASSERT(hasFastInstanceSize(extra));

    //Gcc的内建函数 __builtin_constant_p 用于判断一个值是否为编译时常数,如果参数EXP 的值是常数,函数返回 1,否则返回 0
    if (__builtin_constant_p(extra) && extra == 0) {
        return _flags & FAST_CACHE_ALLOC_MASK16;
    } else {
        size_t size = _flags & FAST_CACHE_ALLOC_MASK;
        // remove the FAST_CACHE_ALLOC_DELTA16 that was added
        // by setFastInstanceSize
        //删除由setFastInstanceSize添加的FAST_CACHE_ALLOC_DELTA16 8个字节
        return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
    }
}
  • 3、跳转至align16的源码实现,这个方法是16字节对齐算法

    //16字节对齐算法 static inline size_t align16(size_t x) { return (x + size_t(15)) & ~size_t(15); }

内存字节对齐原则

在解释为什么需要16字节对齐之前,首先需要了解内存字节对齐的原则,主要有以下三点

  • 数据成员对齐规则:struct 或者 union 的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数据、结构体等)的整数倍开始(例如int在32位机中是4字节,则要从4的整数倍地址开始存储)
  • 数据成员为结构体:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储)
  • 结构体的整体对齐规则:结构体的总大小,即sizeof的结果,必须是其内部做大成员的整数倍,不足的要补齐

为什么需要16字节对齐

需要字节对齐的原因,有以下几点:

  • 通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数降低cpu的开销
  • 16字节对齐,是由于在一个对象中,第一个属性isa8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱
  • 16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况

字节对齐-总结

  • 在字节对齐算法中,对齐的主要是对象,而对象的本质则是一个 struct objc_object的结构体
  • 结构体在内存中是连续存放的,所以可以利用这点对结构体进行强转。
  • 苹果早期是8字节对齐,现在16字节对齐

下面以align(8) 为例,图解16字节对齐算法的计算过程,如下所示

16字节对齐算法图解

  • 首先将原始的内存 8 与 size_t(15)相加,得到 8 + 15 = 23
  • 将 size_t(15) 即 15进行~(取反)操作,~(取反)的规则是:1变为0,0变为1
  • 最后将 23 与 15的取反结果 进行 &(与)操作,&(与)的规则是:都是1为1,反之为0,最后的结果为 16,即内存的大小是以16的倍数增加的
calloc:申请内存,返回地址指针

通过instanceSize计算的内存大小,向内存中申请 大小 为 size的内存,并赋值给obj,因此 obj是指向内存地址的指针

obj = (id)calloc(1, size);

这里我们可以通过断点来印证上述的说法,在未执行calloc时,po objnil,执行后,再po obj法线,返回了一个16进制的地址

在平常的开发中,一般一个对象的打印的格式都是类似于这样的<LGPerson: 0x01111111f>(是一个指针)。为什么这里不是呢?

  • 主要是因为objc 地址 还没有与传入 的 cls进行关联,
  • 同时印证了 alloc的根本作用就是 开辟内存
obj->initInstanceIsa:类与isa关联

经过calloc可知,内存已经申请好了,类也已经传入进来了,接下来就需要将 类与 地址指针 即isa指针进行关联,其关联的流程图如下所示

initInstanceIsa流程

主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行 关联

同样也可以通过断点调试来印证上面的说法,在执行完initInstanceIsa后,在通过po obj可以得出一个对象指针

3.4 设置编译器优化

image.png

4. alloc &init & new

4.1 alloc

  • 开辟申请内存
  • 初始化isa

4.2 init

   // Replaced by CF (throws an NSException)

+ (id)init {
    return (id)self;
}

- (id)init {
    return _objc_rootInit(self);
} 

init是构造方法,工厂设计模式,为开发者自定义子类提供初始化入口

- (instancetype)init{
    self = [super init];
    if(self){
        [self setupUI];
    }
}

4.3 new

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}
  • 如果子类没有重写父类的init,new会调用父类的init方法

  • 如果子类重写了父类的init,new会调用子类重写的init方法

  • 如果使用 alloc + 自定义的init,可以帮助我们自定义初始化操作,例如传入一些子类所需参数等,最终也会走到父类的init,相比new而言,扩展性更好,更灵活。

new 相当于 alloc+init,但不推荐用new,如果对象子定义了初始化函数initWithxxx,则不会调用重写的initWithxxx:的初始化

5.对象地址的分配和属性地址

5.1 mask

5.2 对象的isa 和属性的地址

x/4gx x:表示用16进制的方式打印; 4g:表示打印4段 ;