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

目录

    • 前言
    • 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 {@publicint _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;  // 8int _age;  // 4int _height;  // 4int _no;  // 4
};  //  和为20,内存对齐交后:24@interface Person : NSObject {int _age;int _height;int _no;
}@end@implementation Person@endvoid 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,返回 32NSLog(@"%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指针直接绑定clsnewisa.setClass(cls, this);} else {ASSERT(!DisableNonpointerIsa);ASSERT(!cls->instancesRequireRawIsa());#if SUPPORT_INDEXED_ISAASSERT(cls->classArrayIndex() > 0);newisa.bits = ISA_INDEX_MAGIC_VALUE;// isa.magic is part of ISA_MAGIC_VALUE// isa.nonpointer is part of ISA_MAGIC_VALUEnewisa.has_cxx_dtor = hasCxxDtor;newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#elsenewisa.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_BITnewisa.has_cxx_dtor = hasCxxDtor;
#   endifnewisa.setClass(cls, this);
#endif
#if ISA_HAS_INLINE_RCnewisa.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 instantiationisa() = 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_RCbool isDeallocating() const {return extra_rc == 0 && has_sidetable_rc == 0;}void setDeallocating() {extra_rc = 0;has_sidetable_rc = 0;}
#endif // ISA_HAS_INLINE_RC#endifvoid 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.mzph.cn/diannao/46851.shtml

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

相关文章

IDEA实现SpringBoot项目的自打包自发布自部署

目录 前言 正文 操作背景 自发布 自部署 尾声 &#x1f52d; Hi,I’m Pleasure1234&#x1f331; I’m currently learning Vue.js,SpringBoot,Computer Security and so on.&#x1f46f; I’m studying in University of Nottingham Ningbo China&#x1f4eb; You can reach…

qt 创建一个左侧边线拖拽的矩形

1.概要 2.代码 2.1 代码第一版 在Qt中&#xff0c;要创建一个可以向左侧拖拽边线的矩形&#xff0c;你需要自定义一个QGraphicsRectItem的子类&#xff0c;并重写其事件处理函数来响应鼠标的拖拽动作。以下是一个简单的实现示例&#xff1a; #include <QApplication>…

设计模式——装饰者模式

设计模式——装饰者模式 1.问题1.1 方案一1.2 方案二 2.装饰者模式2.1 基本介绍2.2 结构2.3 代码实现 3.小结 1.问题 咖啡订单项目&#xff1a; 咖啡种类/单品咖啡:Espresso(意大利浓咖啡)、ShortBlack、LongBlack(美式咖啡)、Decaf(无因咖啡)调料:Milk、Soy(豆浆)、Chocolat…

GB35114控制信令认证流程

GB35114控制信令认证说明&#xff1a; 注册成功后,信令发送方与信令接收方进行交互时,采用基于带密钥的杂凑方式保障信令来源安 全&#xff61;对除REGISTER消息以外的消息做带密钥的杂凑&#xff61;启用Date字段,扩展信令消息头域,在头域中 增加 Note 字 段 (值 为 Digest…

项目经理到底要不要考PMP?

在接待PMP学员中我惊讶地发现&#xff0c;不仅是项目经理&#xff0c;连开发、测试、产品、运营、销售、甚至财务团队的朋友们也都在积极备考。他们考证的原因主要有这几点&#xff1a; 1&#xff0c;职业发展&#xff1a;希望在职业生涯中晋升或转型到项目管理角色的朋友来说…

Spring MVC 全注解开发

1. Spring MVC 全注解开发 文章目录 1. Spring MVC 全注解开发2. web.xml 文件 的替代2.1 Servlet3.0新特性2.2 编写 WebAppInitializer 3. Spring MVC的配置3.1 Spring MVC的配置&#xff1a;开启注解驱动3.2 Spring MVC的配置&#xff1a;视图解析器3.3 Spring MVC的配置&…

SourceTree rebase(变基)的使用

参考资料 【Sourcetree】コミットを一つにまとめる【Sourcetree】リベースする 目录 前提0.1 merge与rebase0.2 merge合并分支0.3 rebase合并分支0.4 &#x1f4a5;超级注意事项&#x1f4a5; 一. 代码已提交&#xff0c;未推送&#xff0c;交互式变基1.1 通过SourceTree操作1…

【NLP实战】基于TextCNN的新闻文本分类

TextCNN文本分类在pytorch中的实现 基于TextCNN和transformers.BertTokenizer的新闻文本分类实现&#xff0c;包括训练、预测、数据加载和准确率评估。 目录 项目代码TextCNN网络结构相关模型仓库准备工作项目调参预测与评估 1.项目代码 https://github.com/NeoTse0622/Te…

怎么选流量套餐最划算呢,这篇文章建议收藏!

据小编了解&#xff0c;现在大多数用户手上都不止一张SIM卡&#xff0c;大部分都是双卡&#xff0c;甚至三卡了&#xff0c;那么&#xff0c;这些卡槽你真的利用对了吗&#xff1f; 这篇文章就告诉大家&#xff0c;如何更好的利用这两个卡槽&#xff0c;让你即省钱&#xff0c…

(02)Unity使用在线AI大模型(调用Python)

目录 一、概要 二、改造Python代码 三、制作Unity场景 一、概要 查看本文需完成&#xff08;01&#xff09;Unity使用在线AI大模型&#xff08;使用百度千帆服务&#xff09;的阅读和实操&#xff0c;本文档接入指南的基础上使用Unity C#调用百度千帆大模型&#xff0c;需要…

十五、C++11常用新特性—Lambda表达式

1.基本 这个好像是很好用的&#xff0c;其有以下有点&#xff1a; 声明式的编程风格&#xff1a;直接匿名定义目标函数或函数对象&#xff0c;不需要额外写一个命名函数或函数对象。简洁&#xff1a;避免了代码膨胀和功能分散&#xff0c;让开发更加高效。在需要的时间和地点…

Sentieon应用教程 | 唯一分子标识符(UMI)

介绍 本文介绍了使用Sentieon工具处理下一代测序数据的方法&#xff0c;同时利用分子条码信息&#xff08;也称为唯一分子索引或UMI&#xff09;。分子条码可以在测序之前在模板DNA分子的末端引入唯一标签&#xff0c;从而大大减少PCR重复和测序错误对变异调用过程的影响。 S…

影视迷必备:揭秘高效影视app开发幕后

影视迷必备的高效影视APP开发幕后涉及多个关键环节&#xff0c;从需求分析、规划设计、技术开发到测试上线&#xff0c;再到后续的运营与维护&#xff0c;每一个环节都至关重要。 一、需求分析 在开发影视APP之前&#xff0c;首要任务是进行深入的需求分析。这一阶段的主要目标…

CSS选择器(1)

以内部样式表编写CSS选择器&#xff0c;其主要编写在<head></head>元素里&#xff0c;通过<style></style>标签来定义内部样式表。 基本语法为&#xff1a; 选择器{ 声明块 } 声明块&#xff1a;是由一对大括号括起来&#xff0c;声明块中是一个一个的…

python-矩阵加法(赛氪OJ)

[题目描述] 输入两个 n 行 m 列的矩阵 A 和 B &#xff0c;输出它们的和 AB。矩阵加法的规则是两个矩阵中对应位置的值进行加和&#xff0c;具体参照样例。输入&#xff1a; 输入共 2⋅n1 行&#xff0c;第一行包含两个整数 n 和 m&#xff0c;表示矩阵的行数和列数 (1≤n,m≤1…

艺术创作的新维度:yicaiai照片风格化

艺术创作的新维度&#xff1a;yicaiai照片风格化 一、用户友好的设计理念 1.1 yicaiai照片风格化的核心设计理念 yicaiai平台以其创新的AI技术&#xff0c;颠覆了传统照片处理的方式&#xff0c;将艺术与科技完美融合。其核心设计理念在于赋予普通照片无尽的艺术潜力&#xf…

这3种人适合学习人工智能,看看你在不在其中?

人工智能&#xff08;AI&#xff09;的浪潮正席卷全球&#xff0c;它不仅是科技领域的一场革命&#xff0c;更是社会进步的重要推手。随着AI技术的不断成熟和应用领域的不断拓展&#xff0c;越来越多的人开始关注并渴望掌握这一前沿技术。那么&#xff0c;究竟哪些人适合学习人…

华为od机试真题 — 测试用例执行计划(Python)

题目描述 某个产品当前迭代周期内有N个特性&#xff08;F1, F2, ..., FN&#xff09;需要进行覆盖测试&#xff0c;每个特性都被评估了对应的优先级&#xff0c;特性使用其ID作为下标进行标识。 设计了M个测试用例&#xff08;T1, T2,...,TM&#xff09;&#xff0c;每个用例…

Richtek立锜科技可用于智能门铃的电源管理解决方案

新型的智能门铃不仅能满足呼叫、提醒的需要&#xff0c;还能在线监控、远程操作、闯入通知、记录过程&#xff0c;系统构成相对复杂&#xff0c;与传统门铃相比有了很大的改变。 从电源管理的角度来观察&#xff0c;满足这样需求的系统构成也相对复杂&#xff1a; 处于外置状态…