iOS底层学习——对象初始化探索

简介: iOS底层学习——对象初始化探索

大拿之路,任重道远,永不言弃!!!

一.学习线索

我们平时的开发中都会认为main函数是应用程序的入口,是否是这样的?我们在main函数的入口添加一个断点。bt查看运行的堆栈信息,发现一些端倪!

main函数入口堆栈信息

发现在main函数之前还有一个start,并且这个start来自动态库libdyld.dylid。说明main函数并不是应用程序的入口,在此之前还有一些应用程序启动过程。
很陌生,我们先从简单的开始,在这里添加一个符号断点alloc,过掉main函数入口的断点,bt查看堆栈信息,可以发现更多程序加载内容。

进入主程序后堆栈信息

比如UIKitCoreCoreFoundationlibdispach.dylib等等,我们开发过程中最常用的比如UINSObjectGCD等内容,均是来自这些库。

先从最熟悉的创建对象开始!!!

二.alloc原理初探

开发过程中创建对象是我们最常写的代码。
GFPerson * gf = [[GFPerson alloc] init];
留下一个问题,这里alloc做了什么,init又做了什么呢?

1.探索案例

引入一个案例,使用alloc方法初始化一个对象gf1,通过gf1调用init方法创建了gf2gf3,然后分别输出对象、对象地址、指针地址。

探索案例

根据运行结果,发现三者的对象输出是一样的,对象地址也是一样的,指针地址不一样。

案例总结:

  1. 三个指针指向了同一个对象,这个对象是<GFPerson: 0x600000fbc1f0>;
  2. 三个指针的地址是连续的存储在栈区中,并且从高位向低位开辟内存空间

内存空间

  1. 指针占用空间是8个字节
  2. lg1通过alloc方法开辟了内存空间,创建了对象;通过init方法初始化的lg2lg3并没有开辟内存空间,也就是没有创建对象。

那么,alloc是如何开辟内存空间创建对象的呢?init又起到什么作用呢?

2.底层探究方法

在我们的开发工程中,Jump to Definition只能看到alloc方法的声明,不能看到真正的源码实现流程。

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

下面提供三种底层探究的方法。

1. 添加符号断点的形式直接跟流程

我们要研究alloc方法,那就添加个符号断点alloc。符号断点添加方式见下图:

添加符号断点流程

因为我们暂时只研究GFPerson类的初始化流程,所以避免其他类调用alloc导致的干扰,运行时先将alloc断点设置为Disable Breakpoint。在程序运行到GFPerson * gf1 = [GFPerson alloc];时,再将alloc断点设置为Enable Breakpoint 。运行结果见下图:

alloc符号断点

根据上图的内容发现,[GFPerson alloc]最终走到了+[NSObject alloc];为什么是NSObject呢?因为LGPerson继承自NSObject子类中没有alloc方法,最终会调用父类的alloc方法
并且我们还发现了一个重要线索,[NSObject alloc]来自于libobjc.A.dylib动态库

2. 通过按住control - step into

GFPerson * gf1 = [GFPerson alloc];这行代码添加断点,运行程序,运行到断点后,点击control - step into。操作流程见下图:

control - step into流程

进入后发现调用了objc_alloc,接着采用方式1添加符号断点objc_alloc,运行程序。

objc_alloc符号断点运行结果

同样我们也发现了一个重要线索,objc_alloc也来自于libobjc.A.dylib动态库

3. 汇编查看跟流程

设置方式:Debug -> Debug Workflow -> Always Show Disassembly

汇编查看跟流程设置

GFPerson * gf1 = [GFPerson alloc];这行代码添加断点,运行程序。进入汇编流程,见下图:

汇编流程

看不懂汇编,但是根据关键字眼也是可以发现一些端倪的,比如同样会走到objc_alloc方法,继续采用符号断点方式查找出处。

添加objc_alloc符号断点

4.探索总结

三种底层探究的方法

根据上面的探索方式我们找到了alloc方法的出处,也就是动态库:libobjc.A.dylib
同时也有个疑问,初始化调用了alloc方法,但在上面的汇编跟踪发现会调用objc_alloc方法,这两个方法又有什么关系呢?
如果要深入学习alloc的实现流程,下载源码objc4源码。源码下载地址:objc4源码下载地址

三.结合源码分析alloc

源码下载完成,采用关键字alloc {全局搜索,在NSObject.mm文件(Objective-C++汇编)中找到alloc的方法实现。

1.alloc的流程分析

根据上面搜索的alloc方法实现,跟踪关键源码如下:

// 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

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

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

从源码可以得到如下流程:

alloc的流程

如何验证callAlloc走的是_objc_rootAllocWithZone还是objc_msgSend呢?没错上面已经学习了探索方式,这里可以使用汇编+添加符合断点
下面设置符号断点,在程序运行到GFPerson * gf1 = [GFPerson alloc];之后再将符号断点设置为Enable Breakpoint,确保跟踪的是GFPerson的初始化流程。断点调试,发现走的是_objc_rootAllocWithZone的流程。

汇编+添加符合断点跟踪

继续跟踪源码,_objc_rootAllocWithZone ->class_createInstanceFromZone,那么alloc的流程见下图:

alloc的流程

2.流程梳理

alloc的流程跟踪完成,貌似没啥问题,但是上一章节中的疑问还没有解开!初始化调用了alloc方法,但在上面的汇编跟踪发现会调用objc_alloc方法,这两个方法又有什么关系呢?

  • control - step into的方式中,跟踪代码,走的是objc_alloc方法
  • 汇编查看跟踪方式中,也是调用了objc_alloc方法

问题出在哪?既然有源码了,设置断点,跟踪一下!分别在alloc方法objc_alloc方法中设置断点。看看它到底会走到哪里!设置方式见下图:

设置alloc和objc_alloc断点

同样的处理方式,当程序走到GFPerson * gf1 = [GFPerson alloc];之后再将alloc方法objc_alloc方法两处断点设置为Enable Breakpoint,确保跟踪的是GFPerson的初始化流程。运行代码:

objc_alloc方法被调用,没有调用alloc方法

神奇的事情发生了,调用的alloc方法,结果运行进入了objc_alloc方法中。断点调试,跟踪发现,会调用callAllocobjc_msgSend方法,发送一个alloc消息

发送消息alloc

这个流程有些长,还需要再梳理一下,关键点在callAlloc方法calloAlloc到底会走哪个分支需要深入研究一下!关键代码是:

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

bool hasCustomAWZ() const 
{
    return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}

代码解读:

  1. 判断缓存中是否存在自定义的alloc/allocWithZone地方实现,显然第一次运行类中是没有该方法缓存的。
  2. cls是什么?类嘛?不是!看看源码的定义:typedef struct objc_class *Class;。所以cls是一个指针,指向一个结构体,这个结构体也就是Core Foundation层的类
  3. 类的初始化在read_images方法执行时,而实例对象的初始化在alloc的时候。
  4. 第一次执行((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));,会进行慢速方法查找,找到NSObject类的alloc方法,并将方法放入方法缓存
  5. 所以除了第一调用alloc方法外,之后在进行对象初始化会直接走_objc_rootAllocWithZone方法可进行debug跟踪验证!已验证!

综上,得出以下流程图:

image.png

至此基本摸清了alloc的调用流程,但是问题是调用alloc方法为什么会走到objc_alloc中呢? —— llvm!后面再解密!

四.Alloc核⼼⽅法

_class_createInstanceFromZone方法核心流程见下图:

Alloc核⼼⽅法

1.cls->instanceSize

此流程会计算出需要的内存空间⼤⼩。跟踪cls->instanceSize源码是实现:

cls-&gt;instanceSize

进行了编译器优化,更容易执行缓存中fastInstanceSize方法,进行快速计算所需的内存空间大小

fastInstanceSize

最终会进入align16方法,这个方法是16字节对齐算法。

以8为例,16字节对齐算法过程

流程解析:

  1. 初始化的原始内存8 + 15 = 23,23二进制位:0001 0111
  2. 15二进制位:0000 1111,取反操作得到:1111 0000
  3. 然后将1、2中得到的数据进行&运算,同时为1才得1,结果为:0001 0000 = 16

为什么需要16字节对齐?有以下几点:

  1. cpu在存取数据时,并不是以字节为单位,而是以块为单位存取。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以通过减少存取次数来降低cpu的开销;
  2. 16字节对齐,是由于在一个对象中,第一个属性isa占8字节(继承自父类),当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱;
  3. 16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况。

2.calloc

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

开辟内存

3.obj->initInstanceIsa

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

关联响应的类

五.补充内容

1.编译器优化

#define fastpath(x) (__builtin_expect(bool(x), 1))

#define slowpath(x) (__builtin_expect(bool(x), 0))

fastpath:可以理解为快速流程,对更有可能执行的流程进行优化,调高运行速度;

slowpath:基本流程,不被优化的。

编译器优化设置

一般针对发布版本设置为fastest, Smallest,让发布版本更快更小。对开发版本设置为None,便于调试!

2.内存优化

如果自定义类没有定义属性,仅仅只是继承自NSObject,则这个实例对象实际占用的大小为8字节,使用的为8字节对齐算法。系统实际分配的内存大小,使用的为16字节对齐的算法。

内存对齐可以理解为结构体中的成员申请内存大小时,系统最小分配的内存为8字节,是按每次8字节来申请的,不够8字节的也会申请8字节,然后按结构体中成员变量顺序再次申请,直到所有成员变量都能放下为止。

  • 内存优化(属性重排)

当结构体成员内存小的在前面时会因为内存对齐的原因比较浪费内存,为了解决这个问题苹果中采用空间换时间,将类中的属性进行重排,以此来达到内存优化的目的。

内存重排

3.instanceSize疑问

这里到底是走缓存还是下面的分支呢?
初始化大小

会走到缓存里!

image.png

在类的实现过程中,类是否为非懒加载,如果是非懒加载就会在main函数之前;如果懒加载,就会在第一次发送消息的时候,会对类进行初始化,也就是实现类!过程中会将fastInstanceSize设置到缓存中。

类实现设置过程中会将fastInstanceSize设置到缓存中

所以系统实际分配的内存大小,使用的为16字节对齐的算法。

但是不太理解下面返回的8字节对齐是什么意思!

4.NSObject alloc

补充一下[NSObject alloc]流程,因为在调用NSObjectalloc方法时,alloc已经放入缓存(系统初始化时,已经被其他的类调用,放入了缓存!)。所以NSObjec alloc流程会直接调用_objc_rootAllocWithZone

image.png

相关文章
|
2月前
|
Web App开发 小程序 Android开发
mPaaS小程序问题之接入iOS后阿里百川初始化报错如何解决
mPaaS小程序是阿里巴巴移动平台服务(mPaaS)推出的一种轻量级应用解决方案,旨在帮助开发者快速构建跨平台的小程序应用;本合集将聚焦mPaaS小程序的开发流程、技术架构和最佳实践,以及如何解决开发中遇到的问题,从而助力开发者高效打造和维护小程序应用。
46 1
|
5月前
|
安全 前端开发 Android开发
鸿蒙开发|鸿蒙系统的介绍(为什么要学习鸿蒙开发|鸿蒙系统的官方定义|鸿蒙和安卓、ios的对比)
鸿蒙开发学习是一项探索性的工作,旨在开发一个全场景分布式操作系统,覆盖所有设备,让消费者能够更方便、更直观地使用各种设备。
290 6
鸿蒙开发|鸿蒙系统的介绍(为什么要学习鸿蒙开发|鸿蒙系统的官方定义|鸿蒙和安卓、ios的对比)
|
7月前
|
iOS开发
iOS UIKit Dynamics Demo 学习地址列表
iOS UIKit Dynamics Demo 学习地址列表
23 0
|
9月前
|
C语言 C++ iOS开发
iOS中C++静态全局变量的动态初始化时序
一个由于C++初始化失败导致Realm初始化失败的Crash
131 1
|
iOS开发
iOS开发 - 写一个刷新的控件(未封装,适合新手学习,查看原理)
iOS开发 - 写一个刷新的控件(未封装,适合新手学习,查看原理)
127 0
iOS开发 - 写一个刷新的控件(未封装,适合新手学习,查看原理)
|
物联网 Android开发 iOS开发
iOS开发 - 蓝牙学习的总结
iOS开发 - 蓝牙学习的总结
129 0
|
存储 Unix 编译器
|
存储 算法 iOS开发
|
移动开发 JavaScript weex
Weex项目初始化weex-iOS集成
项目初始化 1、没有现成的工程的话新建iOS项目 命令行cd到项目根目录 执行 pod init,会创建一个pod配置文件 用编辑器打开,加上 pod 'WeexSDK', :path=>'.
931 0
|
27天前
|
API 数据安全/隐私保护 iOS开发
利用uni-app 开发的iOS app 发布到App Store全流程
利用uni-app 开发的iOS app 发布到App Store全流程
83 3