第一篇:iOS底层探索alloc和init

1.开篇

首先让我们来看下对应代码:

    HPWPerson *p = [HPWPerson alloc];
    HPWPerson *p1 = [p init];
    HPWPerson *p2 = [p init];
    NSLog(@"p = %@",p);
    NSLog(@"p1 = %@,p2 = %@",p1,p2);

其对应的输出为

2022-04-18 23:18:41.252721+0800 001--alloc[5857:116065] p = <HPWPerson: 0x6000021bc1b0>
2022-04-18 23:18:41.252772+0800 001--alloc[5857:116065] p1 = <HPWPerson: 0x6000021bc1b0>,p2 = <HPWPerson: 0x6000021bc1b0>

这个时候我们可以发现alloc 后 ,然后将p对象init后的p1和p2值内存地址一样,那alloc是干了啥,以及init是干了啥呢。其实当我们给p1和p2设置成员变量的时候,其输出的值也是一样的。

    HPWPerson *p = [HPWPerson alloc];
    p.name = @"hpw";
    HPWPerson *p1 = [p init];
    HPWPerson *p2 = [p init];
    NSLog(@"p = %@",p.name);
    NSLog(@"p1 = %@,p2 = %@",p1.name,p2.name);

输出如下:

2022-04-18 23:32:56.587929+0800 001--alloc[6185:126956] p = hpw
2022-04-18 23:32:56.587974+0800 001--alloc[6185:126956] p1 = hpw,p2 = how

通过上面我们知道了,用init进行操作的时候是不会开辟内存空间的。那我们继续探究下底层操作,objc的源码在alloc和init里究竟干了啥。

2.alloc方法探索

当我们调试发现当alloc时候,第一步走的下面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));
}

再接着运行会走alloc方法

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

再运行会走_objc_rootAlloc方法

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

再运行下,奇迹就会发生,我们发现竟然还会走callAlloc这个方法,也就是callAlloc这个方法走了两次,那为什么会走两次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));
}

接着我们用汇编调试器进行调试看下对应的原因在哪里。选择 Xcode的Debug里Debug Workflow --> Always Show Disassembly,进行汇编调试,汇编中会用到b和bl指令,这个是个跳转指令,也就是一个函数的调用。ret也就是return的意思,也就是函数的返回,;在汇编里是注释的作用。 当我们运行汇编调试的时候发现断在了如下图:


WechatIMG1904.jpeg

也就是当我们走alloc方法,其实它是走的是objc_alloc,在其注释里可以发现。然后我们去objc源码里去找下objc_alloc方法的使用,我们发现其在fixupMessageRef方法里使用了,其中sel代表的是方法名,imp代表指向方法的实现。从下面代码里我们可以看到,当方法名为alloc的时候,其会把方法名指向objc_alloc方法,所以这里我们知道了汇编里当我们调用alloc方法后其调用的是objc_alloc方法。

fixupMessageRef(message_ref_t *msg)
{    
    msg->sel = sel_registerName((const char *)msg->sel);

    if (msg->imp == &objc_msgSend_fixup) { 
        if (msg->sel == @selector(alloc)) {
            msg->imp = (IMP)&objc_alloc;
        } else if (msg->sel == @selector(allocWithZone:)) {
            msg->imp = (IMP)&objc_allocWithZone;
        } else if (msg->sel == @selector(retain)) {
            msg->imp = (IMP)&objc_retain;
        } else if (msg->sel == @selector(release)) {
            msg->imp = (IMP)&objc_release;
        } else if (msg->sel == @selector(autorelease)) {
            msg->imp = (IMP)&objc_autorelease;
        } else {
            msg->imp = &objc_msgSend_fixedup;
        }
    } 

这样就结束了吗,并没有,我们继续探究,我们添加下objc_alloc这个断点,然后运行跳到如下图位置


WechatIMG1905.jpeg

我们继续运行,发现个奇怪的现象,它不会走_objc_rootAllocWithZone这个方法,如下图

WechatIMG1906.jpeg

而是走了下面的objc_msgSend这个方法,这样我们知道了objc_msgSend这个方法是当我们运行alloc方法调用起来的


WechatIMG1907.jpeg

然后通过) register 也就是寄存器进行读取,寄存器的作用是把参数传递给函数返回,其实通过x0寄存器进行操作的,我们通过
在调试端输入register read x0,就会打印HPWPerson,然后我们再读取下x1寄存器看下,register read x1,这个时候打印的是一串地址,我们需要通过po进行打印以及(char*)强转,打印后我们发现神奇的”alloc“出现了。

(lldb) register read x1
      x1 = 0x00000001c9d1789b
(lldb) po (char *)0x00000001c9d1789b
"alloc"

这里也就说明objc_msgSend 这个方法是通过HPWPerson这个类调用alloc方法,然后我们继续探究,打一个[NSObject alloc]断点,我们发现其走到了_objc_rootAlloc这个方法里,然后我们继续调试走到_objc_rootAlloc方法里面。


WechatIMG1908.jpeg

我们会发现其会走_objc_rootAllocWithZone这个方法,然后我们把这个方法到objc源码里去找下,发现其返回的id类型。也就是通过_class_createInstanceFromZone这个方法把我们的对象创建后返回了,那我们如何去验证呢,我们继续去探究下

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

然后在代码中,我们添加一个断点在ret这个汇编里,因为ret是个返回,再通过register read x0去读取这个值,然后再把这个值进行一个po,就会出现一个HPWPerson对象。

0x1801891e0 <+80>: ret

其实在上面运行objc源码时候,调试objc源码时候我们发现其实其走了两次callAlloc方法,调用汇编指令时候没有发现callAlloc方法调用两次,这个是因为编译器起到了一个优化的作用。那为什么会走两次呢,我们在objc源码里进行打断点操作,第一次发现走的是 return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc))语句;
在callAlloc方法里调用了objc_msgSend函数,然后objc_msgSend函数调用了alloc方法。第二次发现走到了callAlloc方法里 return _objc_rootAllocWithZone(cls, nil)语句;

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

通过上面一系列的操作,我们可以探索到当对象被alloc后,会按如下顺序进行调用:
alloc-> objc_alloc —> callAlloc —> objc_msgSend —> alloc —> _objc_rootAlloc —> callAlloc —>
_objc_rootAllocWithZone —> _class_createInstanceFromZone

3.init方法探索

    HPWPerson *p = [HPWPerson alloc];
    p.name = @"hpw";
    HPWPerson *p1 = [p init];
    HPWPerson *p2 = [p init];
    NSLog(@"p = %@",p);
    NSLog(@"p1 = %@,p2 = %@",p1.name,p2.name);

还是针对这个代码,我们添加一个[NSObject init]这个断点,我们发现只要一个ret的指令,这个也就是汇编指令的返回意思.


WechatIMG1909.jpeg

我们用register read x0进行读取,然后通过po (char *)把x0打印出会发现是HPWPerson,register read x1进行读取,然后通过po (char *)把x1打印出会发现是init

(lldb) register read x0
      x0 = 0x00006000006600f0
(lldb) po (char *)0x00006000006600f0
<HPWPerson: 0x6000006600f0>

(lldb) register read x1
      x1 = 0x00000001c9d176dd
(lldb) po (char *)0x00000001c9d176dd
"init"

然后我们继续去探索下源码,源码如下图,我们发现init方法啥都没干,直接返回了obj。既然init方法啥都没干,那为什么要创建init方法呢,其实这个是一个工厂模式,就是让你自己去重写init方法,在init方法里进行一些操作。

id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}

4.在探索时发现额外知识点补充

1)字节对其

在进行操作的时候,alignedInstanceSize()方法是字节对其算法。同时我们还看到 if (size < 16) size = 16,这样我们知道objc对象它最小的大小是16个字节。字节对其是指用8字节进行对其的。

id 
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
    void *bytes;
    size_t size;

    // Can't create something for nothing
    if (!cls) return nil;

    // Allocate and initialize
    size = cls->alignedInstanceSize() + extraBytes;

    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;

    if (zone) {
        bytes = malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        bytes = calloc(1, size);
    }

    return objc_constructInstance(cls, bytes);
}

苹果里8字节对其源码如下:其实也就是进行一个移位,
(x + 7) >> 3 << 3
当x=8时候,
8+7 = 15换成2进制如下所示
0000 1111
右移3位为 0000 0001
左移3位为 00001000 这里也就是8,所以说苹果是把不满足8的通过这个算法抹掉。

得出结论为:苹果是以8字节为倍数分配内存(8字节对其),且最小为16字节,这样做本质就是以时间换空间。那为什么说是以空间换时间呢,因为苹果存储的时候即使存储对象不满8字节,也按8字节去存储,然后CPU读取的时候就很方便,直接用8字节去读取就可以。但是如果按存储对象实际大小存储,那么这样会有大有小,CPU读取就会很慢,一下4字节读取,一下8字节读取,一下1字节读取......,这样对CPU的速度影响很大,所以用8字节统一去读取就是为了提高CPU的速度,以空间换时间。

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

1)对象里存放着什么

首先我们打开终端,用clang指令clang -rewrite-objc main.m编译我们main.m,然后会得到main.cpp,如下:


WechatIMG1917.jpeg

编译后我们在main.cpp里搜索下HPWPerson,发现有个objc_object,所以说对象的本质就是一个objc_object的结构体

typedef struct objc_object HPWerson;

接着我们再看下,有个叫NSObject_IMPL里有个isa,同时还有其成员变量,所以说isa指针和成员变量的值是存放在对象里的(注意这里是成员变量的值不是成员变量的名字),对象的地址是存在栈里面。

struct NSObject_IMPL {
    Class isa;
};

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

推荐阅读更多精彩内容