【iOS】OC类与对象的本质分析

news2024/11/27 11:32:56

目录

    • 前言
    • clang常用命令
    • 对象本质探索
    • 属性的本质
    • 对象的内存大小
    • isa 指针探究


前言

OC 代码的底层实现都是 C/C++代码,OC 的对象都是基于 C/C++ 的数据结构实现的,实际 OC 对象的本质就是结构体,那到底是一个怎样的结构体呢?

clang常用命令

clang是一个C语言、C++、Objective-C语言的轻量级编译器,是由Apple主导编写的
clang主要用于把源文件编译成底层文件,比如把main.m 文件编译main.cppmain.o或者可执行文件

我们可以在终端使用 clang 命令使OC代码转换成 C++ 代码:

# 将 main.m 转换成 main.cpp 文件
clang -rewrite-objc main.m -o main.cpp

打开此 main.cpp 文件,共有 6 万多行代码:

在这里插入图片描述

实际上,不同平台支持的代码肯定是不一样的:Windows 、 MacOS 、 iOS 等,模拟器(i386)、 32bit(armv7)、 64bit(arm64)等,如果要指定平台(比如在 arm64 下的 iOS 开发),应在终端输入以下xcrun命令:

# xcrun 命令基于 clang 进行了封装更好用

# 真机编译
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
# 模拟器编译
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

打开生成的 main-arm64.cpp 文件:

请添加图片描述

行数缩减到了 2 万多行,显然删减掉了冗余的代码

对象本质探索

将含有以下代码的 OC 文件转换成 Cpp 文件查看源码:

OC 代码:

@interface Student : NSObject {
    @public
    int _no;
    int _age;
}

@end

Cpp 代码:

在这里插入图片描述

可以看到 Student 的定义(IMPL,即 implementation 实现的意思)其实是 struct ,也就是对象的本质是结构体
在此结构体的定义中,有一个struct NSObject_IMPL结构体变量NSObject_IVARS,表明 Student 继承于 NSObject 类(子类的结构都包含父类的结构),即包括 NSObject 的成员变量

在这里插入图片描述

struct NSObject_IMPL的定义中,可以看到只有isa一个成员,其类型是 Class ,我们再找到 Class 的定义:

在这里插入图片描述

Class类型实际是一个objc_class类型的结构体指针,我们还可以看到常用的id是objc_object结构体指针类型,以及SEL是objc_selector(函数)的一个结构体指针

属性的本质

@interface Person : NSObject
@property (nonatomic, assign)int height;  //  自动生成实例变量、 setter/getter 方法
@end

@implementation Person
@end

那么 setter/getter 方法在哪儿呢?
创建出来的实例对象里面只存有实例变量,方法不会放在实例对象里面,而放在类对象中,有关对象的分类因为方法是公用的,方法的代码都是一样的

对象的内存大小

现在我们知道对象的本质其实就是一个结构体,其内存大小与成员变量有关,即isa+其他成员变量。对象第 1 个成员变量就是isa结构体指针,而结构体的地址就是第 1 个成员变量的地址,即isa变量的地址

下面我们来看看一个NSObject对象占用多少内存,NSObject只有一个成员变量,也就是结构体指针变量isa

在 C/C++ 中,结构体指针的内存占用大小通常取决于操作系统和计算机的体系结构,与结构体本身的内容和大小无关。具体来说:

  • 在32位系统上,指针通常占用4字节。
  • 在64位系统上,指针通常占用8字节。
    这个大小是指针自身的大小,与指向的结构体中包含的数据类型或数量无关。指针的主要功能是存储内存地址,所以它的大小应该与系统的地址长度一致

现在绝大多数计算机的都是 64 位,所以 isa 变量占用 8 个字节,也就是说 NSObject 对象占用 8 字节

检验对象到底占用了多少内存,引入以下两个库:

#import <objc/runtime.h>
#import <malloc/malloc.h>

使用其中的 API :

NSObject* obj = [NSObject alloc] init];

//  下面两个函数的返回值都是size_t(unsigned int),所以占位符使用%zd
NSLog(@"%zd", class_getInstanceSize([NSObject class]));

//NSLog(@"%zd", malloc_size(CFBridgingRetain(obj)));
NSLog(@"%zd", malloc_size((__bridge const void *)(obj)));  //OC转C 要进行桥接__bridge

运行结果:

在这里插入图片描述

刚分析 NSObject 对象不应该占用 8 字节吗?为什么第 2 个函数返回了 16 个字节?

实际class_getInstanceSize()返回的是类实例对象的成员变量大小,malloc_size()返回的是传入对象指针所指向的内存大小

下面来看看class_getInstanceSize()的实现:

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

在这里插入图片描述

查看最终的返回值,如果字节数小于 16 ,通过这种方法返回的结果就是 16,可见给 NSObject 分配了 16 个字节,但真正利用起来的只有 8 个字节(用来存放 isa 成员)
注释也有提到:

  1. CoreFoundation框架要求所有对象至少占用 16 字节,这种硬性规定
  2. 返回的是内存对齐后的成员变量所占内存大小

所以一般不去过多地关注此函数,而是通过malloc_size()准确的了解到对象占用内存大小

打断点通过 LLDB 查看内存分布:

在这里插入图片描述

上面以十六进制的形式打出:

LLDB 指令:
print 、 p:打印
po:打印对象
——
读取内存分布:memory read/数量 格式 字节数 内存地址x/数量 格式 字节数 内存地址
格式:x是 16 进制,f是浮点,d是 10 进制;字节大小:b是byte 1字节,w是word 4字节,h是half word 2字节,g是 giant word 8字节
——
修改内存中的值:memory write 内存地址 数值

通过 ViewMemory 实时查看内存数据:

在这里插入图片描述

在此处输入内存地址:
在这里插入图片描述
在这里插入图片描述

不论是通过 LLDB 还是 ViewMemory 都可以查看到标灰的位置就是obj对象的内存分布,后面的都是其他不相关的内存分布
由于一个 16 进制位代表 4 个二进制位,两个就代表 8 个二进制位(1 个字节)
71 20 32 E3 01 00 00 01 00 00 00 00 00 00 00 00,16 个字节中实际就只有 8 个字节有数据(成员变量,这里指 isa 指针变量)
0xe3322071 0x01000001 0x00000000这是地址分布,怎么跟上面的顺序是相反的呢?这里涉及到一个大小端的问题:

大小端(Endian)是指数据在计算机内存中的存储方式,具体来说是多字节数据的存储顺序问题。它主要涉及两种模式:大端(Big Endian)和小端(Little Endian)。
大端: 数据的高位字节存储在内存的低地址端,低位字节存储在高地址端。也就是说,数据的第一个字节(最重要的字节)存储在起始地址上。例如,如果有一个32位的整数0x12345678存储在地址0x1000开始的位置上,其存储顺序如下:

  • 0x1000: 0x12
  • 0x1001: 0x34
  • 0x1002: 0x56
  • 0x1003: 0x78
    大端模式直观地反映了数据在文档和其他数字表示中的顺序,因此在阅读内存转储时容易理解。很多网络协议(如TCP/IP)采用大端模式来传输数据。

小端: 数据的低位字节存储在内存的低地址端,高位字节存储在高地址端。也就是说,数据的最后一个字节(最重要的字节)存储在起始地址上。同样地,如果有一个32位的整数0x12345678存储在地址0x1000开始的位置上,其存储顺序如下:

  • 0x1000: 0x78
  • 0x1001: 0x56
  • 0x1002: 0x34
  • 0x1003: 0x12
    小端模式在某些类型的处理器(如x86架构)中使用,因为它可以在处理器将数据加载到寄存器时减少字节重排的操作。

一些系统允许在大端和小端之间进行配置,这种灵活性在处理多种硬件平台时特别有用。同时,还存在一些更复杂的端模式,例如双端模式或中间端模式,这些通常用于特定的应用或硬件。
端序的理解对于开发者在处理字节级操作时至关重要,尤其是在进行网络编程、读写文件以及在不同架构之间迁移代码时。不匹配的字节序会导致数据解释错误,从而引发程序错误和数据损坏。因此,开发者必须确保在这些操作中考虑到端序的因素。

总结: 创建一个实例对象,至少需要内存class_getInstanceSize(对齐后),实际分配内存malloc_size

最后以下例说明内存对齐:

struct Person_IMPL {
    struct NSObject_IMPL NSOBJECT_IVARS;  // 8
    int _age;  // 4
    int _height;  // 4
    int _no;  // 4
};  //  和为20,内存对齐交后:24

@interface Person : NSObject {
    int _age;
    int _height;
    int _no;
}

@end

@implementation Person

@end

void testAllocSize(void) {
    NSLog(@"%zd", sizeof(struct Person_IMPL));  //同class_getInstanceSize 24
    // sizeof 运算符关键字 不是一个函数 编译过程中就会查看传入的类型并识别数据类型字节大小 编译时就会确定为一个常数
    
    //  内存分配注意的地方
    Person* person = [[Person alloc] init];
    NSLog(@"%zd %zd", class_getInstanceSize([Person class]), malloc_size((__bridge const void *)(person)));  // 24 32
    // 操作系统在分配内存时,也会有“内存对齐”,所以 size 是 24,返回 32
    
    NSLog(@"%zd", sizeof(person));  // 8 指的是person指针本身的大小
}

isa 指针探究

之前在【iOS】alloc、init和new原理文章中提到了对象通过obj->initInstanceIsa初始化关联isa指针,下面就来看看isa的结构,如何通过它关联类

initInstanceIsa方法

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

initIsa方法

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);  //  isa初始化

    if (!nonpointer) {  //  非nonpointer指针直接绑定cls
        newisa.setClass(cls, this);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());


#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
#if ISA_HAS_INLINE_RC
        newisa.extra_rc = 1;
#endif
    }

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    // ...but not too atomic because we don't want to hurt instantiation
    isa() = newisa;
}

initIsa方法中,如果是非nonpointer类型指针,则直接通过clssetClass方法绑定;否则现有一些位域bits赋值操作,再调setClass方法绑定

我们可以看到方法内创建了一个 isa_t 类型的newisa实例, 做了赋值操作newisa.bits = ISA_MAGIC_VALUE;后,返回了newisa
注释中写得很清晰,news.bits初始化时候只设置了nonpointermagic两个部分,其余部分都没有进行设置,故值都为默认值0
该源码解释了上面初始化isa的两种方式:

  • 通过cls初始化:非nonpointer,存储着Class、Meta-Class对象的内存地址信息
  • 通过bits初始化:nonpointer,进行一系列的初始化操作

请添加图片描述

isa_t联合体

#include "isa.h"

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

#if ISA_HAS_INLINE_RC
    bool isDeallocating() const {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif // ISA_HAS_INLINE_RC

#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated) const;
    Class getDecodedClass(bool authenticated) const;
};

isa_t是一个联合体类型,其中的变量有bitscls以及结构体里面的ISA_BITFIELD宏定义变量,ISA_BITFIELD结构如下,是按位域定义的:

在这里插入图片描述

各个变量的内存分布图:

在这里插入图片描述

各个变量的解释:

  • nonpointer:表示是否对 isa 指针开启指针优化,0:纯isa指针,1:不止是类对象地址,isa中包含了类信息、对象的引用计数等
  • has_assoc:关联对象标志位,0没有,1存在
  • has_cxx_dtor:该对象是否有C++Objc的析构器,如果有析构函数,则需要做析构逻辑,没有则可以更快的释放对象
  • shiftcls:存储类指针的值,开启指针优化的情况下,在ARM64架构中有33位来存储类指针值
  • magic:用于调试判断当前对象是真的对象还是未初始化的空间
  • weakly_referenced:标志对象是否被指向或曾经指向一个ARC的弱变量,没有弱引用对象则可以更快地释放
  • unused:对象是否释放
  • has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位
  • extra_rc:对象的引用计数值(实际上是引用计数减1),例如:引用计数是10,那么extra_rc为9,如果引用计数大于10,则需要使用到has_sidetable_rc

总结

  • isa正是通过以上联合体中的变量信息将类关联起来
  • isa指针分为nonpointer和非nonpointer类型,非nonpointer类型(0)就是一个纯指针,nonpointer类型(1)指针开启了指针优化
  • 指针优化的设计,更好地利用isa的内存,程序中有大量的对象,针对isa的优化节省了大量内存

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1929599.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

glibc-all-in-one+patchelf修改程序libc

主要是做堆的时候经常遇到libc小版本不对导致libcbase不对打不通的情况&#xff0c;再者&#xff0c;每个题换一个ubuntu虚拟机属实麻烦&#xff0c;所以还是回到最初也是最好用的做法&#xff1a;patch libc。 核心就是两个工具&#xff1a;glibc-all-in-one和patchlef。但是…

buuctf-web

查看后端源码 得到base64编码&#xff0c;解码得flag

pc端注册页面 密码校验规则

1.密码校验规则 格应包含大小写字母、数字和特殊符号,长度为8-20 var validateRetrievePassword (rule, value, callback) > {let reg /^(?.*[A-Za-z])(?.*\d)(?.*[~!#$%^&*()_<>?:"{},.\/\\;[\]])[A-Za-z\d~!#$%^&*()_<>?:"{},.\/\\;…

WSL-Ubuntu20.04训练环境配置

1.YOLOv8训练环境配置 训练环境配置的话就仍然以YOLOv8为例&#xff0c;来说明如何配置深度学习训练环境。这部分内容比较简单&#xff0c;主要是安装miniAnaconda以及安装torch和torchvision. 首先是miniAnaconda的安装(参考官网的教程Miniconda — Anaconda )&#xff0c;执行…

开发笔记 | 快速上手[法大大]电子合同SDK使用SpringBoot+JAVA

Springbootmavenjava 官方API文档 API - 法大大电子合同和电子签云平台 官方SDK下载 API - 法大大电子合同和电子签云平台 目录 目录 开发前准备 项目整合 功能1&#xff1a;查询文档模板列表 功能2&#xff1a;文档模板字段填充 开发前准备 1.点下方链接注册法大大测试环…

昇思25天学习打卡营第12天|munger85

基于MindSpore通过GPT实现情感分类 这个实现情感分类意思就是通过一些电影的数据最后知道他对于这个电影的评价&#xff0c;最后知道他对于这个电影的评价到底是好还是不好&#xff0c;零就是不好&#xff0c;一就是好。首先我们肯定是按安装这些依赖包了为了今天这个模型我们…

Postman、Apifox、Apipost用哪个?

Postman、Apifox、Apipost都是流行的API接口管理工具&#xff0c;它们各自具有不同的特点和优势&#xff0c;因此哪个更好用取决于具体的使用场景和需求。以下是对这三个工具的比较分析&#xff1a; 一、Postman 特点与优势&#xff1a; 支持多种请求方式&#xff1a;包括GE…

游戏分组(DFS)

游戏分组&#xff08;DFS&#xff09; 将10名参赛者根据其游戏水平评分分为实力尽量相近的两队。 深度优先搜索&#xff08;DFS&#xff09;是游戏分组中常用的一种算法思路。 DFS在解决特定类型的分组问题时&#xff0c;特别是需要遍历所有可能组合的情况&#xff0c;表现出了…

一文详解:医疗营销升级的智能解决方案

顺境是所有人的狂欢&#xff0c;逆境才是优秀者的天堂。淘金的时代过去了&#xff0c;未来是冶金的时代。 01、享受完改革开放40年的高速区间红利 企业正处于中速区间的全面竞争期 1978年&#xff0c;中国的GDP是3679亿。改革开放40多年&#xff0c;我们不断引进资本&#xf…

【嵌入式Linux】<总览> 网络编程(更新中)

文章目录 前言 一、网络知识概述 1. 网路结构分层 2. socket 3. IP地址 4. 端口号 5. 字节序 二、网络编程常用API 1. socket函数 2. bind函数 3. listen函数 4. accept函数 5. connect函数 6. read和recv函数 7. write和send函数 三、TCP编程 1. TCP介绍 2.…

Monaco 使用 DocumentFormattingEditProvider

文档格式化&#xff0c;是 VSCode 比较常用的功能&#xff0c;在文档上点击右键选择格式化文档。效果如下&#xff1a; 在 Monaco 通过 registerDocumentFormattingEditProvider 方法注册处理函数&#xff0c;实现 provider 方法。 provider 方法返回格式化好的代码。 TextE…

Android C++系列:Linux文件系统(二)

1. VFS虚拟文件系统 Linux支持各种各样的文件系统格式&#xff0c;如ext2、ext3、reiserfs、FAT、NTFS、iso9660 等等&#xff0c;不同的磁盘分区、光盘或其它存储设备都有不同的文件系统格式&#xff0c;然而这些文件系统 都可以mount到某个目录下&#xff0c;使我们看到一个…

Kafka(四) Consumer消费者

一&#xff0c;基础知识 1&#xff0c;消费者与消费组 每个消费者都有对应的消费组&#xff0c;不同消费组之间互不影响。 Partition的消息只能被一个消费组中的一个消费者所消费&#xff0c; 但Partition也可能被再平衡分配给新的消费者。 一个Topic的不同Partition会根据分配…

【C#】部分国家/语言,string字符串转decimal、float时,小数点解析异常、小数点丢失、小数点被忽略

现象&#xff1a; 部分国家地区&#xff0c;字符串转小数后&#xff0c;小数点丢失&#xff0c;比如&#xff1a;输入"12.34"&#xff0c;输出1234&#xff0c;而非12.34。 部分相关函数decimal.Parse、decimal.TryParse、float.Parse、float.TryParse 原因&…

【Linux】常用命令总结(updating)

1.date2.du&#xff08;disk use&#xff09;3.df&#xff08;disk free&#xff09;4.find5.crontab6.netstat shell命令可以使用man查看命令文档说明&#xff0c;说明界面中可通过b(backward)向上翻页&#xff0c;f(forward)向下翻页&#xff0c;g(go to)跳到说明首页&#x…

【问题记录】Docker配置mongodb副本集实现数据流实时获取

配置mongodb副本集实现数据流实时获取 前言操作步骤1. docker拉取mongodb镜像2. 连接mongo1镜像的mongosh3. 在mongosh中初始化副本集 注意点 前言 由于想用nodejs实现实时获取Mongodb数据流&#xff0c;但是报错显示需要有副本集的mongodb才能实现实时获取信息流&#xff0c;…

springboot老年慢性病药物管理系统-计算机毕业设计源码70568

目录 摘要 Abstract 第一章 绪论 1.1 选题背景及意义 1.2 国内外研究现状 1.3 研究方法 第二章 相关技术介绍 2.1 MySQL简介 2.2 Java编程语言 2.3 B/S模式 2.4 springboot框架 第三章 老年慢性病药物管理系统 系统分析 3.1 系统目标 3.2 系统可行性分析 3.2.1 技…

【linux】服务器ubuntu安装cuda11.0、cuDNN教程,简单易懂,包教包会

【linux】服务器ubuntu安装cuda11.0、cuDNN教程&#xff0c;简单易懂&#xff0c;包教包会 【创作不易&#xff0c;求点赞关注收藏】 文章目录 【linux】服务器ubuntu安装cuda11.0、cuDNN教程&#xff0c;简单易懂&#xff0c;包教包会一、版本情况介绍二、安装cuda1、到官网…

Java面试八股之Redis哨兵机制

Redis哨兵机制 Redis Sentinel&#xff08;哨兵&#xff09;模式是一种高可用解决方案&#xff0c;用于监控和自动故障转移Redis主从集群。以下是对哨兵模式详细过程的描述&#xff1a; 1. 初始化与配置 部署哨兵节点&#xff1a;在不同的服务器上部署一个或多个Redis Sentin…

链表题目专题

19. 删除链表的倒数第 N 个结点 给定一个链表&#xff0c;删除链表的倒数第 n 个节点&#xff0c;并且返回链表的头结点。 非递归解决 这题让删除链表的倒数第n个节点&#xff0c;首先最容易想到的就是先求出链表的长度length&#xff0c;然后就可以找到要删除链表的前一个结…