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"
地址(即 str1
与 str3
指针相同)。
对于后面的
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:用户生成的重要数据,需要进行备份,存储用户主动生成的,需要长期保留的文件(如文档、编辑的内容、用户保存的文件等)
2. 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))