iOS底层原理探究 - alloc的底层原理

在iOS开发的过程中,我们最熟悉的就是对象,经常会使用到的一个函数:alloc,那这个函数的底层到底做了什么呢 ?我们一起一探究竟。

开始探索前,先看一下探索过程中可能用到的一些指令!

一、常用指令

1. po:   为 print object 的缩写,显示对象的文本描述
2. bt:   打印函数的堆栈  
3. register read    读取寄存器
4. x/nuf 
    n表示要显示的内存单元的个数
    u表示一个地址单元的长度:
    取值范围: 
            b 单字节  
            h 表示双字节
            w 表示四字节
            g 表示八字节
    f表示显示方式:
    取值范围:
            x 按十六进制格式
            d 按十进制格式
            u 按十进制格式显示无符号
            o 按八进制格式
            t 按二进制格式
            a 按十六进制格式
            i 指令地址格式  
            c 按字符格式
            f 按浮点数格式

持续更新中...

二、alloc做了什么?

通过以下代码我们可以知道alloc是向系统申请内存空间

    JLPerson *p1 = [JLPerson alloc];
    JLPerson *p2 = [p1 init];
    JLPerson *p3 = [p1 init];
    
    NSLog(@"%@-%p-%p",p1,p1,&p1);
    NSLog(@"%@-%p-%p",p2,p2,&p2);
    NSLog(@"%@-%p-%p",p3,p3,&p3);
-----------------------------------------------------------
  -0x6000032c8020-0x7ffeef26e1a8
  -0x6000032c8020-0x7ffeef26e1a0
  -0x6000032c8020-0x7ffeef26e198

从上面的代码中可以看出p1、p2、p3的指针地址之间是相差8个字节,并且地址是连续的,这就符合栈内存的分配原则

根据代码的演示我们可以得到以下的图示。


图片1.png

总结:指针地址是在栈内存,申请的内存空间在堆内存

三、alloc底层是怎么调用?

我们已经知道了alloc是申请内存空间,那么它是怎么申请内存的呢?申请多少内存空间?内存的大小怎么计算?
带着这些问题往下探索。

三种探索底层的方式:

  • 下符号断点的形式直接跟流程: Symbolic Breakpoint
  • 按住control -> step into
  • 汇编查看跟流程:Debug -> Debug workflow -> Always show Disassembly

通过上面三种方式我们知道了alloc底层是属于libobjc库,我们将源码下载编译跑起来。

苹果开源源码汇总: https://opensource.apple.com
这个地址⽤的更直接: https://opensource.apple.com/tarballs/

  • 发现问题

首先我们对下载的源码进行编译,对alloc函数进行断点跟踪(也可以使用符号断点或者汇编的方式进行),按照正常的思维流程应该是响应alloc函数的底层调用,但是真正的调试却是走了objc_alloc,这是为什么呢?

  • 探索问题
  1. 通过对源码进行全局搜索 objc_alloc,对结果一个个解读我们可以从中发现一个函数fixupMessageRef,里面有一个if (msg->sel == @selector(alloc))判断,满足条件就是msg指向的imp 替换成objc_alloc

    objc_alloc.png

  2. 既然找到了fixupMessageRef,那么我顺着这条思路找一找fixupMessageRef是什么时候调用的呢?
    通过逆向的查找我们可以得出以下的一个调用流程:
    fixupMessageRef<--_read_images<--map_images_nolock<--map_images<--_dyld_objc_notify_register<--_objc_init
    把这些函数全部打上断点,运行程序,看是否如我们所想的那样进行了IMP的替换;运行后发现还是会走objc_alloc方法,但是并没有走fixupMessageRef方法进行替换。为什么会提供一个不被执行的修复函数呢?难道是因为在编译的过程中就有可能发生问题,然后做一个容错的处理吗?

  3. 找到LLVM的源码,通过解读LLVM的源码可以得出alloc、release、autoRelease等一些方法在编译的过程中LLVM会对这些函数进行Hook拦截

我们已经知道了为什么要走objc_alloc方法了,那对于alloc主线的流程通过断点方式跟下来就可以了。

  • alloc调用流程图:
alloc调用流程图.png
  1. alloc的主线流程图我们已经比较清晰了,接下来我们重点看一下 _class_createInstanceFromZone这个函数的实现。
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ...
    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 (!zone && fast) {
        # 将类cls和obj指针进行关联
        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);
}
  1. 我们重点看一下instanceSize这个函数,主要用来计算当前类需要开辟的内存空间大小。
看一下函数的整个流程图:
instanceSize流程图.png
字节对齐算法:
字节对齐算法.png

问题1:为什么alloc第一次会进objc_alloc,然后才会进去_objc_rootAlloc ?
(LLVM底层对objc_alloc进行拦截)

扩展:
  • init 初始化,使用工厂模式,可以对其进行重写,用来扩展
  • new 底层是allocinit的组合,直接使用new相对于使用alloc init扩展性更差了

四、内存对齐原则

前言:

1.属性和成员变量会影响内存大小,方法不影响内存大小
2.oc对象开辟内存空间大小是以16字节对齐,对象的成员变量的字节是以8字节对齐

各类型字节大小:
字节大小.png
问题1:为什么需要字节对齐?
  • 通常内存是由字节组成,cpu在存取数据时,是以为单位存取,的大小决定了内存存取的力度。频繁的存取未对齐的数据,会降低cpu的性能,所以可以通过内存对齐的方式来减少存取次数,从而达到降低cpu的开销,以空间来换取时间
问题2:为什么oc对象开辟内存空间是以16字节对齐?
  • 由于在一个对象中,第一个属性isa8字节,一个对象中肯定还会包含其他的属性成员变量,系统会预留8字节,即16字节对齐,而如果是8字节对齐的话,该对象的isa和下一个对象的isa紧挨着,访问时容易造成访问混乱。
  • 16字节对齐,可以加快cpu读取速度,也可以使访问更加安全
下面我们看一下结构体的内存对齐
struct LGStruct1 {
    double a;       // 8    [0 7]
    char b;         // 1    [8]
    int c;          // 4    [12 13 14 15]  (9 10 11空3个字节 12是4的倍数) 
    short d;        // 2    [16 17]
}struct1;
# 根据字节对齐是8字节原则  8的倍数最后为   24

struct LGStruct2 {
    double a;       // 8    [0 7]
    int b;          // 4    [8 9 10 11]    (8是4的倍数)
    char c;         // 1    [12]
    short d;        // 2    [14 15]    (13 空1个字节  14是2的倍数)
}struct2;
# 根据字节对齐是8字节原则  8的倍数最后为   16

从上述代码中可以看出,结构体的属性都一样,属性的顺序不一样,内存大小也不一样。

上面是单个结构体的内存对齐,如果结构体嵌套又是怎样的呢?
struct LGStruct1 {
    double a;       // 8   
    char b;         // 1    
    int   c;        //4
    short d;        // 2   
}struct1;

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]
    struct LGStruct1 {
        double a;       // 8    [24 --> 31]
        char b;         // 1    [32]
        int   c;        //4   [36  37  38   39]
        short d;        // 2    [40  41]
    }str;
}struct3;
# 将struct3进行展开, 根据8字节内存对齐原则,最终输出为 48
总结:
一般结构体大小
  • 1.结构体成员的偏移量必须是成员大小的整数倍
  • 2.结构体大小是最大元素的倍数(最大元素字节对齐)
嵌套结构体大小
  • 1.展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员变量的整数倍
  • 2.结构体大小必须是所有成员中最大元素的整数倍(8字节对齐)

如果以上内容有错误的地方,还请各位大佬指点!
持续更新和修复中...

你可能感兴趣的:(iOS底层原理探究 - alloc的底层原理)