iOS 底层原理-alloc流程

194 阅读8分钟

一.了解对象与指针

先看一张图:

1.png 这张图我们可知:

  • imgv是指针,指针指向的是对象;
  • [JPeople alloc]创建了一个对象;
  • p1写在等号前面,等于把P1指向了那个对象的内存地址,所以p1是指针;
  • 同理可知p2,p3也是指针,指向了P1指向了那个对象的同一块内存地址;
  • 所以我们都是通过通过这个指针找到内存中的对象(通过指针来找到对象而不是表示对象)

2.png

二.底层探索的三种方式

2.1 下断点方式

按着control + in 进入真机调试(模拟器是x86架构,真机是arm64架构):

WeChatc6591695e689b79e08b405899847b832.png in这里表示是上图所示的红框的按钮;

此时往下走找到libobjc.A.dylib动态库下的objc_alloc如图所示:

WeChat89d94366fc3364d42aeb66051eeee9c8.png

2.2 下符号断点

选择Symbolic Breakpoint(objc_alloc),

WeChat4b6f9f0391d9eb220e50d82b7a70c74a.png

过去断点,我们得到了 alloc 实现位于 libObjc 这个动态库;

WeChate3ff5f2ac75a07ba7b74ebc13bee7fa1.png

2.3 汇编方式

> Debug -> Debug Workflow -> Always show Disassembly 

具体操作打开Debug菜单下的 Debug Workflow 下的 Always Show Disassembly如图:

WeChatbdeaa6a7114b7c2e8668f4316553470b.png

WeChat83e3bb967434edfd5fe9e028de8fcf8d.png

WechatIMG143.jpeg

然后跳转到objec_alloc 继续按住control + in 下一步,我们可以一步一步的找到libobjc.A.dylib库下的objc_alloc;

三.alloc初始化源码跟踪流程

1. alloc源码

1.alloc

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

2._objc_rootAlloc

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

3.callAlloc

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

4._objc_rootAllocWithZone

注意:allocWithZone under OBJC2 ignores the zone parameter allocWithZone 在 OBJC2 下可以忽略zone的此参数,可能做了优化

_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);
}
<font color=red>alloc源码流程图</font>

5._class_createInstanceFromZone

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

2. 跟着源码查看堆栈信息分析:

在这里插入图片描述 我们看一下每一个堆栈对应的实现函数:

  1. main

    People *p = [Peple alloc];

  2. objc_alloc 在这里插入图片描述
  3. callAlloc ---> objc_msgSend 走的第二个流程 在这里插入图片描述
  4. [NSObject alloc]的alloc alloc 在这里插入图片描述
  5. _objc_rootAlloc 注意callAlloc第三个参数传true在这里插入图片描述
  6. callAlloc 注意:走的第一个流程在这里插入图片描述
  7. _objc_rootAllocWithZone 在这里插入图片描述 8. _class_createInstanceFromZone 在这里插入图片描述 代码解读
  • 判断缓存中是否存在自定义的alloc/allocWithZone地方实现,显然第一次运行类中是没有该方法缓存的;
  • 类的初始化在read_images方法执行时,而 实例对象的初始化在alloc 的时候。
  • 第一次执行 ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));会进行慢速方法查找,找到 NSObject类的alloc方法,并将方法放入方法缓存。
  • 所以除了第一调用 alloc方法外,之后在进行对象初始化会直接走_objc_rootAllocWithZone方法
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));
}

我们发现无论我们运行多少次都是这样流程:

3. alloc源码流程图

在这里插入图片描述

4.补充[NSObject alloc]流程

以上我分析了对象p的alloc流程,我们知道People继承于NSObject,那对于NSObject对象的底层流程到底是什么流程呢?

objc_alloc -> callAlloc -> _objc_rootAllocWithZone ->_class_createInstanceFromZone

走这样的流程是因为缓存已经有了,所以不会发送alloc消息

四.重要知识点的分析:

以上alloc流程图中我们三个方法如下图: 在这里插入图片描述

1. cls->instanceSize

此流程会计算出需要内存大小,跟踪源码如下:

  • 通过缓存进行快速计算 在这里插入图片描述 快速途径 在这里插入图片描述 最终来到了align16方法,系统内存16字节对齐算法

为什么系统内存16字节对齐

  1. cpu在存取数据时,并不是以字节为单位,而是以块为单位存取。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以通过减少存取次数来降低cpu的开销;
  2. 16字节对齐,是由于在一个对象中,第一个属性isa占8字节(继承自父类),当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱;
  3. 16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况;
  4. 苹果对于内存做的一些容错考虑
  • 第一进来没有在缓存中查找 在这里插入图片描述 alignedInstanceSize 方法走到 word_align方法,word_align方法中调用了unalignedInstanceSize
May be unaligned depending on class's ivars.
依赖于对象的属性
uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }

此方法可以看到从内存中ro取出干净的内存,大小为8字节;为什么呢? 1.其继承于NSObject ,中有isa,占用8个字节; 疑问点: 1.我们是在对象内存大小,而此时对象没有任何属性、方法成员变量、协议等,那到底影响对象内存大小因素到底是什么呢? 2.系统的16字节对齐为什么呢?从哪分析呢?上面也有部分分析到了,真正底层在哪呢?

2.calloc

向系统申请开辟内存,返回地址指针。此流程会临时分配一个脏内存,调用calloc后分配的内存空间才是创建对象的内存地址。 在这里插入图片描述

3.obj->initInstanceIsa

关联到相应的类,即将开辟的内存空间指向所要关联的类!通过运行结果发现,在调用obj->initInstanceIsa之前,obj只有一个内存地址且是id类型,而调用之后明确了对象类型为LGPerson

在这里插入图片描述

4.影响对象内存大小因素分析

对象占用内存内存大小,由其成员变量确定

验证过程如下: 在这里插入图片描述

打印可知对象占用了8个字节,(内存中数据以16字节对齐)

添加几个属性 在这里插入图片描述

结论:成员变量越多,对象内存占用空间越大

x/5gx p:以5个16进制的排版打印对象,也可以说成:5个8字节的排版打印对象p

5. init和new

1.init(汇编和符号断点方式)

在LGPerson * person = [[LGPerson alloc] init];这行代码中添加断点,并设置:Debug -> Debug Workflow -> Always Show Disassembly。运行程序,结果如下图: 在这里插入图片描述 发现该过程调用objc_alloc_init ,加入符号断点查看objc_alloc_init汇编在这里插入图片描述 汇编流程上图所示:[[LGPerson alloc] init];过程是调用了callAlloc方法创建对象之后,向该对象发送init消息。这里再添加init符号断点,继续运行程序

在这里插入图片描述 最后找到[NSObject init]函数,LGPerson没有实现init方法,寻到是其父类的,在源码中最终会调用到_objc_rootInit方法在这里插入图片描述 结合源码,设置断点进行调试跟踪,流程和我们在汇编+符号断点的流程是一致的,如下图所示!最终返回的内容是callAlloc创建的对象自身 在这里插入图片描述 总结:init只是一个构造方法,没有参与对象初始化的创建;工厂模式设计

init的流程
  • [[LGPerson alloc] init];
  • objc_alloc_init;
  • 先进行对象内存开辟alloc流程;
  • objc发送init消息;
  • [NSObject init];
  • _objc_rootInit;
  • return self 返回当前对象;
2.new

探索方式和 -init() 一样, LGPerson * newlg = [LGPerson new];并不会直接调用+new(),而是执行了 objc_opt_new 方法,见下图: 在这里插入图片描述 在这里插入图片描述 源码上: 在这里插入图片描述

这里有类似callAlloc的判断流程,如果初次初始化,hasCustomCore()会返回true,见下图,这样会进入发送消息流程,即发送new消息。

因为LGPerson没有实现new类方法,所以在进行方法查找过程中会找到NSObject中,最终执行+[NSObject new];方法。见下图所示: 在这里插入图片描述 继续执行程序,会走到_objc_rootAllocWithZone中进行对象初始化 在这里插入图片描述 总结:new方法是个类方法,其是对[[cls alloc] init];流程的封装的过程

new的流程
  • 1.[LGPerson new];
  • 2.objc_opt_new;
  • 3.先fastpath(cls && !cls->ISA()->hasCustomCore()有没有缓存第一次没有缓存;
  • 4.没有缓存[NSObject new]发送new的消息;
  • 5.调用[callAlloc(self, false/checkNil/) init]方法
  • 6.有缓存时候直接[callAlloc(cls, false/checkNil/) init];
  • 7.objc发送init消息;
  • 8.[NSObject init];
  • 9._objc_rootInit;
  • 10.return self 返回当前对象;

虽然+new()是对[[cls alloc] init];的封装,但是依然建议使用[[cls alloc] init];进行对象的初始化!因为init作为构造器,可以自定义提供自己所需要的初始化方法;

更多解释
x/nuf <addr>

n 表示要显示的内存单元的个数
------------------------
u 表示一个地址单元的长度
b 表示单字节
h 表示双字节
w 表示4字节
g 表示8字节
------------------------
f 表示显示方式,可取以下值:
x 按十六进制格式显示变量
d 按十进制格式显示变量
u 按十进制格式显示无符号整型
o 按八进制格式显示变量
t 按二进制格式显示变量
a 按十六进制格式显示变量
i 按指令地址格式显示变量
c 按字符格式显示变量
f 按浮点数格式显示变量