iOS底层原理--001:alloc & init

内存地址 & 指针地址

搭建allocDemo项目

打印对象的内存地址和指针地址

- (void)viewDidLoad {
   [super viewDidLoad];

   LGPerson *p1 = [LGPerson alloc];
   LGPerson *p2 = [p1 init];
   LGPerson *p3 = [p1 init];
   
   NSLog(@"对象:%@,内存:%p,指针:%p",p1,p1,&p1);
   NSLog(@"对象:%@,内存:%p,指针:%p",p2,p2,&p2);
   NSLog(@"对象:%@,内存:%p,指针:%p",p3,p3,&p3);
}

输出结果:

对象:<LGPerson: 0x282a28700>,内存:0x282a28700,指针:0x16b375b38
对象:<LGPerson: 0x282a28700>,内存:0x282a28700,指针:0x16b375b30
对象:<LGPerson: 0x282a28700>,内存:0x282a28700,指针:0x16b375b28

上述案例中,三个对象的内存地址相同,但指针地址不同

alloc方法中,为对象在堆区开辟内存空间,并返回指针地址。而init方法中,并没有对内存做任何处理,所以三个对象的内存地址相同

但它们的指针地址不同,因为指针地址在栈区,以连续的内存地址存储,相隔8字节,并指向相同的堆空间

底层探索的三种方法

日常开发中,我们只能找到alloc的方法定义,却找不到它的方法实现

+ (instancetype)alloc OBJC_SWIFT_UNAVAILABLE("use object initializers instead");

所以想了解底层原理,必须从源码中进行探索

底层探索的三种方法:

  • 使用Control + Step into单步调试
  • 查看汇编代码
  • 对已知方法设置符号断点

【方式一】

使用Control + Step into单步调试

底层调用的objc_alloc函数

选择Symbolic Breakpoint...,设置符号断点

objc_alloc设置符号断点

点击Continue继续执行

进入objc_alloc函数

  • 来自于libobjc.A.dylib动态库,想了解它的底层原理,必须探索objc源码

【方式二】

查看汇编代码

在菜单中,选择DebugDebug WorkflowAlways Show Disassembly

来到汇编代码,当bl指令一旦执行,就会进入objc_alloc函数

使用Control + Step into单步调试,执行两步,进入objc_alloc函数

objc_alloc设置符号断点,即可跟踪到方法来源

【方式三】

对已知方法设置符号断点

对象初始化依赖于alloc方法,对已知的alloc方法设置符号断点

一旦设置成功,整个项目中,针对alloc方法设置的断点非常多

所以,先对[LGPerson alloc]设置断点,暂时禁用alloc断点

项目运行后,先进入[LGPerson alloc]断点,然后启用alloc断点,点击Continue继续执行

+[NSObject alloc]方法,同样来自libobjc.A.dylib。所以我们想更深入的了解底层,对于objc源码的探索是必不可少的

下载objc源码

【方式一】

打开 Apple Open Source

选择系统版本,例如:11.3

在列表中,搜索objc

【方式二】

打开 Source Browser

在列表中,搜索objc

选择objc的源码版本

汇编结合源码探索

下载objc4-818.2源码,打开项目

搜索alloc {关键字,打开NSObject.mm文件,找到alloc方法实现

alloc方法的执行流程:alloc_objc_rootAlloccallAlloc

callAlloc方法中,出现了复杂的代码逻辑

使用汇编结合源码,定位条件分支的触发

延用allocDemo项目

alloc流程中找到的几个的函数,全部设置符号断点

运行项目,查看汇编代码,进入alloc方法

进入_objc_rootAlloc函数,由于编译器优化,不会执行callAlloc函数,直接跳转_objc_rootAllocWithZone函数

进入_objc_rootAllocWithZone函数

编译器优化

Code Generation Options

Build Setting中的设置:

设置Optimization Level,编译器的优化程度

  • None [-O0]:不优化
  • Fast [-O1]:大函数所需的编译时间和内存消耗都会稍微增加
  • Faster [-O2]:编译器执行所有不涉及时间空间交换的所有的支持的优化选项
  • Fastest [-O3]:在开启Fast [-O1]项支持的所有优化项的同时,开启函数内联和寄存器重命名选项
  • Fastest, Smallest [-Os]:在不显着增加代码大小的情况下尽量提供高性能
  • Fastest, Aggressive Optimizations [-Ofast]:与Fastest, Smallest [-Os]相比该级别还执行其他更激进的优化
  • Smallest, Aggressive Size Optimizations [-Oz]:不使用LTO的情况下减小代码大小
alloc源码解析

打开objc源码

进入alloc方法

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

进入_objc_rootAlloc函数

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

进入callAlloc函数

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

   if (allocWithZone) {
       return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
   }
   return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
  • #if __OBJC2__:编译器优化。如果给定条件为真,则编译下面代码
  • slowpath:假值判断。入参较大可能为false
  • fastpath:真值判断。入参较大可能为true
  • hasCustomAWZ:类或父类具有默认的alloc/allocWithZone:实现

fastpathslowpath的定义

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
  • __builtin_expect,由GCC引入。向编译器提供分支预测信息,从而帮助编译器进行代码优化
  • __builtin_expect(EXP, N),表示EXP等于N的概率较大

进入_objc_rootAllocWithZone函数

NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
   return _class_createInstanceFromZone(cls, 0, nil,
                                        OBJECT_CONSTRUCT_CALL_BADALLOC);
}

进入_class_createInstanceFromZone函数

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)
{
   ASSERT(cls->isRealized());

   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 {
       obj->initIsa(cls);
   }

   if (fastpath(!hasCxxCtor)) {
       return obj;
   }

   construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
   return object_cxxConstructFromClass(obj, cls, construct_flags);
}
  • instanceSize:计算内存大小
  • calloc:开辟内存空间
  • initInstanceIsa:将classisa进行关联

alloc流程图

llvm优化alloc

调用alloc方法,入口函数却是objc_alloc,这是为什么呢?

objc源码中,_read_images函数在dyld之后被调用

_read_images函数中,调用fixupMessageRef函数

进入fixupMessageRef函数

  • 关键代码:如果方法编号为alloc,修改为objc_alloc的函数地址

fixupMessageRef函数的作用,修复旧版本的方法调度表。难道在新版本中,alloc方法理应关联objc_alloc的函数地址吗?

fixupMessageRef函数被_read_images调用,而_read_imagesobjc源码中的执行时机,已经非常优先了

所以,只剩下两个时机可以关联allocobjc_alloc

  • dyld中进行关联
  • 在编译时期已经关联

查看MachO文件,在符号表中搜索alloc

  • 在编译时期,MachO中已经生成_objc_alloc符号

可以确定allocobjc_alloc的关联,是在编译时期由llvm完成

打开llvm源码,搜索objc_alloc关键字

  • 在注释中找到线索,alloc关联objc_allocallocWithZone:nil关联objc_allocWithZone
  • 代码进行了版本控制,哪些系统和版本有此关联

进入GeneratePossiblySpecializedMessageSend函数

  • 判断如果为特殊消息,调用tryGenerateSpecializedMessageSend函数,否则调用GenerateMessageSend函数
  • 特殊消息,例如:alloc方法

进入tryGenerateSpecializedMessageSend函数

  • OMF_alloc条件中,如果方法编号为alloc,调用EmitObjCAlloc函数并返回

进入EmitObjCAlloc函数

  • alloc方法编号,修改为objc_alloc的函数地址

苹果对特殊方法,自身会进行HOOK。例如alloc方法,优先进入objc_alloc流程,执行完毕后,对当前对象发送alloc消息,然后进入alloc流程

打开objc源码

在对象的alloc方法上设置断点

首先进入objc_alloc函数

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

来到callAlloc函数,发送alloc消息

进入alloc流程:alloc_objc_rootAlloccallAlloc

再次进入callAlloc函数,调用_objc_rootAllocWithZone函数,继续alloc流程

[LGPerson alloc]流程图

init源码解析

在对象的alloc + init方法上设置断点

调用alloc + init方法,入口函数为objc_alloc_init

打开objc源码

进入objc_alloc_init函数

id
objc_alloc_init(Class cls) {
   return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}
  • callAlloc函数执行完毕后,调用对象的init方法

进入init方法

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

进入_objc_rootInit函数

id
_objc_rootInit(id obj) {
   return obj;
}

至此,init流程结束,方法只做了一件事,将传入的self对象返回。init本质是构造方法,通过工厂设计模式,给用户提供入口以便重写和定制

new源码解析

对象初始化的另一种方式,new方法

调用new方法,入口函数为objc_opt_new

打开objc源码

进入objc_opt_new函数

id
objc_opt_new(Class cls)
{
#if __OBJC2__
   if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
       return [callAlloc(cls, false/*checkNil*/) init];
   }
#endif
   return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
}
  • hasCustomCore:类或父类具有默认的new/self/class/respondsToSelector/isKindOfClass
  • 符合条件,直接调用alloc + init方法。否则进行消息发送

触发消息发送流程,调用new方法,最终调用的还是alloc + init方法

+ (id)new {
   return [callAlloc(self, false/*checkNil*/) init];
}

结论:new方法等价于alloc + init方法。但我更推荐alloc + init方式,因为在开发中,我们会定义initWithXXX等方法,new方法将初始化固定为init,所以alloc + init相比new方法而言扩展性更好,使用更灵活。并且在原则上,显示调用比隐式调用更清晰

NSObject初始化流程

NSObject与自定义类的初始化流程有一些区别

alloc流程

NSObject调用alloc方法,首先进入objc_alloc流程

进入callAlloc函数,不触发objc_msgSend,直接调用_objc_rootAllocWithZone函数

new流程

NSObject调用new方法,进入objc_opt_new流程,不触发objc_msgSend,直接调用alloc + init方法

总结

内存地址 & 指针地址

  • 不同指针地址指向相同堆空间
  • alloc方法,为对象开辟内存空间,并返回指针地址
  • init方法,并没有对内存做任何处理

底层探索的三种方法

  • 使用Control + Step into单步调试
  • 查看汇编代码
  • 对已知方法设置符号断点

下载objc源码

汇编结合源码探索

  • alloc方法的执行流程:alloc_objc_rootAlloccallAlloc
  • 由于编译器优化,没有触发callAlloc方法的断点

编译器优化

  • Build Setting中,设置Optimization Level,编译器的优化程度
  • 七个不同程度的优化等级供开发者选择,Debug模式默认为None [-O0]不优化

alloc源码解析

  • alloc_objc_rootAlloccallAlloc_objc_rootAllocWithZone_class_createInstanceFromZone
  • alloc核心方法
    instanceSize:计算内存大小
    calloc:开辟内存空间
    initInstanceIsa:将classisa进行关联

llvm优化alloc

  • alloc方法,优先进入objc_alloc流程,执行完毕后,对当前对象发送alloc消息,然后进入alloc流程
  • 自定义对象,callAlloc函数会执行两遍
    ◦ 对当前对象发送alloc消息
    ◦ 调用_objc_rootAllocWithZone函数
  • 通过源码分析和MachO中的_objc_alloc符号,可以确定allocobjc_alloc的关联,是在编译时期由llvm完成
  • llvm源码中,如果是特殊消息,例如:alloc方法,调用tryGenerateSpecializedMessageSend函数,否则调用GenerateMessageSend函数
  • OMF_alloc条件中,如果方法编号为alloc,修改为objc_alloc的函数地址

init源码解析

  • init方法本质是构造方法
  • 用于将传入的self对象返回
  • 通过工厂设计模式,给用户提供入口以便重写和定制

new源码解析

  • new方法等价于alloc + init方法
  • 更推荐alloc + init方式,扩展性更好,使用更灵活,显示调用比隐式调用更清晰

NSObject初始化流程

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

推荐阅读更多精彩内容