var counter = 0 // 在栈上分配
let increment = {
counter += 1 // 通捕获的引用修改原内存的位置
}
increment()
print(counter) // 输出: 1
原理机制:
Swift闭包默认通过引用捕获 来捕获var变量
即使对于int string 等值类型 闭包也是捕获变量的引用,而非值的副本
这允许闭包内部修改原始变量的值
闭包捕获了外部变量name, 当在表达式中需要访问name的时候,编译器会识别name为外部变量,此时编译器就会生成一个内存空间,存储外部变量name的引用
name是通过引用捕获进入闭包的
单一职责
接口隔离
适配器
依赖倒置
迪米特法则
里氏替换
Publisher
发布者
s.subspec
Networklayer
Config
Core
Plugin
cache
Error
Api
Mutaing 底层机制
高阶函数的底层实现原理
map函数用于将一个序列中的每个元素通过一个提供的转换函数转换成新的形式,然后返回一个新的数组。
map函数会遍历序列中的每个元素,并对每个元素应用闭包中定义的转换 ,然后将结果收集到一个新数组 中
filter函数会遍历序列,对每个元素应用闭包(该闭包返回一个布尔值),如果闭包返回true,则将该元素添加到新数组中。
Reduce函数接受一个初始值和一个组合闭包,她从初始值开始,依次将序列中的每个元素与当前累积值通过组合闭包进行组合
touchbegin 和手势
Cancellable
调用sink 或assgin时,返回一个anycancellable对象
你必须强引用这个对象,当anycancellable对象被释放,比如离开作用域,自动取消订阅并释放资源、
如果不保留他 订阅立即被取消 可能什么也收不到
通常的做法是将所有AnyCancellable 收集到一个set中
Swift Property Wrapper 是 Swift 5.1 引入的一个特性,它允许我们封装属性访问的通用模式,从而减少重复代码。下面我将通过几个例子来展示 Property Wrapper 的用法。
@Published 的核心原理是:
-
属性包装器拦截值的读写操作
-
设置新值时自动通知所有订阅者
-
投影值 ($property) 提供发布者接口
-
与响应式框架深度集成,支持 SwiftUI 的自动更新
在Swift中,协议可以通过继承AnyObject来限定只有类(class)可以遵循这个协议,而结构体(struct)和枚举(enum)不能遵循。
struct Weak {
private weak var _value: AnyObject?
var value: T? {
return _value as? T
}
init(_value: T) {
self._value = value as AnyObject
}
}
Sendable 标记可以安全的传递的数据类型
Actor 封装并发访问的对象,保证数据安全
AnyActor 用于类型擦除,使得不同类型的actor可以通用处理
GlobalActor MainActor 用于指定代码执行的全局并发上下文 确保代码在特定的线程上执行
Task async await 用于创建和管理异步任务
asyn let 用于并发的启动多个子任务 并延迟等待结果
每个async let 都会创建一个子任务,子任务立即开始执行 不会阻塞当前任务
使用await获取结果 按顺序等待从左到右
⚠️ 1. async let 不支持逃逸闭包
不await也会等待完成
不支持真正的fire-and-forget
TaskGroup: 动态并发任务创建机制 适合任务数量在运行时决定的场景
Async let 固定并发任务的语法糖,声明周期绑定作用域 错误传播顺序依赖await顺序
taskgroup 是动态并发任务的管理器 声明周期绑定闭包,错误传播顺序更及时 可控
Swift 方法支持重载
struct extension final 是静态调度
@objc修饰的函数是函数表调度,如果方法需要在oc中使用。类需要继承nsobject
dynamic 修饰的函数调度方式是函数表调度 是动态可以修改的 可以进行method-swizzling
@objc+dynamic 是通过objc_msgSend 来调用的
Class是引用类型,是通过函数表调度
Swift runtime
纯Swift类没有动态特性 方法和属性不加任何修饰符,已经不具备runtime特性 此时的方法调度依旧是函数表调度
纯Swift类 方法和属性添加@objc 标识 可以通过runtime api 获取到,但是在oc中无法进行调度 原因是Swift.h 中没有Swift类的声明
对于继承自NSObject类来说,如果想要动态的获取当前属性+方法,必须在其声明前添加@objc关键字,如果想要使用方法交换,还必须在属性+方法前添加dynamic关键字,否则当前属性+方法只是暴露给OC使用,而不具备任何动态特性
-
Any:任意类型,包括function类型、optional类型
-
AnyObject:任意类的instance、类的类型、仅类遵守的协议,可以看作是Any的子类
-
AnyClass:任意实例类型,类型是AnyObject.Type
-
T.self:如果T是实例对象,则表示它本身,如果是类,则表示metadata.T.self的类型是T.Type
@unchecked Sendable 在 Swift 中是一个强大的工具,它能让你在确保类型线程安全的前提下,绕过编译器的自动 Sendable 一致性检查。这通常用于那些编译器无法自动验证,但你通过其他方式(如锁)确保了线程安全的类型。
@unchecked Sendable 的核心用途
简单来说,@unchecked Sendable 主要用于以下两种情况:
-
标记已知线程安全的复杂类:当一个类(尤其是遗留代码)内部已经使用了锁(如 DispatchQueue)或其他同步机制来保护其可变状态,但编译器无法理解这些机制时,你可以使用 @unchecked Sendable 来告诉编译器:"相信我,这个类是线程安全的"。
-
解决编译器的限制:例如,非 final 的类无法直接符合 Sendable,因为其子类可能破坏线程安全。如果你能确保该类的所有使用都是线程安全的,可以使用 @unchecked Sendable 来突破这一限制。
记住这个核心原则:仅在你能通过代码逻辑和同步机制 100% 保证线程安全,且编译器无法自动验证时,才使用 @unchecked Sendable。对于新代码,应优先考虑使用 actor 等更安全的现代并发抽象。
-
@MainActor 等价于“UI 专属线程”。
-
actor 自带互斥锁,适合可变共享状态(如缓存)。
-
nonisolated 让某个方法脱离当前 Actor,继承调用者线程。
/// Wrapper for Kingfisher compatible types. This type provides an extension point for
/// convenience methods in Kingfisher.
public struct KingfisherWrapper {
public let base: Base
public init(_ base: Base) {
self.base = base
}
}
public protocol KingfisherComptible {
Associatedtype Base
Var kf: KingfisherWrapper {get}
}
public class Animator: @unchecked Sendable { //@unchecked Sendable 绕过编译器的自动sendable 一致性检查,通常用于编译器无法自动验证,但你通过其他方式确保线程安全
let arrayOfOptionalInts: [Int?] = [nil, 2, 3, nil, 5]
// Match only non-nil values.
for case let number? in arrayOfOptionalInts {
print("Found a (number)")
}
In- out参数的传递方式:
1 当函数调用时,参数的值会被修改
2 在函数体内,副本被修改
3 当函数返回是,副本的值被赋给原始参数
与类型相关的方法,而不是与类型实例相关的方法,必须使用 static声明修饰符来标记(枚举和结构体使用 static,类可以使用 static或 class声明修饰符)。
用 class声明修饰符标记的类类型方法可以被子类的实现重写;
用 class final或 static标记的类类型方法则不能被重写。
Actor:
actor 类型可以采用任意数量的协议,但不能从类、枚举、结构体或其他 actor 继承。然而,标记为 @objc特性的 actor 隐式地遵循 NSObjectProtocol协议,并作为 NSObject的子类型暴露给 Objective-C 运行时。
在 CocoaPods 中,subspec 允许你将一个庞大的 Pod 库拆分成多个逻辑上独立的功能模块。这样,其他项目在引入你的库时,可以只依赖其需要的特定部分,而不是整个庞大的代码库,从而保持项目的轻量。
NSOperation相比GCD的优点? 为什么需要NSOperation这个封装
1 解决复杂的依赖关系
gcd实现需要dispatchgroup 、信号量或嵌套回调
NSOperation只需要一句operation.addDependency
2 可取消性: 用户离开页面或需要中断时,取消操作
3 状态管理和观察
4 更精确的队列控制 maxConcurrentOperationCount 可以实现一个并发数受限但非串行的队列
-
dispatch_semaphore_create():创建信号量
-
dispatch_semaphore_wait(): 信号量减1,当信号量<0才会阻塞当前线程,
-
dispatch_semaphore_signal(): 释放信号量,信号+1. 当信号》=0时会执行wait之后的代码
-
⑤ dispatch_once执行一次的原因: 单例的流程只执行一次,底层是如何控制的,即为什么只能执行一次?
1 宏观使用:令牌机制
onceToken 是关键,是静态变量具有唯一性,函数通过检测和修改这个令牌的状态,来决定代码块是否需要执行
本地组件化
cocoapods组件化:
1 执行repo命令添加私有repo, 将私有仓库添加至本地~/.cocoapods/repos 目录
pod repo add DemoSpec xxxxx
2 pod lib create 创建工程
3 配置pod工程
4 提交到git
Git init
Git status
Git add .
Git status
Git commit -m ‘xxx’
Git tag -a ‘0.01’ -m ‘xxxx’
Git tag
Git remote add origin xx
Git push -f origin master
5 验证podspec文件
pod spec lint —allow-warings
6 提交到私有仓库 pod repo push 【本地spec repo名称】 [路径] —allow-warings
组件化通讯
1 协议
2 中间者 target-action : 基于oc的runtime category 特性动态获取模块CTMediator
3 URL路由 MJRouter
事件响应:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !isUserInteractionEnabled || isHidden || alpha <= 0.01 {
return nil
}
if self.point(inside: point, with: event) { //判断触摸点是否在自身内部
for subview in subviews.reversed() {// 按FILO 遍历子视图
let convertrePoint = subview.convert(point, from: self)
let resultView = subview.hitTest(convertrePoint, with: event)
// 判断触摸点是否在子视图内部,在就返回视图,不在就返回nil
if resultView != nil {return resultView}
}
return self// 此处指所有的子视图都不符合要求,而触摸点又在该视图自身内部
}
return nil //触摸点不在改视图内
}
设计模式:
单一职责:一个类只允许一个职责
里氏替换: 子类可以替换父类
接口隔离:不建立庞大臃肿的接口,细化接口,接口中的方法尽量少
依赖倒置: 依赖抽象。不是依赖实现
开闭原则:对扩展开放,对修改关闭
迪米特:一个对象尽可能少的对象接触
- Swift Property Wrapper
- Combine的生命周期怎么管理的
- combine的PassthroughSubject 与CurrentValueSubject区别
- async let 和taskgroup的区别
- 有多个async let时, 只的返回顺序,如果想顺序返回,怎么做
- actor是怎么使用的?使用场景
- map ffilter compactmap reduce 的底层原理
- struc和class的区别
- combine和rxswift的区别
- @publised的原理
- nonisolated
- cancellable
- Task运行在什么线程
Task 运行在由 Swift 运行时管理的协作式线程池中的线程上。它的核心优势在于,作为开发者,你无需关心具体的线程分配,只需通过 Actor (尤其是 MainActor )来声明代码所需的执行上下文(如主线程) ,系统便会自动、高效地为你安排。
- cell上的 按钮绑定AnyPublisher 需要考虑复用的问题吗
- 用 Future 来封装异步请求以创建一次性的发布者 combine的核心
- 播放视频时黑屏、卡顿怎么优化
- 利用 RunLoop 监测卡顿:通过添加 Observer 到主线程 RunLoop,监控其 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting为什么要监控这2个状态
- 为什么 @MainActor 并不总是确保主线程执行?对于非隔离上下文中的同步方法,并不能保证
task运行的线程是由系统调度器管理的,并不一定是子线程
// 在主线程中启动一个 Task
Task {
// 这里可能还在主线程,除非遇到挂起点
print("Is main thread: (Thread.isMainThread)") // 可能为 true
// 执行一个异步操作(比如网络请求)
let data = await downloadData()
// 挂起后恢复,这里可能在其他线程
print("Is main thread after await: (Thread.isMainThread)") // 可能为 false
// 如果需要更新UI,必须切回主线程
await MainActor.run {
// 这里保证在主线程
updateUI(with: data)
}
}
// 使用 detached Task
Task.detached {
// 这里很可能在后台线程
print("Is main thread in detached: (Thread.isMainThread)") // 通常为 false
let data = await downloadData()
// 注意:在 detached task 中,如果你需要更新UI,必须切回主线程
await MainActor.run {
updateUI(with: data)
}
}
-
默认情况下,Task 由 Swift 并发运行时系统调度
-
可能运行在主线程,也可能运行在后台线程
-
系统自动管理线程池,开发者不需要手动管理
一个 Sendable 类型是指可以在并发环境中安全传递的类型,包括结构体、常量属性的 final 类、自动保护其可变状态的 Actors 等。
- actor 是一种引用类型,类似于类,但不同之处在于 Actor 一次只允许一个任务访问其可变状态。
- 内部的属性和方法默认是隔离的,外部代码不能直接访问,除非通过await 调用
- 使用场景: 当多个任务需要访问和修改共享数据时,使用actor可以保证数据安全: 例如多个任务同时操作一个计数器,使用actor可以确保每次修改都是原子的
actor与@MainActor:
@MainActor是一个特殊的actor运行在主线程上,用于更新UI,任何标记为@Mainactor 的代码都必须运行在主线程上
combine
cancellable。每一个订阅都会生成一个 AnyCancellable 对象,用于控制订阅的生命周期。通过这个对象,我们可以取消订阅。当这个对象被释放时,订阅也会被取消。
/// 取消订阅
cancellable.cancel()
Combine提供了2个常用的subject: PassthroughSubject 与CurrentValueSubject
PassthroughSubject 透传事件,不会持有最新的Output
CurrentValueSubject: 除了传递事件之外,会持有最新的Output
@published : combine提供了一个Property Wrapper @Pubilshed 可以快速封装一个变量得到一个publisher
特殊的操作符 erasedToAnyPublisher,让我们可以擦除掉具体类型:
通知的声明周期绑定
1 // 存储为属性,与实例生命周期绑定
private var notificationCancellable: AnyCancellable?
private func setupNotification() {
notificationCancellable = NotificationCenter.default
.publisher(for: .yourNotification, object: nil)
.sink { [weak self] notification in
print("Received notification: (notification)")
self?.handleNotification(notification)
}
}
// 可选:手动取消订阅
func cancelNotification() {
notificationCancellable?.cancel()
notificationCancellable = nil
}
方案2:
使用 Set(最佳实践)
// 使用集合管理多个订阅
private var cancellables = Set()
NotificationCenter.default
.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { [weak self] _ in
self?.appDidBecomeActive()
}
.store(in: &cancellables) // 存储到集合中
可以手动取消所有订阅:
// 手动取消所有订阅
func cancelAllSubscriptions() {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
}
Combine 发布者和订阅者涉及到的 Swift 类型
当你在 Swift 中构建管道时,函数链导致该类型被聚合为嵌套的通用类型。 如果你正在创建一个管道,然后想要将该管道作为 API 提供给代码的另一部分,则对于开发人员来说,暴露的属性或函数的类型定义可能异常复杂且毫无用处。
为了说明暴露的类型复杂性,如果你从 PassthroughSubject 创建了一个发布者,例如:
let x = PassthroughSubject<String, Never>()
.flatMap { name in
return Future<String, Error> { promise in
promise(.success(""))
}.catch { _ in
Just("No user found")
}.map { result in
return "(result) foo"
}
}
结果的类型是:
Publishers.FlatMap<Publishers.Map<Publishers.Catch<Future<String, Error>, Just>, String>, PassthroughSubject<String, Never>>
当你想要暴露这个 subject 时,所有这些混合的细节可能会让你感到非常迷惑,使你的代码更难使用。
为了清理该接口,并提供一个好用的 API,可以使用类型擦除类来包装发布者或订阅者。 这样明确隐藏了 Swift 中从链式函数中构建的类型复杂性。
用于为订阅者和发布者暴露简化类型的两个类是:
每个发布者还继承了一种便利的方法 eraseToAnyPublisher(),它返回一个 AnyPublisher 实例。 eraseToAnyPublisher() 的使用非常像操作符,通常作为链式管道中的最后一个元素,以简化返回的类型。
如果你在上述代码的管道末尾添加 .eraseToAnyPublisher():
let x = PassthroughSubject<String, Never>()
.flatMap { name in
return Future<String, Error> { promise in
promise(.success(""))
}.catch { _ in
Just("No user found")
}.map { result in
return "(result) foo"
}
}.eraseToAnyPublisher()
结果的类型将被简化为:
AnyPublisher<String, Never>
同样的技术在闭包内构造较小的管道时将非常有用。 例如,当你想在闭包中给操作符 flatMap 返回一个发布者时,你可以通过明确的声明闭包应返回 AnyPublisher 来获得更简单的类型推断。 可以在模式 有序的异步操作 中找到这样的一个例子。
内置的combine 拓展Publisher 如timer NotificationCenter Array URLSession KVO
一些特殊的Publisher
Future: 只会产生一个事件,要么成功,要么失败,适用于大部分简单回调场景
Just: 值的简单封装
@Published
Future 在创建时立即发起其中异步 API 的调用,而不是 当它收到订阅需求时。 这可能不是你想要或需要的行为。 如果你希望在订阅者请求数据时再发起调用,你可能需要用 Deferred 来包装 Future。
Concurrency
关键转换技术解析
1. withCheckedThrowingContinuation
- 作用:将基于回调的异步操作转换为 async/await 模式
- 工作原理:
-
-
挂起当前任务
-
提供 continuation 对象用于恢复任务
-
在回调中调用 resume 恢复任务执行
-
Task
Async await
TaskGroup
// 示例异步操作
private func performAsyncCommit() async -> Bool {
// 这里可以替换为实际的异步操作
// 例如使用 withCheckedContinuation 封装网络请求
return await withCheckedContinuation { continuation in
// 模拟异步操作
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
// 随机返回成功或失败
let success = Bool.random()
continuation.resume(returning: success)
}
}
}
func checkUserIsExistRealNameCertify() async ->Int? {
return await withCheckedContinuation { continuation in
DZJRequestUtil.GET(url: "real_name_auth/v2/check",
parameters: ["idCard": self.idNumTextField.text ?? "",
"name": self.nameTextField.text ?? ""]).success { data in
let status = data as? Int
continuation.resume(returning: status)
}.failed { error in
continuation.resume(returning: nil)
}
}
}
@objc public dynamic static func requestDoctorInfo() async ->DZJHomeDoctorInfoModel? {
return await withCheckedContinuation { continuation in
let accountCode = UserManager.shared.userInfo?.accountCode ?? ""
let url = "user/doctor_detail/homepage/get?accountCode=(accountCode)"
DZJRequestUtil.GET(url: url).success { data in
let dataDic = data as? [String: Any]
let model = DZJHomeDoctorInfoModel.init(dataDic)
continuation.resume(returning:model)
}.failed{ error in
continuation.resume(returning: nil)
}
}
}
@MainActor
确保代码在主线程执行
其中 @MainActor 作为一个全局 actor,为我们提供了一种优雅的方式来确保代码在主线程上执行。在 iOS 开发中,我们经常需要在主线程上更新 UI,而 @MainActor 的引入大大简化了这一过程。
@dynamicMemberLookup
让你可以自定义一个类型,在使用 object.member 这种写法时,由你自己决定如何处理这个“不存在”的 member。
它本质上是一个 下标函数,根据传入的键名(keyPath 或 String),返回你想要的结果。
Combine
Publisher : 发布者,表示可以发出值的对象
Subscriber: 订阅者,订阅发布者的值并响应处理
operator: 操作符,用于转换或组合数据流
Subject: 特殊的publisher,也能作为数据源主动发送值
Cancellable: 返回值,,用于取消订阅
map: 映射值
filter:过滤值
combinelatest: 合并多个publisher
Flatmap: 链式转换并展开publisher
Debounce : 防抖处理,如输入框
removeDuplicates: 去重处理
catch: 错误捕获并恢复
PassthroughSubject:
用于外部主动发送值
CurrentValueSubject: 有初始值,并能追踪最新状态
3. 泛型的使用场景
(1) 泛型函数
2 泛型类型:定义可以存储任意类型数据的容器
3 泛型协议:associatedtype
4 泛型约束: 限制泛型类型必须满足特地条件 ;?(如遵循协议、继承类或实现特定方法):
5 关联类型
withTaskGroup : 创建的一个任务组 组内可以添加多个子任务,子任务会并发执行
addTask : 像组内添加子任务
for await 循环收集子任务的结果
BUILD_LIBRARY_FOR_DISTRIBUTION对性能的影响主要体现在编译阶段和运行时两个层面,具体表现如下:
1 编译性能影响
当设置为YES时,会生成额外的Swift模块接口文件(.swiftinterface)以保证跨Swift版本的兼容性,这会显著增加编译时间,尤其在大型项目中可能延长30%-50%的构建耗
2 运行时性能差异
开闭原则: 对扩展开发,对修改关闭
单一职责:一个类只允许有一个职责,
依赖倒置:依赖抽象而不是依赖实现
接口分离:不建立庞大雍总的接口 细化接口
迪米特法则:一个对象尽可能少的对象有接触
里氏替换
PassthroughSubject :
紧急修复流程
bash
# 1. 从生产分支创建热修复分支
git checkout main
git pull origin main
git checkout -b hotfix/critical-bug
# 2. 修复问题并提交
git add .
git commit -m "fix: resolve critical security issue"
# 3. 合并到主分支和开发分支
git checkout main
git merge --no-ff hotfix/critical-bug
git push origin main
git checkout develop
git merge --no-ff hotfix/critical-bug
git push origin develop
# 4. 清理分支
git branch -d hotfix/critical-bug
常见分支类型:
Master/main: 用于生产环境,保持稳定
develop 用于集成开发 保持相对稳定
feature 用于开发新功能,从develop 分支拉取,完成后合并到develop
release 用于准备发布,从develop分支拉取 进行测试和修复 完成后合并到master和develop
hotfix 用于紧急修复生产环境问题,从master拉取,修复后合并到master和develop
git branch xx 创建分支
Git checkout -b xx 创建并切换到新分支
切换分支:
git checkout xx
合并分支
git merge xx
删除分支 git branch -d xx
Lazy
// 假设你在设计一个视图控制器
class MyViewController: UIViewController {
// 决策1: 这个视图可能在某些情况下不需要显示吗?
// -> 是,考虑 lazy
lazy var optionalPopupView: UIView = {
let view = UIView()
// 复杂的配置...
return view
}()
// 决策2: 这个视图的初始化是否严重依赖 self.view 的布局?
// -> 是,必须用 lazy
lazy var positionedView: UIView = {
let view = UIView(frame: self.view.bounds.insetBy(dx: 20, dy: 20))
return view
}()
// 决策3: 这个属性是简单的数据,且总是会被用到吗?
// -> 是,不要用 lazy!
var titleString: String = "默认标题" // ✅ 好
// lazy var titleString: String = "默认标题" // ❌ 过度设计,引入无谓开销
// 决策4: 这个数据计算成本高,且不一定用到吗?
// -> 是,考虑 lazy
lazy var expensiveData: [ExpensiveItem] = {
return self.calculateExpensiveData() // 耗时操作
}()
}
Private (set) 修饰懒加载属性 ,达到无法懒加载let的效果
Dump 打印对象会显示对象的属性
Print:不会显示
button.addAction(UIAction, for: .touchUpInside): iOS 14 添加点击事件
For number in numbers where number.isMultiple(of: 2)
Swift 5.7 可以省略=
If let a {
print(a)
}
- is
-
Is 用于检查一个实例是否属于特定的类型。或其子类返回布尔值
-
is 不会改版实例的类型,它仅仅进行类型检查
-
As
用于将实例转换为指定的类型
As?:条件转换,返回一个可选值,如果转换成功,则返回转换后的值,否则转换为nil
As! 强制转换,如果转换失败,会触发运行时错误
as 用于向上转型或编译器能确定的类型转换
CaseIterable 是 Swift 中一个非常有用的协议,它允许你获取枚举所有情况的集合。下面为你梳理它的核心用法、限制以及实际应用场景。
让一个枚举遵循 CaseIterable 协议后,Swift 编译器会自动合成一个名为 allCases 的静态属性。这个属性是一个包含该枚举所有 case 的集合,通常是一个数组 [YourEnum]
switch (boolean1: boolean1, boolean2: boolean2, boolean3: boolean3) {
case (boolean1: true, _, boolean3: false):
functionA()
case (_, boolean2: false, boolean3: true):
functionB()
case (boolean1: true, boolean2: false, boolean3: false):
functionC()
default:
break
}
Taskgroup: 动态产生n个任务 适合批量下载
actor 让可变状态串行访问 自带一把串行锁
class actor
Class谁都可以同步访问 actor必须await进门
class 锁或队列保证线程安全 actor 编译器保证
sendable 是一个协议 用于指示类型的值可以安全地在并发域之间共享。既可以在多个actor或线程中传递而不会引起数据竞争
使用场景: 当你在并发代码中传递数据时,
MVVM:
model
View
Viewmodel
viewModel 就是mvp模式的p,在mvvm中叫做VM
MVVM 相当于MVP做的改进就是对VM/P 和view做了双向的数据和命名绑定,利用binder机制使得model和view可以状态同步
MVP:
model太过于简单
model里面封装网络请求
Presenter 持有model
view绑定presenter
View 层处理用户交互事件,如果涉及到与model交互,应该交由present处理
presenter 只关心业务逻辑的实现,不直接操作view和进行界面跳转
model 负责数据和处理数据的业务逻辑
View: 负责显示数据并将用户交互事件传递给presenter
Presenter :作为view和model的中间人,从model获取数据,处理业务逻辑,并更新view
View和model是解耦的 。之间的通信是通过presenter , view 通常通过协议来定义,presenter只与这个协议交互,
1 定义model
2 定义view协议:view协议定义了presenter如何与view交互,view通常是被动的,等待presenter更新
3 定义presenter : presenter 负责从model获取数据,处理用户交互,并更新view
viewcontroler ->presenter
Presenter ->model 直接调用userApi
Model层独立 不依赖其他层
MVP/ mvc
Subspec
1 模块化解耦
将一个大型的库拆散分成多个独立的小模块,这样,使用者可以根据需要引入自己需要的部分,而不是整个库
2 减少依赖和编译时间
- 当只引入需要的subspec 时,不需要的代码就不会引入项目,从而减少编译时间,也减少了最终生成二进制的文件的大小
- 同时,这也可以避免因为引入不必要的模块而带来的额外依赖,从而减少依赖冲突的可能性
提高复用性
presenter: 业务逻辑层 model和view的中间人
MVC :1 model只能跟controller交互 ,controller 职责过多,2model和view太简单
3 业务逻辑和UI混杂在一起,难以编写单元测试
Actor vs Class:只差一个隔离域
| 维度 | Class | Actor |
| 引用语义 | ✅ | ✅ |
| 继承 | ✅ | ❌ |
| 隔离域 | ❌(谁都能同步访问) | ✅(必须 await进门) |
| 线程安全 | 手动锁/队列 | 编译器保证 |
| 同步调用 | 任意 | 外部禁止 |
Actor 想成远程服务,数据在服务器里,要发请求await 才能读写
同时满足下面3条才值得上actor:
1 有非sendable状态
纯sendable 结构体或类 无需保护
需要共享可变状态? ├─ 否 → 用 struct / class(Sendable) ├─ 是 → 状态 Sendable? │ ├─ 是 → 用 Sendable 值类型或锁自由类 │ └─ 否 → 操作必须原子? │ ├─ 否 → 拆成 Sendable 片段 │ └─ 是 → 能在 MainActor 完成? │ ├─ 是 → @MainActor │ └─ 否 → 上 Actor ✅
AsyncStream 是 Swift Concurrency 中一个非常强大的工具,主要用于将现有的回调式、委托式或事件驱动的 API 转换为现代的 AsyncSequence。以下是 AsyncStream 的主要使用场景:
async-let
async-let 是一种并发的编程模式,它允许你同时启动多个异步操作,并且这些操作会并发执行。使用 async-let 绑定的变量,你可以在需要的时候使用 await 来获取结果。如果在这个过程中有错误发生,错误会在你使用 await 的时候抛出。
Async 用于标记一个函数或方法为异步, 这意味着该函数可以执行异步操作,并且可以使用await关键字来挂起自己,直到异步操作完成
- 使用方式:
-
- async 用于声明一个函数是异步的。
- async-let 用于在异步上下文中并发地执行多个异步操作。
- 并发性:
-
- 使用 await 顺序调用多个异步函数时,这些函数会按顺序执行,即一个完成后才执行下一个。
- 使用 async-let 可以同时启动多个异步函数,它们并发执行,从而提高效率。
- 错误处理:
-
- 对于 async 函数,错误通常通过 throw 抛出,并使用 try await 来捕获。
- 在 async-let 中,每个 async-let 绑定的表达式都可以抛出错误,但错误会在你使用 await 获取结果时抛出。如果有多个 async-let 绑定,你可以分别对每个进行错误处理。
- 适用场景:
-
- 当你需要按顺序执行异步操作时,使用 async 和 await。
- 当你有多个独立的异步操作,并且希望它们同时执行时,使用 async-let。
注意
使用 async-let 时,你实际上创建了多个并发的子任务。这些子任务会同时执行,但你需要确保在它们完成后再使用结果。另外,注意避免产生数据竞争,确保线程安全。
async let返回的结果的顺序取决于你等待它们的顺序,而不是它们完成的顺序,如果按照声明的顺序等待,结果就是声明的顺序,如果你改变等待的顺序,那么结果顺序也会改变
📢: async let 绑定的任务在声明时就开始执行了,而不是在等待的时候
async let 返回的结果不是按照任务完成的顺序,而是按照你等待他们的顺序
如果要保持顺序,
1 按顺序await
2 使用taskgroup 并手动排序
Actor: 是一种引用类型,可保护对其可变状态的访问,并随关键字actor一起引入
Any : 任意值(含函数,元组 struct class)
AnyObject 协议 引用类型class /actor 弱引用容器,协议限制为引用语义
any: existential 关键字,任意协议 声明存在某个协议实现
Any少用,1用协议约束 2 用泛型
some: 编译器已知道具体的类型
any: 运行时才知道具体的类型
-
Any ≈ 远古时代的 id,能装万物,但类型信息全丢。
-
AnyObject ≈ “必须是引用类型”的协议约束,常用于弱引用容器。
-
any ≈ Swift 5.7 的新关键字,解决“带关联类型协议”的泛型多态问题。
withUnsafeContinuation 不处理抛出错误
func fetchData() async -> String {
return await withUnsafeContinuation
}
withUnsafeThrowingContinuation 处理抛出错误的场景
func fetchData() async throws -> String {
return try await withUnsafeThrowingContinuation { continuation in
}
}
使用时
do {
} catch {
}
App启动
main() 函数之后的优化是启动性能优化的重点,这部分完全由开发者控制
1 延迟初始化非核心组件
2异步初始化
3 UI启动优化
优化rootviewcontroller 加载
方案1 : 立即显示轻量级启动页
方案2: 根据业务状态决定初始页面
延迟加载主界面复杂内容
在后台线程准备主界面
分段加载viewcontroller
阶段1: 立即设置基础UI
阶段2: 首屏显示后,加载次要内容
4 数据预加载优化
5 第三方库优化
按需初始化sdk
6 性能检测和测量: 启动时间测量
资源预加载 预热技术
📱 从系统到 main() 函数
这个过程主要由系统自动完成,可以分为 dyld 加载和 Runtime 初始化两大步骤。
- dyld 加载阶段
dyld(Apple 的动态链接器)是这一阶段的“总指挥”。当你点击 App 图标后,内核会为 App 创建进程,随后将控制权交给 dyld,它负责: -
- 加载可执行文件:首先将 App 编译生成的 Mach-O(可执行文件格式)加载到内存。
- 加载依赖库:递归加载所有依赖的动态库(例如系统库 libSystem、Objective-C 的 libobjc 等)。
- 符号修正 (Rebase & Bind) :
-
- Rebase:由于 ASLR(地址空间布局随机化)的存在,dyld 需要根据加载到内存的随机地址偏移,修正镜像内部的指针。
- Bind:处理镜像外部指针的指向,例如将符号名称绑定到具体的实现地址。
- Runtime 初始化阶段
当 dyld 完成基础加载后,就进入了 Objective-C Runtime 的舞台: -
- 注册 ObjC 类:Runtime 会读取编译时写入的类信息,并注册所有 Objective-C 类(class registration)。
- 处理 Category:将 Category 中定义的方法插入到原始类的方法列表中(category registration)。
- 调用初始化方法:按顺序执行声明为 attribute((constructor)) 的 C 函数、C++ 静态对象的构造函数,以及 Objective-C 类的 +load 方法。
完成以上步骤后,dyld 会找到 App 可执行文件的 main() 函数入口并调用,从而将接力棒交到开发者熟悉的代码世界中。
🧩 从 main() 到界面呈现
这一阶段是开发者可以主动控制和优化的核心环节。
- main() 函数
这是所有 C 系程序的起点,iOS App 也不例外。它通常由 Xcode 自动生成,核心工作是调用 UIApplicationMain 函数。 - AppDelegate 与首屏渲染
-
启动完成回调:系统会调用 AppDelegate 的 application(_:didFinishLaunchingWithOptions:) 方法。这里是开发者进行全局初始化(如初始化第三方 SDK、设置根视图控制器等)的关键位置。
-
创建 UI:系统会根据 Info.plist 中配置的 Main Storyboard 文件名(如有)自动加载主故事板,创建 UIWindow,并设置其根视图控制器(rootViewController)。当然,你也可以在此方法中手动创建 UIWindow 和根控制器。
-
首屏渲染:当根视图控制器的视图被加载和呈现时,会依次触发 viewDidLoad、viewWillAppear、viewDidAppear 等生命周期方法。在这些方法中,你需要进行界面布局和数据填充。当首帧画面渲染完成,用户就可以与 App 进行交互了
Any
任何实例的具体类型
- Any可以表示任何类型的实例,包括函数类型、结构体、枚举、类等。
- AnyObject 任何类类型的实例
any范围比较大
any包括所有类型,结构体 枚举 类函数等
anyobject 只包括类类型
Any: 代表任意类型,包括值类型和引用类型
AnyObject : 代表任意类类型 引用类型
与 OC Category 的区别
-
OC Category 需要名字,Swift 扩展无名字。
-
OC Category 能“声明”属性但不能“实现”存储属性;Swift 扩展同样只能写计算属性,但编译期直接报错而非运行时崩溃。
-
Swift 扩展支持协议遵守、泛型 where 约束,OC 做不到。
总结:在 Task 中,await 会挂起当前 Task 的执行,直到异步函数返回,然后继续执行后面的代码。
Task {
print("步骤1: 立即执行")
// 这里会挂起当前Task,但不会阻塞线程
// 线程可以继续执行其他任务
let result = try await someAsyncFunction()
// 只有在someAsyncFunction完成后才会执行
print("步骤2: 等待异步操作完成后执行")
}
Await会挂起当前任务的执行,直到异步操作完成
挂起期间线程不会阻塞,可以执行其他任务
await之后的代码必须等待异步操作完成成后才会执行
MainActor —— 专为 UI 准备的“主线程保险箱”
SPM
事件传递:
点击屏幕->事件传递给UIApplication->UIWindow>调用hitTest:WithEvent: 方法返回响应的视图
hitTest:WithEvent:方法内部通过pointInside:withEvent方法判断点击point是否在当前的window范围内
用户点击屏幕的某个位置,该事件会被传递给UIApplication,UIApplication又传递给当前的UIWindow,UIWindow会通过hitTest:WithEvent:方法返回响应的视图。hitTest:WithEvent:方法内部通过pointInside:withEvent:方法判断点击point是否在当前UIWindow范围内,如果在,则会遍历其中的所有子视图SubViews来查找最终响应此事件的视图,遍历方式为倒序遍历,即最后添加到UIWindow的视图最优先被遍历到,依次遍历,可以看作是递归调用。每个UIView中又都会调用其对应hitTest:WithEvent:方法,最终返回响应视图hit,如果hit有值,则hit视图就作为该事件的响应视图被返回,如果hit没有值,但在当前UIWindow范围内,则当前UIWindow作为事件的响应视图。
MarkdownKit
SSE
TextView
use_frameworks!
每个pod都会作为一个独立的动态框架被集成,
Module_headers 强制某个pod将其头文件构建为模块,从而允许Swift代码通过import 导入
静态库.a 静态framework
动态库.dylib 动态framework
IJKMediaFramework 媒体播放器 IJKPlayer, 利用FFmpeg 库提供流媒体处理、编解码和网络传输功能,
播放器:ZFPlayerSDK
ZFIJKPlayerManager 直播 — >>>> IJKFFMoviePlayerController
ZFAVPlayerManager 播放视频 - >>> AVPlayer
IJKFFMoviePlayerController 播放器
由FFmpeg统一集成软硬解码
软解码:libavcodec
硬解码:VideoToolBox
音视频会议: AgoraRtcEngine_iOS/RtcBasic
Swift 通过关联类型实现协议泛型
-
@MainActor:确保代码在主线程上执行,用于UI更新和主线程相关的操作。
-
@Sendable:确保闭包或函数在并发域之间安全传递,避免数据竞争。
@MainActor - 主线程隔离
作用:确保代码在主线程上执行,主要用于 UI 更新。
@Sendable - 可安全跨域传递
作用:标记可以在并发域之间安全传递的类型、函数或闭包。
| 特性 | @MainActor | @Sendable |
| 目的 | 线程隔离(主线程) | 跨域安全传递 |
| 作用对象 | 类、函数、属性 | 类型、闭包、函数 |
| 并发模型 | Actor 模型的一部分 | 数据竞争预防机制 |
| 编译检查 | 检查是否在主线程执行 | 检查是否可安全共享 |
pod 'DZJModule_Cerificate', :path => '../dzj_spec/',:subspecs => ['BaiDuFaceCertificate']
pod 'DZJModule_Cerificate/BaiDuFaceCertificate', :path => '../dzj_spec/'
import Combine
@propertyWrapper
struct MyPublished {
private var value: Value
private let subject: CurrentValueSubject<Value, Never>
var wrappedValue: Value {
get { value }
set {
value = newValue
subject.send(newValue)
}
}
var projectedValue: AnyPublisher<Value, Never> {
subject.eraseToAnyPublisher()
}
init(wrappedValue: Value) {
self.value = wrappedValue
self.subject = CurrentValueSubject(wrappedValue)
}
}
PublishSubject
当订阅PublishSubject的时候,只能接收订阅他之后发生的事件
subject.onNext()发出onNext事件,对应的还有OnError()和onCompleted()事件
ReplaySubject
当你订阅ReplaySubject的时候,你可以接收到订阅他之后的事件
但也可以接受订阅他之前发出的事件,接受前面几个事件,取决与bufferSize的大小
BehaviorSubject
BehaviorSubject会接受到订阅之前的最后一个事件及订阅后的都可以接收到
Pod::Spec.new do |s|
s.name = 'DZJModule_BaiDuFaceCertificate'
# ...其他配置...
s.pod_target_xcconfig = {
# 核心配置
'OTHER_LDFLAGS' => ['-ObjC', '-lc++'],
'ENABLE_BITCODE' => 'NO',
# 推荐补充配置
'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES', # 允许非模块化头文件
'DEFINES_MODULE' => 'YES', # 明确声明模块化
'GCC_PREPROCESSOR_DEFINITIONS' => 'BAIDU_FACE=1' # 添加预编译宏
}
# 百度人脸SDK通常需要这些系统库
s.libraries = ['c++', 'z', 'sqlite3.0']
s.frameworks = ['CoreTelephony', 'Security', 'SystemConfiguration']
end
-
可选闭包参数( (() -> Void)? )默认支持逃逸,无需显式添加 @escaping。
-
仅当闭包是非可选类型且需要逃逸时,才需要显式标记 @escaping:func doSomething(completion: @escaping () -> Void) { ... }
async await Actor 、Task Group
- Swift5.5
- 1 引入的async/await ,使用async 标记异步函数 使用await 等待异步结果
- 2 利用Task在同步上下文中调用异步方法
- 3 使用Actor 类型和MainActor 保护共享数据,避免数据竞争
通过声明某个类型为actor 保证其方法在同一时间只被一个任务调用,这样可以避免多线程并发访问是可能出现的数据竞争问题
全局actor
@globalActor: 处理全局共享资源的访问 使用@globalActor注解将某个类型声明为全局actor
- 4 使用withUnsafeContinuation将基于回调的旧api 包装成异步函数 • withUnsafeContinuation: 用于不抛出错误的回调
-
withUnsafeThrowingContinuation: 用于可能抛出错误的回调
-
使用withCheckedContinuation 会在调式时检查常见错误
// 旧的回调式API
func fetchDataWithCompletion(completion: @escaping (String) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
completion("数据加载完成!")
}
}
// 包装成异步函数
func fetchDataAsync() async -> String {
return await withUnsafeContinuation { continuation in
fetchDataWithCompletion { result in
continuation.resume(returning: result)
}
}
}
// 使用
func useAsyncFunction() async {
let result = await fetchDataAsync()
print(result) // 输出: "数据加载完成!"
}
@propertyWrapper
线程与task的区别
1 task是高级抽象 线程是低级原语
2 task开销极低本质是数据结构Swift运行时在协作线程池上调度 thread开销很高
3 task可以创建成千上万个,thread有限个
4 一个task在生命周期内可能在不同线程上执行 thread 一旦启动,代码通常固定在一个线程上
5 调度控制:task是协作式 thread是抢占式
Task与线程是多对一,由调度器复用
Await 会挂起任务并释放线程
Task执行顺序不保证,取决于挂起点与调度策略
https://juejin.cn/post/7539832718000881690
在同步方法中调用异步方法需要使用 Task 来创建一个并发环境,因为同步方法不能直接等待异步方法
在viewDidload中调用一个async 方法
override func viewDidLoad() {
super.viewDidLoad()
loadDataAndUpdateUI()
}
@MainActor
private func loadDataAndUpdateUI() {
Task { [weak self] in
do {
let data = try await self?.loadData()
// 因为方法被 @MainActor 标记,所以这里更新 UI 是在主线程上
self?.updateUI(with: data)
} catch {
self?.handleError(error)
}
}
}
在 iOS 中,NSTextStorage、NSLayoutManager 和 NSTextContainer 是 TextKit 的三个核心组件,它们共同负责富文本的存储、布局和渲染。当我们需要自定义下划线(例如控制偏移量、粗细、只对关键字加下划线)时,就必须借助它们来精确获取文本的绘制位置。下面详细解释其原理和协作流程。
1. 类型转换操作符
1.1 as (类型转换)
- 用于向上转型(将子类实例转换为父类类型)或匹配已知正确的类型(例如在switch语句中匹配类型)。
- 也可以用于桥接,比如Swift字符串和NSString之间的转换。
- 使用as进行转型时,如果转换失败,会引发编译错误或运行时错误(因此只有在确定转换安全时使用)。
1.2 as? (条件转换)
- 返回一个可选类型,如果转换成功,返回转换后的值,否则返回nil。
- 通常用于可能失败的转换,比如将任意类型转换为目标类型,但不确保成功。
1.3 as! (强制转换)
-
强制进行类型转换,如果转换失败,会触发运行时错误(程序崩溃)。
-
只有在非常确定转换一定会成功时才使用。
死锁:
1 对外暴露sync接口
2 主线程sync到主队列
struct Token: ~Copyable {
var value: String
}
-
解释:通过 ~Copyable 语法,Token 类型被标记为不可复制。
-
不可复制类型:通过 ~Copyable 语法,禁止值类型的复制行为。
-
消耗语义:使用 consuming 关键字标记函数参数,确保值在传递后被消耗,无法重复使用。
-
适用场景:适用于需要确保值只能被使用一次的场景,如一次性令牌、资源管理等。
Task.sleep
Task.yield
nonisolated 现可用于类型声明,避免从协议继承的全局 Actor 隔离。
模块的扩展成员不再隐式传递到其他文件,需显式导入。
问题示例:
若 Maps 和 GeoKit 模块都扩展了 Double.toRadians(),旧版本可能导致冲突。
解决方案:
在需要的文件中显式导入模块。并且开启MemberImportVisibility
withUnsafeThrowingContinuation
withUnsafeContinuation
| 问题 | 一句话答案 |
| 旧 SDK 回调如何转 async? | withCheckedThrowingContinuation 一键桥接 |
| actor 与类最大区别? | 编译期强制串行访问,自动解决数据竞争 |
| Sendable 对函数意味着什么? | 闭包及其捕获变量必须线程安全,否则编译报错 |
| concurrentMap 会不会乱序? | 内部用索引对还原,保证与输入顺序一致 |
compactMap 过滤nil值 flatMap : 展平嵌套结构
Swift 6 单利不安全问题
方案-: 将单例设计为actor, 外部通过await访问
方案二: 使用全局变量+sendable+合适的隔离
情况 A:不可变单例(只读)
如果单例只包含不可变数据,可以简单标记为 Sendable:
struct MyConfig: Sendable {
let apiKey: String
}
let globalConfig = MyConfig(apiKey: "123")
情况 B:可变单例(需要同步)
可以使用 actor 作为内部存储,或者使用锁 + @unchecked Sendable(需谨慎):
class MyManager: @unchecked Sendable {
static let shared = MyManager()
private let queue = DispatchQueue(label: "serial")
private var _counter = 0
var counter: Int {
get { queue.sync { _counter } }
set { queue.sync { _counter = newValue } }
}
private init() {}
}
注意:@unchecked Sendable 告诉编译器“我保证这个类型是线程安全的”,但如果实现有漏洞,依然可能产生数据竞争,所以必须自己确保正确同步。
方案三:使用全局 let + 不可变结构体 + 内部可变状态
有时单例需要提供同步的接口,但内部状态可以用 actor 封装,外部保持非 actor 的便捷访问:
| NSUInteger | UInt | 直接对应,无符号整数 |
| NSUInteger | Int | 实际开发中最常用 |
| NSUInteger | Array.count 的类型 | 系统 API 返回的计数 |
// 创建串行队列(与原始代码相同)
_ioQueue = DispatchQueue(label: "com.dzj.dzjCacheUtil.ioQueue")
// 或者明确指定串行
_ioQueue = DispatchQueue(label: "com.dzj.dzjCacheUtil.ioQueue", attributes: [])
// 创建并发队列
_ioQueue = DispatchQueue(label: "com.dzj.dzjCacheUtil.ioQueue", attributes: .concurrent)
// 创建串行队列并指定 QoS(服务质量)
_ioQueue = DispatchQueue(label: "com.dzj.dzjCacheUtil.ioQueue",
qos: .default,
attributes: [])
dispatch_group是一个初始值为LONG_MAX的信号量,group中的任务完成是判断其value是否恢复成初始值。
dispatch_group_enter和dispatch_group_leave必须成对使用并且支持嵌套。
如果dispatch_group_enter比dispatch_group_leave多,由于value不等于dsema_orig不会走到唤醒逻辑,dispatch_group_notify中的任务无法执行或者dispatch_group_wait收不到信号而卡住线程。如果是dispatch_group_leave多,则会引起崩溃。
Swift 使用协议扩展而不是方法交换
- Swiftjson
不需要考虑服务器返回的类型 。 会返回一个默认值,如果没有获取到数据的话就会返回一个默认值即 .stringValue获得空字符串"",.intValue得到 0,.arrayValue获得空数组[],
- ObjectMapper ObjectMapper中定义了一个协议,类 class 或者结构体 struct 需要实现Mappable协议, 这个协议包含下面两个方法:
init?(map: Map) {
}
mutating func mapping(map: Map) {
}
- HandyJosn
Swift 反射+内存赋值 缺点: 内存泄露 兼容性差
实现HandyJson 协议
可以自定义key
实现 Mapping函数来做这两个支持:
mapper.specify()
-
用 is:
只需要判断类型,不需要使用转换后的对象时
if object is MyClass { /* 仅做类型判断 */ } -
用 as?:
需要将对象当作目标类型使用时
if let converted = object as? MyClass {
converted.someMethod() // 安全使用转换后的对象
}
⚠️ 注意:Swift 中没有直接名为 iskindof 的操作符,Objective-C 的 isKindOfClass: 在 Swift 中对应 is。
是的,is操作符会包含父类。也就是说,子类的实例会被认为是其自身类型以及所有祖先类(父类、祖父类等)的类型
对比 is 和 as? 的继承行为:
| 场景 | is 结果 | as? 结果 |
|-----------------------|----------------|--------------------------|
| 实例是目标类型本身 | true | 成功(非 nil) |
| 实例是目标类型的子类 | true | 成功(非 nil) |
| 实例是目标类型的父类 | false | 失败(nil) |
| 实例是无关类型 | false | 失败(nil)