ios面试八股文

Swift 中,for-in循环遍历数组时,默认是 "值遍历" 还是 "引用遍历"?如果想在遍历中修改数组元素(如将所有元素加 1),需要怎么做?请举一个具体例子(如遍历var nums = [1,2,3],修改后得到[2,3,4])。

在 Swift 中,for-in 循环遍历数组时,默认是值遍历(遍历的是数组元素的副本),而非引用遍历。这意味着在循环中直接修改遍历变量,不会影响原数组中的元素。

要在遍历中修改原数组的元素,需要通过索引访问元素(而非直接使用循环变量)。

复制代码
var nums = [1, 2, 3]
// 遍历索引,通过索引修改原数组元素
for i in nums.indices {
    nums[i] += 1  // 直接修改原数组中索引为 i 的元素
}

var nums = [1, 2, 3]
// 同时获取索引和元素,通过索引修改原数组
for (i, _) in nums.enumerated() {
    nums[i] += 1  // 忽略元素值,仅用索引修改
}

Objective-C 中,NSString *str1 = @"abc";NSString *str2 = [NSString stringWithFormat:@"abc"];创建的字符串,在内存存储上有什么区别?为什么前者更高效?(考察字符串常量池概念)

方式 存储位置 内存特性 生命周期
@"abc"(字符串字面量) 字符串常量池(属于进程的全局数据区,编译期分配) 不可变、共享复用 从程序启动到进程结束,全局存在
stringWithFormat:@"abc" 堆内存(运行期动态分配) 不可变、独立存在 受引用计数管理,无引用时被释放

对于@"abc"

@"abc" 是编译期就能确定的字符串字面量,编译器会将其存入字符串常量池(一个全局维护的字符串哈希表)。

若后续代码中再次使用 @"abc"(如 NSString *str3 = @"abc";),系统不会创建新字符串,而是让 str3 直接指向常量池中已有的 @"abc" 地址(即 str1str3 指针相同)。

对于后面的

stringWithFormat: 是运行期调用的方法,即使格式化结果与字面量相同(@"abc"),系统也会在堆内存 中创建一个新的 NSString 对象。该字符串与常量池中的 @"abc" 完全独立,拥有自己的内存地址

介绍一下swift中的枚举值

维度 原始值(Raw Value) 关联值(Associated Value)
定义时机 枚举定义时预定义,固定不变 枚举实例创建时动态传递,可变化
类型一致性 所有 case 必须同类型 不同 case 可不同类型
访问方式 通过 rawValue 属性直接访问 需通过 switch 模式匹配提取
核心作用 给 case 一个固定标识(映射外部常量) 给 case 动态附加数据(承载上下文)
初始化方式 可通过 枚举名(rawValue:) 初始化 需通过 case(关联值) 直接创建实例

结合方法、属性、泛型和协议,Swift 枚举可作为 "轻量级数据模型 + 逻辑封装" 的载体,在实际开发中(如网络回调、状态管理、事件分发)应用广泛。

iOS 的 "RunLoop" 是什么?它的核心作用是什么?请举例说明:为什么主线程的 RunLoop 不能退出?如果在主线程中写一个 "死循环",会对 App 有什么影响?

RunLoop是ios中的一套事件处理循环机制,本质上是一个无限循环的逻辑

每个线程对应于唯一一个RunLoop,但是子线程的RunLoop需要手动创建和启动

其核心作用在于保持线程存活,通过循环等待的机制,避免线程用完即毁

主线程负责所有 UI 渲染、用户交互、定时器执行 等关键任务,而这些任务都需要通过 RunLoop 分发处理

主线程的死循环(如 while(true) {})会 阻塞 RunLoop 的正常运行,导致 RunLoop 无法处理任何事件,最终使得应用无响应

UIKit 中,"UI 刷新" 为什么通常要在主线程执行?如果在子线程中直接修改 UI(如self.label.text = "Hello"),一定会崩溃吗?可能出现哪些异常现象(如 UI 不更新、布局错乱)

核心原因在于UIkit不是线程安全的(避免资源竞争),且主线程被设计为唯一负责UI调度的专属线程,主线程绑定了一个RunLoop,其核心职责就是调度所以UI相关的任务,包括处理用户交互,UI绘制,布局计算等

子线程中修改UI,也不一定会崩溃,但是是不安全的操作,存在不可预测的风险

其可能导致的异常现象包括UI不更新,布局错乱,控件状态异常等等

iOS 的 "沙盒(Sandbox)" 机制是什么?一个 App 的沙盒目录下,主要有哪几个核心文件夹(如 Documents、Library)?分别用来存储什么类型的数据(如用户生成的文件、缓存文件)?

沙盒本质上是一个独立的文件目录系统,每个App安装的时候,系统自动为其差创建一个唯一的沙盒目录,路径由系统随机生成,App无法修改

其核心作用在于三点:

  1. 隔离性:每个App的沙盒完全独立,App只能访问自己的沙盒u了,无法读取其他App的沙盒数据,除非通过系统允许的共享机制

  2. 安全性:限制App对系统资源的访问权限,避免恶意App窃取用户数据或者破坏系统

  3. 可管理性:系统可以通过沙盒统一管理App的数据,当用户删除App的时候,系统自动删除其对应的整个沙盒目录,避免残留垃圾文件

沙盒目录的核心文件及其用途

1.Documents:用户生成的重要数据,需要进行备份,存储用户主动生成的,需要长期保留的文件(如文档、编辑的内容、用户保存的文件等)

  1. Library:系统/App配置与缓存数据:包含两个核心子目录,preferences和Caches

Library/Preferences:主要用于存储App偏好设置的,包括用户的个性化设置,开关状态,账号信息等

Library/Caches存储临时缓存等数据(如网络请求缓存,图片缓存,离线资源等)

3.tmp存储临时文件(如临时生成的日志,临时下载的文件片段,处理中的数据等)

iOS 中常见的多线程方案有哪些?(如 GCD、NSOperationQueue、pthread)请对比 GCD 和 NSOperationQueue 的差异(如依赖管理、取消任务、优先级控制)。

包含GCD,NSOperationQueue,Nsthread,pthread

对比维度 GCD (Grand Central Dispatch) NSOperationQueue
底层实现 内核级调度(基于 mach 内核),C 语言函数式 API 基于 GCD 的 OC 面向对象封装,任务以 NSOperation 对象存在
依赖管理 无原生支持,需手动通过 dispatch_group/ 信号量 /dispatch_barrier 实现,代码繁琐 原生支持:通过 addDependency:/removeDependency: 直接设置操作间依赖,支持多对多依赖(如任务 B 依赖 A,任务 C 依赖 B)
任务取消 任务提交后 难以取消 : 1. 仅 dispatch_block_t 可通过 dispatch_block_cancel() 取消(需在执行前); 2. 执行中无法强制中断,只能通过 dispatch_block_testcancel() 主动检查 原生支持灵活取消: 1. 调用 -[NSOperation cancel] 发起取消请求; 2. 任务内通过 self.isCancelled 主动判断,可中断执行中任务
优先级控制 基于 QoS(Quality of Service) 控制队列优先级(如 QOS_CLASS_USER_INTERACTIVE > QOS_CLASS_DEFAULT),影响系统 CPU/IO 资源调度; 无法直接控制并发数(并发队列由系统管理) 双重优先级控制: 1. queuePriority:同一队列内操作的执行顺序(如 NSOperationQueuePriorityHigh); 2. qualityOfService:对应 GCD 的 QoS,影响系统调度; 支持通过 maxConcurrentOperationCount 设置最大并发数(设为 1 则为串行队列)
任务状态管理 无明确状态标识,仅能间接判断 "提交 / 执行中 / 完成",无法监听状态变化 原生支持状态监听: 通过 isReady/isExecuting/isFinished/isCancelled 查看状态,可通过 KVO 监听状态变化(如任务完成时触发回调)
任务复用性 任务是 block 或函数,无法复用,每次需重新定义 任务是 NSOperation 对象,可自定义子类(如继承 NSOperation 重写 main/start 方法),支持复用和扩展
适用场景 简单异步任务(如后台计算、主线程更新 UI)、追求高效调度 复杂任务管理(如多任务依赖、需取消任务、控制并发数)、需监听任务状态

当需求较为简单的时候,优先使用GCD无需额外对象管理

如何用 GCD 实现 "任务 A 执行完成后,再并发执行任务 B 和 C,最后执行任务 D"?(考察 dispatch_group 的用法)

复制代码
import Foundation

// 1. 创建并发队列(用于执行任务B和C,实现真正的并行)
let concurrentQueue = DispatchQueue(
    label: "com.example.concurrent",
    attributes: .concurrent
)

// 2. 创建调度组(监听任务B和C的完成状态)
let group = DispatchGroup()

// 3. 执行任务A(先于B和C执行)
print("开始执行任务A")
taskA()
print("任务A执行完成")

// 4. 任务A完成后,将B和C加入调度组并并发执行
// 任务B加入组
group.enter() // 手动标记任务开始(与leave成对出现)
concurrentQueue.async {
    defer {
        group.leave() // 任务完成后标记结束
    }
    print("开始执行任务B")
    taskB()
    print("任务B执行完成")
}

// 任务C加入组
group.enter() // 手动标记任务开始
concurrentQueue.async {
    defer {
        group.leave() // 任务完成后标记结束
    }
    print("开始执行任务C")
    taskC()
    print("任务C执行完成")
}

// 5. 监听组内任务(B和C)完成,然后执行任务D
group.notify(queue: DispatchQueue.main) { // 可指定D的执行队列(如主线程更新UI)
    print("任务B和C均已完成,开始执行任务D")
    taskD()
    print("任务D执行完成")
}

// 模拟任务A(同步执行,耗时操作)
func taskA() {
    Thread.sleep(forTimeInterval: 1) // 模拟耗时1秒
}

// 模拟任务B(异步执行,耗时操作)
func taskB() {
    Thread.sleep(forTimeInterval: 2) // 模拟耗时2秒
}

// 模拟任务C(异步执行,耗时操作)
func taskC() {
    Thread.sleep(forTimeInterval: 2) // 模拟耗时2秒
}

// 模拟任务D(在B和C完成后执行)
func taskD() {
    Thread.sleep(forTimeInterval: 1) // 模拟耗时1秒
}

// 保持程序运行(命令行工具环境)
RunLoop.main.run(until: Date().addingTimeInterval(10))
相关推荐
知其然亦知其所以然3 小时前
面试官微笑发问:第100万页怎么查?我差点当场沉默…
后端·mysql·面试
breeze_whisper4 小时前
当前端收到一个比梦想还大的数字:BigInt处理指南
前端·面试
小高0074 小时前
性能优化零成本:只加3行代码,FCP从1.8s砍到1.2s
前端·javascript·面试
今禾4 小时前
深入浅出:ES6 Modules 与 CommonJS 的爱恨情仇
前端·javascript·面试
前端小白19954 小时前
面试取经:Vue篇-Vue2响应式原理
前端·vue.js·面试
用户47949283569155 小时前
每天都在用大模型,但是你知道temperature、top_p、top_k这些常见参数是做什么的吗?
人工智能·面试·llm
然我5 小时前
面试官:这道 Promise 输出题你都错?别再踩 pending 和状态凝固的坑了!(附超全解析)
前端·javascript·面试
TT哇5 小时前
【多线程案例】:单例模式
java·单例模式·面试
在未来等你5 小时前
Elasticsearch面试精讲 Day 14:数据写入与刷新机制
大数据·分布式·elasticsearch·搜索引擎·面试