当前位置: 首页 > news >正文

OC-初识NSArray的底层逻辑

NSArray的底层逻辑

文章目录

  • NSArray的底层逻辑
    • 占位符
    • init后的空数组
    • 只有单个元素的NSArray
    • 大于一个元素的NSArray
    • 可变数组NSMutableArray
      • 存储方式
    • 类簇
    • 遍历NSArray
      • for循环
      • 枚举
      • for-in
      • 多线程下的遍历方法
      • 性能分析
      • 分析

首先我们看一下下面这段代码的输出:

int main(int argc, const char * argv[]) {@autoreleasepool {NSArray* placeHolder = [NSArray alloc];NSArray* arr1 = [[NSArray alloc] init];NSArray* arr2 = [[NSArray alloc] initWithObjects:@1,nil];NSArray* arr3 = [[NSArray alloc] initWithObjects:@1, @2, nil];NSArray* arr4 = [[NSArray alloc] initWithObjects:@1, @2, @3,nil];NSLog(@"placeHolder: %s", object_getClassName(placeHolder));NSLog(@"arr1: %s", object_getClassName(arr1));NSLog(@"arr2: %s", object_getClassName(arr2));NSLog(@"arr3: %s", object_getClassName(arr3));NSMutableArray* mplaceHolder = [NSMutableArray alloc];NSMutableArray* marr1 = [[NSMutableArray alloc] init];NSMutableArray* marr2 = [[NSMutableArray alloc] initWithObjects:@1, nil];NSMutableArray* marr3 = [[NSMutableArray alloc] initWithObjects:@1, @2, nil];NSMutableArray* marr4 = [[NSMutableArray alloc] initWithObjects:@1, @2, @3, nil];NSLog(@"mplaceHolder: %s", object_getClassName(mplaceHolder));NSLog(@"marr1: %s", object_getClassName(marr1));NSLog(@"marr2: %s", object_getClassName(marr2));NSLog(@"marr3: %s", object_getClassName(marr3));}return 0;
}

在这里插入图片描述

占位符

根据上面的输出我们发现,NSArray和NSMutableArray的alloc结果都是获得一个名为_NSPlaceholderArray的类,顾名思义这个类就是用来进行占位的。

        NSArray* placeHolder2 = [NSArray alloc];NSMutableArray* mplaceHolder2 = [NSMutableArray alloc];NSLog(@"placeHolder: %p", placeHolder);NSLog(@"placeHolder2: %p", placeHolder2);NSLog(@"mplaceHolder: %p", mplaceHolder);NSLog(@"mplaceHolder2: %p", mplaceHolder2);

在这里插入图片描述

我们输出一下地址发现,这两种占位符的地址是相同的.

当元素为空时,返回的是_NSArray0的单例;

为了区分可变与不可变类型,在init的时候,会根据是NSArray还是NSMutableArray来创建immutablePlaceHolder和mutablePlaceholder,这两种都是_NSPlaceholderArray类型的

init后的空数组

对于空的NSArray来说,由于NSArray是不可变的,也就是说空的NSArray只有一种(即单例),我们可以通过打印NSArray对象的引用计数值来判断。即调用对象的retainCount方法,注意需要关闭ARC。
如果输出一个非常大的数,那么就能说明这是一个单例类的对象了

只有单个元素的NSArray

对于单个元素的NSArray的数组,我们打印出的类名是_NSSingleObjectArrayI,其结构体定义如下:

@interface __NSSingleObjectArrayI : NSArray {id Object;
}

大于一个元素的NSArray

对于大于一个元素的NSArray,输出的类名是__NSArrayI,其结构体定义如下:

@interface __NSArrayI : NSArray {NSUInteger _used;id _list[0];
}

其中的_used是数组元素的个数,调用数组的count方法时,返回的就是_used值。

其中的id_list[0]可以看成id*_list(在内存机制上还是有区别的),被称为柔性数组

当我们定义一个结构体,并在结构体末尾放置一个大小为0的数组时,编译器会将这个数组视为柔性数组,这个柔性数组的大小是在运行时根据需要动态分配内存来确定的。因此,通过访问结构体的柔性数组成员,实际上可以访问结构体后面的额外分配的内存空间,这样就能实现动态长度的数组

我们简单了解下柔性数组的注意事项:

  • 内存是连续的,结构体系和数据在同一块连续内存中,malloc一次,free一次(如果使用指针来存储的话,结构体和数据是两块不连续的内存,需要malloc两次和free两次)
  • 必须动态分配内存,柔性数组是不能在栈上定义的。
  • 只需释放一次内存,结构体和柔性数组的内存是连续的
  • 需要自己控制变量长度,避免出现数组越界的问题

可变数组NSMutableArray

通过上面的代码和运行结果我们可以发现,无论什么情况,我们alloc、init创建出的数组类名都是_NSArrayM,因为可变数组是可以随意添加和删除数据的,其结构体如下:

@interface __NSArrayM : NSMutableArray {NSUInteger _used;	NSUInteger _offset;int _size:28;int _unused:4;uint32_t _mutations;id* _list;
}

可变数组的内部也是一块连续内存id* _list,正如__NSArrayI的id _ _list[0]一样。但是注意内存的连续状态

_used:当前对象数目

_offset:实际对象数组的起始偏移

_size: 已分配的 _ list大小(能储存的对象的个数)使用位域存储设计,用28比特位存储容量,最大可表示2^28-1个元素

mutations:修改标记,每次对 _NSArrayM的修改操作都会使 _mutations加1

id* list是一个循环数组,并且在增删操作时会动态地重新分配以符合当前的存储需求

_unused:预留的4位空间,为后面使用做准备

存储方式

_NSArrayM底层通过一块连续的线性内存模拟环形结构,我们可以将指向 _list的内存想象成一个首尾相接的圆环,元素从 _offset位置开始存储,当存储到内存末尾时,会绕到内存开头继续存储。(实际线性,模拟环形)

类簇

在这里插入图片描述

遍历NSArray

for循环

for (int i = 0; i < arr.count; i++) {id object = arr[i];
}

枚举

我们使用OC提供的枚举器实现便利数组

NSEnumrator* enumrator = [array objectEnumrator];
id object;
while (object = [enumrator nextObject]) {	//执行相关操作
}

for-in

for (id object in array) {//执行相关操作
}

多线程下的遍历方法

[array enumrateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL* _Nonnull stop){//
}];

NSArray提供该方法用于遍历数组中的所有对象。

  • 参数Options:指定遍历的选项,Concurrent就是并发遍历

  • usingBlock:会在遍历过程中对数组中的每个元素执行。

    • obj:当前元素
    • idx:当前元素索引
    • stop:是一个指向BOOL类型的指针,控制遍历是否停止,通过修改为YES终止遍历

系统会启动多个线程,并分配给每个线程一部份元素进行处理,这些线程在并发的方式下同时执行usingBlock,每个线程遍历不同元素,不必等待其他线程

性能分析

图示我们可以发现,随着数据的增多,不同遍历方式的性能差别愈发明显,我们觉得的并发遍历方式的耗时确实最高的,反而for-in遍历的性能比较好。

分析

在这里插入图片描述
这里我们直接看结论:

  • for-in:背后使用的是快速枚举机制(fast enumeration),在底层优化了迭代器,避免了传统for循环可能产生的开销
    • 例如,NSArray数组会将元素保存在连续的内存块中,这样,CPU可以利用缓存局部性,提高访问速度。相对而言,传统的for循环需要进行复杂的索引计算,损失性能
    • 在NSArray或者字典中,数据通常是顺序存储或者通过哈希表存储,以此来有效利用CPU缓存
    • 使用引用传递避免不必要的对象拷贝,就是直接去访问对象。
  • 对于多线程的并发遍历方法,虽然在大量数据下性能消耗很大,但是其采用了多线程,这分担了主线程的压力,并且在需要对元素进行复杂处理的情况是非常合适。

由于笔者的能力有限,在学长的博客中学到了这些知识,对于更深层的内容等后面有能力了再继续拓展

http://www.dtcms.com/a/398614.html

相关文章:

  • AI技术:变革未来社会的关键驱动力,第878章
  • Kubernetes 证书监控--x509-certificate-exporter
  • MMD动画(六)Ray渲染--打光
  • 检察院网站建设方案erp系统是什么系统
  • a做爰网站o2o
  • 非关系型数据库-Redis
  • 从不确定性到确定性,“动态安全+AI”成网络安全破题密码
  • python+django/flask二手物品交易系统 二手商品发布 分类浏览 在线沟通与订单管理系统java+nodejs
  • 找事做的网站网站开发需要什么步骤
  • Go 语言切片和 Map 操作常用库指南
  • 旧项目适配Android15
  • DIC技术在极端条件下的应用及解决方案
  • nginx限制国外ip访问
  • OpenLayers地图交互 -- 章节十:拖拽平移交互详解
  • 北京壹零零壹网站建设网页设计教程视屏
  • 做一个商城网站需要什么流程做网站的一般多钱
  • Charles移动端调试教程,iOS与Android抓包、证书安装与优化技巧
  • PHP应用文件操作安全上传下载任意读取删除目录遍历文件包含(2024小迪安全Day32笔记)
  • 九江网站设计服务机构哪家好旅游网络营销的特点有
  • 【win10】Windows 任务管理器可以查看软件的虚拟内存使用情况
  • 一致性Hash算法:解决扩容缩容时数据迁移问题
  • Smol VLA是什么,怎么用
  • 人工智能医疗系统灰度上线与评估:技术框架实践分析python版(上)
  • 条款12:为意在重写的函数添加override声明
  • 如何自动生成ONNX模型?​​
  • 建设部网站江苏金安微信商城软件开发
  • 网站建设项目分析株洲做网站的
  • React Native:如何将原有的依赖复用给新的RN project?
  • WhisperLiveKit上手及主观评测
  • iOS 26 系统流畅度深度评测 Liquid Glass 动画滑动卡顿、响应延迟、机型差异与 uni-app 优化策略