美文网首页iOS底层
OC对象原理探究(上)

OC对象原理探究(上)

作者: 浅墨入画 | 来源:发表于2021-07-09 18:04 被阅读0次
APP启动流程探索

创建空工程代码如下,并且添加符号断点命名如下图

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"alloc 探索");
    Person *p1 = [Person alloc];
    Person *p2 = [p1 init];
    Person *p3 = [p1 init];
}
添加符号断点

运行工程查看堆栈信息

app启动以及对象加载过程

红色为app启动过程: _dyld_start(dyld开始加载) -> dyld::main -> dyld_initialzeMainExecutable、ImageLoader...等等,表示主程序由_dyld_start开始到main等为启动做准备,包括加载动态库,共享内存,全局C++函数的析构,还有一系列的初始化,注册回调函数都在此步骤内完成。
蓝色为对象加载过程: App启动一系列函数 -> libSystem_initializer -> libdispatch_init -> GCD环境的准备 -> _objc_init

OC对象初始化

分析alloc源码之前,先来查看三个变量内容内存地址的区别:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"alloc 探索");
    Person *p1 = [Person alloc];
    Person *p2 = [p1 init];
    Person *p3 = [p1 init];
    NSLog(@"%@-%p",p1,p1);
    NSLog(@"%@-%p",p2,p2);
    NSLog(@"%@-%p",p3,p3);
}

<!-- 打印信息 -->
alloc 探索
2021-07-07 23:16:11.578015+0800 001-alloc&init探索[5266:540842] <Person: 0x600001994710>-0x600001994710
2021-07-07 23:16:11.578220+0800 001-alloc&init探索[5266:540842] <Person: 0x600001994710>-0x600001994710
2021-07-07 23:16:11.578378+0800 001-alloc&init探索[5266:540842] <Person: 0x600001994710>-0x600001994710
得出结论:
  • 三个对象指向的是同一个内存空间,所以其内容内存地址是相同的
  • p1 alloc之后拥有了内存,并且拥有了指针的指向
  • init未对指针进行任何操作
<!-- 查看指针地址 -->
Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
NSLog(@"%@-%p-%p",p1,p1,&p1);
NSLog(@"%@-%p-%p",p2,p2,&p2);
NSLog(@"%@-%p-%p",p3,p3,&p3);

<!-- 打印信息 -->
alloc 探索
2021-07-08 20:57:54.778216+0800 001-alloc&init探索[7619:869162] <Person: 0x600001720490>-0x600001720490-0x7ffeee444028
2021-07-08 20:57:54.778464+0800 001-alloc&init探索[7619:869162] <Person: 0x600001720490>-0x600001720490-0x7ffeee444020
2021-07-08 20:57:54.778699+0800 001-alloc&init探索[7619:869162] <Person: 0x600001720490>-0x600001720490-0x7ffeee444018
由上图指针地址0x7ffeee444028 0x7ffeee444020 0x7ffeee444018得出结论:
  • *p1、*p2、*p3属于栈上内存地址
  • *p1、*p2、*p3是连续的地址空间,每个相隔8字节(解释:0x18+0x8=0x20、0x20+0x8=0x28)
图形详解:内存、指针的关系
连续开辟,指向同一块空间

下面探索alloc做了什么?init做了什么?

探索源码的三种方法

这里使用的模拟器,也可以使用真机探索

方法一
  • 代码中打上断点
image.png
  • 将工程运行,停在断点处之后,按住control + Step into进入到汇编代码
image.png image.png
  • 对看到的objc_alloc添加符号断点
image.png
  • 按住control + Step into向下走
image.png

这里看到了libobjc.A.dylib objc_alloc,也看到了接下来会调用的方法_objc_rootAllocWithZone,objc_msgSend,我们就找到了objc_alloc底层源码来自于哪个动态库,为向下探索提供了线索!

方法二:通过汇编流程查看
  • 选择菜单栏Debug->Debug wrokflow->Always Show Disassembly打开汇编模式,打上断点同上,运行工程
image.png
  • 按住control + Step into找到objc_alloc
image.png
  • 对看到的objc_alloc添加符号断点(同方式一)
  • 按住control + Step into向下走(同方式一)
方法三:直接通过已知符号断点设定,直接进入
  • 打断点同上,运行工程至断点处
  • 现在我们只知道alloc符号,直接添加alloc符号断点
image.png
  • 点击跳过上面断点
image.png image.png

直接锁定libobjc.A.dylib +[NSObject alloc],为找到objc_alloc底层源码来自于哪个动态库提供了线索!

汇编结合底层源码调试分析

苹果开源源码汇总: https://opensource.apple.com
Source Browser -> 找到objc4
这里查看的源码是objc4-818.2.tar.gz

image.png
  • 打开源码项目objc4-818.2,搜索alloc查看alloc源码执行的详细流程:
image.png
  • 进入_objc_rootAlloc方法
image.png
  • 进入callAlloc方法
image.png
  • 这里有#if __OBJC2__判断,如何验证执行_objc_rootAllocWithZone还是执行objc_msgSend? 对上面的每一个方法添加符号断点进行验证,最终发现先执行_objc_rootAllocWithZone
image.png
  • 进入_class_createInstanceFromZone方法
image.png
alloc + init 整体源码的探索流程
alloc + init 整体源码的探索流程

编译器优化

编译器优化设置 BuildSetting -> Optimization level(GCC_OPTIMIZATION_LEVEL)指定生成的代码针对速度和二进制大小进行优化的程度

设置 参数
None[-O0] 编译器不会优化代码。编译器的目标是蒋迪编译成本并使调试产生预期的结果,通常在Debug模式下使用。
Fast[-O,O1] 快速,优化编译器需要编译的时间更久,对大型函数需要更多的内存。编译器会尝试减少代码大小和执行时间,而不执行任何需要大量编译时间的优化。
Faster[-O2] 更快速,编译器执行几乎所有不涉及空间速度权衡的受支持优化。使用此设置,编译器不会执行循环展开或函数内联或寄存器命名,次设置会增加编译时间和生成代码的性能。
Fastest[-O3] 设置指定的所有优化,并打开函数内联和寄存器重命名选项,此设置可能会产更大的二进制文件
Fastest,Smallest[-Os] 最快、最小,此设置启用所有通常不会增加代码大小的更快的优化,它还会做减少代码大小的进一步优化

创建新工程编写代码如下,并打开汇编模式Debug->Debug wrokflow->Always Show Disassembly

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

//MARK: - 测试函数
int lgSum(int a, int b){
    return a+b;
}

int main(int argc, char * argv[]) {
    int a = 10;
    int b = 20;
    int c = lgSum(a, b);
    NSLog(@"查看编译器优化情况:%d",c);
    return 0;
}
优化模式
  • None[-O0]
    不优化的情况下所有信息在寄存器中显示完整,分别打印a、b、计算前后x0寄存器的值结果如下:
image.png
  • Fastest,Smallest[-Os]
image.png

执行结果:优化掉了a、b两个变量,甚至连lgSum函数都被优化掉了,只剩下了一个结果0x1e存在w8寄存器中。

得出结论:
  • 由于选择了Fastest,Smallest[-Os]优化方案,导致lgSum函数没有了,同理callAlloc函数也是一样的。

alloc的主线流程

alloc源码的核心操作主要分为三部分

  • cls->instanceSize:计算需要开辟的内存空间大小
  • calloc:申请内存,返回地址指针
  • obj->initInstanceIsa:将 类 与 isa 关联
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());

    // Read class's info bits all at once for performance
    //一次性读取类的位信息以提高性能
    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对象进行新地址的赋值
        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对象与cls对象进行绑定关联
        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);
}
cls->instanceSize:计算所需内存大小
  • instanceSize这个函数决定开辟内存,进入到这个函数,首先判断是否有缓存
  • 如果有执行cache.fastInstanceSize函数,打断点执行到align16,这个方法是16字节对齐算法。执行完成之后内存开辟结束,获得该对象内存大小。
  • 如果没有缓存,会执行alignedInstanceSize函数,执行word_align函数,此函数的参数是函数unalignedInstanceSize,而这个函数通过data()->ro()->instanceSize获取到对象的实例大小,也就是说,最终开辟内存空间的大小是根据对象的成员变量大小决定的。
    默认情况下,不创建任何成员变量,类开辟的内存空间是8字节,因为继承NSObject造成的,NSObject内有成员变量isa,由于isa的类型是结构体指针,所以isa是8字节,所以创建一个新的对象,没有任何成员变量,默认内存大小是8字节

字节对齐及其原理

字节对齐优势以空间换取时间
  • 8字节来自于NSObject对象的isa结构体指针
  • 不满16等于16
  • 如果大于16会根据对象在内存分布中的特性来决定(根据传入的x,取x的整数倍),如果传入8,最后得到的是8的倍数
#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

//字节对齐算法
//define WORD_MASK = 7
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

(8 + 7) & ~7 -> 15 & ~7 -> 8字节对齐,取8的整数,这里为什么是8的倍数?而不是16 32的倍数?因为只有8字节的指针,double类型,没有16字节或者32字节的数据类型

对象的内存对齐

<!-- Person.h -- >
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic) long height;
@property (nonatomic, copy) NSString *nickName;

- (void)saySomething;

@end

NS_ASSUME_NONNULL_END

<!-- Person.m -- >
#import "Person.h"

@implementation Person
- (void)saySomething{
    NSLog(@"%s",__func__);
}
@end

<!-- main.m类中使用Person类 -- >
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [Person alloc];
        NSLog(@"%@",p);
    }
    return 0;
}
  • 添加断点
添加断点
  • lldb调试打印
<!-- 源码中查看ISA_MASK -->
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD 
lldb调试
<!-- main.m类中使用Person类 -- >
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [Person alloc];
        p.name      = @"hello";
        p.nickName  = @"h";
        p.age       = 18;
        p.height    = 180;
        NSLog(@"%@",p);
    }
    return 0;
}

<!-- NSLog打印这一行添加断点,进行lldb调试 -->
// x/4gx 表示 以16进制的格式打印4段
(lldb)  x/4gx p
0x1006781a0: 0x011d800100008489 0x0000000000000012
0x1006781b0: 0x0000000100004010 0x00000000000000b4
(lldb) po 0x0000000000000012
18
(lldb) po 0x0000000100004010
hello
(lldb) po 0x00000000000000b4
180

<!-- 修改Person类height属性为Bool -- >
@property (nonatomic) long height;  -> @property (nonatomic) BOOL height;
<!-- 修改p对象height为1,打断点运行 -- >
p.height    = 1;
(lldb) x/4gx p
0x101b477a0: 0x011d800100008489 0x0000001200000001
0x101b477b0: 0x0000000100004010 0x0000000100004030
(lldb) po 0x0000001200000001
77309411329
(lldb) po 0x00000012
18
(lldb) po 0x00000001
1

通过上面打印我们发现int ageBOOL height放在同一处开辟的8字节内存空间中,这就是内存对齐,编译器进行了优化。

相关文章

  • OC对象原理探究(上)

    前言:作为一名已经工作5年iOS开发人员,突然发现自己在底层方面的知识是如此的薄弱,甚至对一个APP的启动细节的认...

  • OC对象原理探究(上)

    APP启动流程探索 创建空工程代码如下,并且添加符号断点命名如下图 运行工程查看堆栈信息 红色为app启动过程: ...

  • 初探OC对象原理(三)

    前言: 这是探究OC对象原理的第三章,也是按照对象的 的底层实现原理顺序来进行的。今天我们探究下对象的本质以及一...

  • iOS--OC底层原理文章汇总

    OC底层原理01—alloc + init + new原理OC底层原理02—内存对齐OC底层原理03— isa探究...

  • 【1】OC对象原理探究

    1)了解OC运行底层入口 通常是直接进入main函数,通过插入断点,在工作台运行bt命令,可以得知线程调用状态,如...

  • OC对象底层原理探究

    1.runtime 是什么?回答:runtime是由C C++ 汇编为oc提供的一套运行时的api 2.以下代码输...

  • OC对象原理探究(中)

    objc4-818.2源码编译调试不能过,请你检查以下几点 Build Setting -> enable har...

  • OC对象原理探究(下)

    介绍正文前,我们思考一个问题,什么是对象?或者说OC对象的本质是什么? 对象本质以及拓展 在探索oc对象本质前,先...

  • OC 与 Swift

    OC对象的本质(上):OC对象的底层实现原理OC对象的本质(中):OC对象的种类OC对象的本质(下):详解isa&...

  • OC对象的本质(中)—— OC对象的种类

    OC对象的本质(上):OC对象的底层实现原理OC对象的本质(中):OC对象的种类OC对象的本质(下):详解isa&...

网友评论

    本文标题:OC对象原理探究(上)

    本文链接:https://www.haomeiwen.com/subject/iqynultx.html