Swift 相关知识点学习
一、Main Actor 隔离
1 | .task { |
遇到的这个编译错误:
1 | Main actor-isolated class property 'shared' can not be mutated from a Sendable closure |
是因为在 .task 这个 modifier 的异步闭包中访问了 UIApplication.shared.isIdleTimerDisabled = true,而这个属性 受 Main Actor 隔离(Main actor-isolated),不能在一个并发上下文中(比如 TaskGroup 内部)直接修改。
⚠️ 原因分析:
UIApplication.shared是MainActor隔离的,也就是说只能在主线程上访问和修改它的属性。.task是并发上下文,Swift认为这个闭包是@Sendable,意味着它可能在任意线程运行。- 在
TaskGroup的闭包中调用UIApplication.shared.isIdleTimerDisabled = true,就是从非主线程修改主线程隔离属性,因此编译器报错。
✅ 解决方案:
方法一:在主线程中设置 isIdleTimerDisabled
你可以把它包裹到 MainActor.run 中运行:
1 | group.addTask { |
方法二:把 UIApplication.shared.isIdleTimerDisabled = true 放到 .onAppear 或 DispatchQueue.main.async 中
其实完全可以避免放在 task 的 group 中,像这样写也可以:
1 | .onAppear { |
或者直接:
1 | .onAppear { |
✅ 总结建议
由于设置 isIdleTimerDisabled 本身并不耗时,也不需要并发处理,直接放在 .onAppear 中或者使用 MainActor.run 包裹,确保线程安全即可。
一、先解释:@MainActor 是什么?
👉 它的作用:
@MainActor 是 Swift 的一种 actor 隔离机制,用于确保标记的变量或方法 只能在主线程上被访问或修改,适用于:
UI相关的属性(比如SwiftUI / UIKit的UI状态)- 线程安全性要求高的变量(尤其和主线程交互)
1 |
|
在原来的代码里,不用加 @MainActor 也可以,只要保证在主线程上更新 UI 状态或平台变量即可。
同步耗时操作阻塞主线程
1 |
|
这个 MyCommand.shared.searchPlatform() 是执行命令,返回平台是platform10xx还是 platform20xx。可能比较耗时,使用异步线程调用就解决了这个问题。它为什么会影响 view 的启动。
🧠 一、为什么 MyCommand.shared.searchPlatform() 会影响 view 启动?
✅ 原因归结为一点:它是一个“同步耗时操作”,放在 初始化阶段(init)阻塞主线程,间接破坏了 AVKit 或 UI 的初始化时机。
🔍 具体影响路径:
init()是在主线程上调用的,如果其中执行了耗时操作(如searchPlatform()是同步执行shell命令或I/O的话)——- 会 阻塞主线程,导致整个
app UI构建、系统服务(如AVAudioSession、AVPlayer、AVPiPController)初始化延迟甚至错过系统期望的生命周期阶段。 - 特别是 PiP view初始化涉及的系统服务需要在合适的生命周期点启动,且依赖主线程的“响应性”,此时如果主线程卡住,就会出现系统组件初始化失败,或者系统认为“你不支持
PiP”。
✅ 将 searchPlatform() 放到异步线程后,系统组件终于能按时正常初始化,自然也就不会影响 PiP 启动了。
1 | init() { |
✅ 1. init() 方法一般用来做什么?
💡 init() 的本质职责:
构造出有效可用的对象初始状态,不建议在其中执行副作用重的逻辑,尤其是:
- 耗时操作,异步任务,系统权限请求,网络请求 / 文件访问,主线程
UI修改
❗为什么?
init()是同步执行的,若在其内执行耗时逻辑会阻塞主线程,尤其在App启动阶段;- 如果
init()失败或者行为不可预测,会导致对象构造异常,使程序逻辑失控; - 异步操作在
init()内很难正确捕获、管理生命周期(比如weak self、任务取消等); - 构造函数不支持
async,所以只能硬塞Task或DispatchQueue,这会让副作用不可控。
✅ 合理的 init() 用法 ✅
| 合法的行为 | 说明 |
|---|---|
| 设置默认值 | 初始化本地属性,比如 self.platform = .unknown |
| 从内存中恢复状态 | 例如 UserDefaults / 本地缓存 |
| 创建依赖对象 | 比如 Logger()、URLSession() |
| 做轻量判断逻辑 | 不会阻塞线程 |
❌ 不推荐的 init() 行为 ❌
| 不推荐的行为 | 原因 |
|---|---|
执行 shell 命令 / I/O 操作 |
易阻塞主线程,且不易捕获异常 |
配置系统服务(如 AVAudioSession) |
初始化时系统组件可能尚未准备好 |
创建 AVPlayer / PiP Controller 等系统资源 |
这些往往依赖系统生命周期和 UI 状态 |
| 启动异步任务并期望依赖其结果 | 初始化结束后没法等结果完成,不可预测 |
🧠 2. 那耗时或异步操作放在哪更合适?
- 放在
Task中,异步执行 - 放在
viewDidAppear、.taskmodifier、.onAppear中 - 放在
init()后的 setup()/load()/start() 方法中,延后主动调用
二、private(set) 和 didSet
这是 Swift 中一种非常简洁且实用的语法组合
完整语法结构分析
1 | private(set) var currentSession: MySession? { |
1. private(set):访问权限控制
- 含义:
currentSession对外部可读,但只有当前作用域(比如类)内部可以修改。 - 目的:保护这个值只能被类内部逻辑控制修改,外部只能观察(只读)。
2. var currentSession: MySession?
- 属性定义:这是一个可选类型(
MySession?),表示可能有值,也可能是nil。
3. didSet 是属性观察器
- 作用:一旦
currentSession被赋新值,didSet块就会自动执行。 - 用于做副作用的处理,比如:记录日志、更新相关状态、触发其他逻辑。
4. if let currentSession 是 简化版的 Optional Binding
1 | if let currentSession { |
等价于:
1 | if let currentSession = currentSession { |
Swift 5.7+ 中支持这种 简写绑定形式。
5. didSet 内的逻辑说明
1 | self.sessionStart = Date() |
- 设置会话开始时间。
1 | self.logPath = currentSession.folderPath + "/myLog.txt" |
- 设置日志路径,基于当前的测试会话文件夹路径。
📌 小结:这是一个“状态属性 + 观察器 + 封装写权限”的典型用法
它实现了:
- 状态更新控制
- 自动执行副作用(如时间记录、日志输出)
- 属性值变化的封装保护(
private(set)) - 函数式逻辑与日志的结合
📚 可借鉴的场景
- 登录用户信息:更新
currentUser时触发UI更新 - 网络会话对象:更新
session时触发日志与状态同步 - 设备连接状态:更新连接状态时重设内部依赖项
三、串行队列 DispatchQueue
1 | let queue = DispatchQueue(label: "com.test.queue", qos: .userInitiated) |
这个代码中的 DispatchQueue 是一个串行队列(serial queue)。在这行代码中,DispatchQueue(label: "com.test.queue", qos: .userInitiated) 创建了一个串行队列,但它的使用是否异步取决于如何在队列中调度任务。
解释:
串行队列(Serial Queue):
- 串行队列按顺序执行任务,一个任务在前一个任务完成后才会执行下一个任务。即使将多个任务放入队列,它们也会逐一执行,而不会并行。
- 通过传递
label来创建一个串行队列。这里的com.test.queue只是队列的名称,qos: .userInitiated是队列的质量服务(Quality of Service)级别,表示任务是用户发起的,应该优先执行。
为什么不是异步的:
- 当创建了一个队列,但队列的调度模式是决定异步还是同步的。在这行代码中,队列本身没有指定任务的调度方式。
- 如果在队列中调度任务时使用的是
async,那么任务就是异步执行的;如果使用的是sync,则是同步执行。
例如:
1 | let queue = DispatchQueue(label: "com.test.queue", qos: .userInitiated) |
async会让任务在队列中异步执行,意味着它会立即返回,不会等待任务完成。sync会让任务同步执行,意味着它会阻塞当前线程,直到任务完成。
总结:
DispatchQueue(label: "com.test.queue")本身只是创建了一个队列,决定是否异步或同步是在调度任务时选择async或sync来决定的。
四、stride(from:through:by:) 函数学习
stride(from:through:by:) 是 Swift 中用来生成一个数值序列的函数。它通过指定起始值、结束值和步长来创建一个有规律的数值序列。你可以使用它来创建一个范围内的数值集合,步长可以是正数也可以是负数。
函数签名:
1 | func stride(from start: T, through end: T, by step: T) -> StrideTo<T> where T : Strideable |
参数说明:
from start: T:起始值。through end: T:结束值,序列会包含该值。by step: T:步长值,用于确定数值递增或递减的幅度。
返回值:
- 返回一个
StrideTo<T>类型的序列,这个序列是一个步进的数值集合。
示例代码:
正向步进(递增):
生成从 0 到 10(包括 10)之间的数,步长为 2。1
2
3
4let numbers = stride(from: 0, through: 10, by: 2)
for number in numbers {
print(number)
}输出:
1
2
3
4
5
60
2
4
6
8
10负向步进(递减):
生成从 10 到 0(包括 0)之间的数,步长为 -2。1
2
3
4let numbers = stride(from: 10, through: 0, by: -2)
for number in numbers {
print(number)
}输出:
1
2
3
4
5
610
8
6
4
2
0通过
...创建闭区间:stride(from:through:by:)是一个闭区间(through),意味着它会包含结束值。
使用场景:
stride 适用于需要创建有规律的数值序列时,例如:
- 生成均匀分布的数值(比如绘图时的坐标点)。
- 处理循环或者需要指定步长的逻辑。
总结:
stride(from:through:by:) 函数是 Swift 提供的一个非常灵活的工具,可以用于生成带有特定步长的数值序列。它非常适合需要精确控制范围和步长的场景。
五、split(separator:maxSplits:omittingEmptySubsequences:)
这个方法用来把一个字符串分割成多个子字符串([Substring]),根据指定的“分隔符”来进行切割。
🧾 函数签名:
1 | func split( |
📌 参数说明:
separator: 分隔符字符。字符串会按照这个字符进行拆分。maxSplits: 最多分割多少次。默认是.max,表示不限次数。omittingEmptySubsequences: 是否忽略空的子串(如连续两个分隔符之间没有内容),默认是true。
🔍 这个例子的解析:
1 | let components = content.split(separator: "?", maxSplits: 1) |
等价于:
1 | let components = content.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: true) |
- 把
content这个字符串按?分隔; - 最多分一次(只分一次,也就是得到两部分);
- 忽略空字符串(如果
?在最开始或中间连续出现,不会保留空部分)。
🧪 示例:
例 1:正常分割
1 | let content = "abc?def?ghi" |
只分一次。第一次遇到
?就切一刀,剩下的部分原样保留。
例 2:分割多次(默认值)
1 | let content = "a?b?c?d" |
默认是
maxSplits: .max,会尽可能多地切。
例 3:保留空子串
1 | let content = "a??b" |
🧠 总结记忆法:
split(separator:)→ 用某个字符拆字符串。maxSplits:→ 控制“最多切几刀”。omittingEmptySubsequences:→ 是否丢掉空的部分。
六、Swift 中使用 static
在 Swift 中频繁使用 static 有什么问题?
static 方法是类型方法,它们属于类型(class/struct/enum),而不属于某个实例对象。频繁使用 static 方法有一些 潜在问题:
❌ 可能的问题
无法访问实例属性
static方法无法访问self或实例变量,这意味着所有需要实例数据的逻辑都不能放在static方法中。- 例如:
1
2
3
4
5
6
7class Example {
var name = "Swift"
static func printName() {
print(name) // ❌ 编译错误,不能访问实例属性
}
} - 解决方案:如果方法需要访问实例数据,就不应该使用
static,应该用实例方法。
不适用于需要继承的情况
static方法不能被子类重写,而class func可以。- 例如:
1
2
3
4
5
6
7
8
9
10
11class Parent {
static func greet() {
print("Hello from Parent")
}
}
class Child: Parent {
override static func greet() { // ❌ 报错,static 方法不能被 override
print("Hello from Child")
}
} - 解决方案:
- 如果方法需要允许子类重写,请使用
class func代替static func。
- 如果方法需要允许子类重写,请使用
容易导致全局状态污染
过多
static方法会让类逐渐变成工具类(Utility Class),但大部分时候,面向对象设计更鼓励使用实例方法。例如:
1
2
3
4
5class Utility {
static func format(date: Date) -> String { ... }
static func log(message: String) { ... }
static func validateEmail(_ email: String) -> Bool { ... }
}问题:
- 这些方法全是
static,导致Utility变成了一个全局工具类。 - 缺乏面向对象的封装性,无法在不同实例间存储状态。
- 难以扩展:如果需要不同的
log级别、不同的date format,就必须增加参数或新增方法,代码维护困难。
- 这些方法全是
解决方案:
- 如果方法属于特定的对象实例,尽量用实例方法。
- 如果方法需要继承,使用
class func。
什么时候适合使用 static?
虽然 static 可能带来上述问题,但在以下场景中使用 static 是合适的:
✅ 1. 工具方法(Utility Methods)
- 不依赖实例,不需要存储状态 的方法,适合用
static:1
2
3
4
5
6
7struct MathUtil {
static func square(_ value: Double) -> Double {
return value * value
}
}
let result = MathUtil.square(4) // ✅ 4 * 4 = 16 square(_:)方法不需要MathUtil的实例,因此static是合理的选择。
✅ 2. 单例模式
- 全局唯一的实例,适合用
static:1
2
3
4
5
6
7
8
9
10
11class Logger {
static let shared = Logger()
private init() { } // 禁止外部创建实例
func log(_ message: String) {
print("[LOG]: \(message)")
}
}
Logger.shared.log("App started") // ✅ 访问单例对象 static let shared确保Logger只有一个实例(单例模式)。- 这里
log(_:)是 实例方法,因为它可能需要维护日志级别等状态。
✅ 3. 枚举中的静态常量
static适用于 枚举中定义常量:1
2
3
4
5
6enum API {
static let baseURL = "https://api.example.com"
static let timeout = 30.0
}
let url = API.baseURL // ✅- 避免用
case表示常量,因为case主要用于枚举值。
✅ 4. 类型级计算属性
static可以用于 全局不可变的数据:1
2
3
4
5struct Configuration {
static var defaultTimeout: TimeInterval = 60.0
}
print(Configuration.defaultTimeout) // ✅ 60.0- 注意:如果可能改变,考虑
class var,这样子类可以重写:1
2
3
4
5
6
7class BaseConfig {
class var defaultTimeout: TimeInterval { return 60.0 }
}
class CustomConfig: BaseConfig {
override class var defaultTimeout: TimeInterval { return 120.0 }
}
✅ 5. static 作为工厂方法
- 如果创建对象需要特殊逻辑,可以用
static func:1
2
3
4
5
6
7
8
9struct User {
let name: String
static func createGuestUser() -> User {
return User(name: "Guest")
}
}
let guest = User.createGuestUser() // ✅ - 相比
init()方法,工厂方法可以定制创建逻辑。
什么时候不适合用 static?
| 场景 | static 适合? |
推荐做法 |
|---|---|---|
| 需要访问实例属性 | ❌ 不适合 | 用实例方法 |
| 可能需要被子类重写 | ❌ 不适合 | 用 class func |
| 可能存储状态 | ❌ 不适合 | 用实例方法 |
| 需要创建对象(工厂方法) | ✅ 适合 | 可用 static func |
| 纯工具方法 | ✅ 适合 | 可用 static func |
代码如何优化?
在 ReportConfigModel 里,频繁使用 static,但它们大部分是不依赖实例的数据解析逻辑,所以**static 是合适的**。
✅ 但如果以后 booleanValue 需要根据实例的设定解析数据,建议改成实例方法:
1 | func booleanValue(forKey key: String, in dictionary: NSDictionary) -> Bool { |
这样更灵活,避免滥用 static。
总结
适合
static的场景:- 工具方法(不需要实例状态)。
- 全局常量(如 API 地址)。
- 单例模式(如
Logger.shared)。 - 类型级计算属性(如
Configuration.defaultTimeout)。 - 工厂方法(如
User.createGuestUser())。
不适合
static的场景:- 方法依赖实例数据,应使用实例方法。
- 方法可能需要被子类重写,应使用
class func。
✅ 结论:
在 ReportConfigModel 里,booleanValue 目前是工具方法,用 static 没问题。
但如果未来它需要访问 self,就应该改成实例方法。合理使用 static,避免让代码变成全是工具函数的“过程式编程”。
七、数字系统及其转换
1 基本概念
二进制(Binary)
- 定义:使用
0和1两个数字表示信息,是计算机内部的基本数制(基数为2)。 - 应用:所有计算机数据最终都以二进制形式存储和运算。
- 定义:使用
十进制(Decimal)
- 定义:我们日常生活中使用的数字系统,基数为
10,由0~9十个数字组成。 - 应用:人类最直观的数制,用于计算和计量。
- 定义:我们日常生活中使用的数字系统,基数为
十六进制(Hexadecimal)
- 定义:一种基数为
16的数制,使用0~9和A~F(代表10~15)来表示。 - 应用:在计算机系统中常用来表示内存地址、颜色值、以及底层硬件接口数据,因为它可以更紧凑地表示二进制数据(每
1个十六进制数字等于4个二进制位)。
- 定义:一种基数为
2 数字转换方法
二进制 ⇔ 十进制
- 二进制转十进制:对二进制数的每一位,乘以相应的
2的幂次方再求和。
例如,二进制1011 = 1×2³ + 0×2² + 1×2¹ + 1×2⁰ = 8 + 0 + 2 + 1 = 11。 - 十进制转二进制:将十进制数不断除以
2,记录余数,再将余数倒序排列。
例如,11 ÷ 2 = 5 余 1,5 ÷ 2 = 2 余 1,2 ÷ 2 = 1 余 0,1 ÷ 2 = 0 余 1,倒序得到 1011。
- 二进制转十进制:对二进制数的每一位,乘以相应的
十进制 ⇔ 十六进制
- 十进制转十六进制:将十进制数不断除以
16,记录余数(10~15分别用A~F表示),余数倒序排列。
例如,255 ÷ 16 = 15 余 15(F),15 ÷ 16 = 0 余 15(F),得到 FF。 - 十六进制转十进制:对每一位乘以
16的幂次方再求和。
例如,FF = 15×16¹ + 15×16⁰ = 240 + 15 = 255。
- 十进制转十六进制:将十进制数不断除以
二进制 ⇔ 十六进制
- 每
4个二进制位对应1个十六进制位。例如,二进制1011 1100 分成 1011(B)和 1100(C),即十六进制 BC。
- 每
3 应用场景
- 硬件接口:很多低层设备接口返回的是十六进制数据,使用十六进制便于阅读和调试。
- 调试输出:在调试工具中,经常需要将二进制数据以十六进制格式显示,便于快速定位问题。
- 数据存储和传输:序列化数据时,将内部数据转换为文本格式(如
JSON)时,使用Codable可以自动处理枚举的raw value。
八、Xcode 调试工具:LLDB、Console 和 OSLog
1 LLDB
- 概念:
LLDB是Xcode使用的调试器,用于设置断点、检查变量、跟踪函数调用等调试操作。 - 用途:
- 设置断点:在代码中暂停执行,检查内存、变量状态等。
- 命令行调试:通过
LLDB命令(如po、bt)查看对象、打印堆栈信息等。
- 使用方法:
- 在
Xcode中点击代码行号设置断点; - 在调试控制台中输入
LLDB命令,例如:po someVariable(打印变量描述)breakpoint set --name functionName(设置断点)thread backtrace(查看调用堆栈)
- 在
2 Console
- 概念:
macOS的Console应用用于查看系统日志和应用日志。 - 用途:
- 查看
Xcode、系统和应用产生的日志信息。 - 过滤日志、搜索特定关键字,帮助调试问题。
- 查看
- 使用方法:
- 打开
macOS的Console应用; - 在搜索栏中输入相关关键字(例如项目的子系统名称、进程名称等);
- 观察日志中是否有异常信息或错误提示。
- 打开
3 OSLog
- 概念:
OSLog是苹果提供的新型日志系统,支持结构化日志记录。 - 用途:
- 用于在代码中记录调试、信息、错误日志;
- 支持高性能日志写入,并可通过
Console或OSLogStore API进行查询。
- 使用方法:
- 在代码中通过
Logger类记录日志,例如:1
2
3let logger = Logger(subsystem: "com.example.myapp", category: "network")
logger.info("Network request started")
logger.error("Network error: \(error.localizedDescription)") - 在
Console应用中查看日志,或者使用OSLogStore API导出日志。
- 在代码中通过