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);
-----------------------------------------------------------
  <JLPerson: 0x6000032c8020>-0x6000032c8020-0x7ffeef26e1a8
  <JLPerson: 0x6000032c8020>-0x6000032c8020-0x7ffeef26e1a0
  <JLPerson: 0x6000032c8020>-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字节对齐)

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容