阅读视图
求求你,别在 Swift 协程开头写 guard let self = self 了!
别让 guard let self = self 锁死你的控制器:Swift 协程内存“绑架”大揭秘 🚀
01. 一个“灵异”的 deinit
你是否遇到过这种场景:在 ViewController 里的 Task 启动了一个长达 10 秒的图片上传任务。用户觉得太慢,点击“返回”退出了页面。按照 ARC 的逻辑,VC 应该立即销毁。
但控制台却一片寂静。直到 10 秒后上传完成,那个 deinit 才像“诈尸”一样蹦出来。
真相是:你的 ViewController 被“绑架”了,而绑匪正是你亲手写的 guard let self = self。
02. 硬核拆解:Task 到底在堆里做了什么?
要理解这个现象,我们得聊聊 Swift 协程的底层存储机制:异步帧(Async Frame) 。
传统的栈(Stack)
普通函数执行时,变量存在栈上。函数跑完,栈帧弹出,变量销毁。
协程的堆(Heap)
Task 是为了 await 设计的。await 的本质是 “挂起(Suspension)” 。
-
挂起瞬间:当代码运行到
await,当前线程会被释放去干别的事。为了保证await回来后代码能接着跑,Swift 必须把当前函数的所有状态(包括你的强引用self)从栈上拷贝到堆(Heap)上。
结论: 只要这个 Task 没运行到最后那个大括号 },堆上的空间就不会销毁。你提前 guard 出来的强引用 self 就像一把锁,把 VC 死死锁在堆里。
03. 别做“暴力解包”的搬运工 🧱
很多开发者习惯在 Task 开头就“暴力解包”:
Swift
// ❌ 错误示范:VC 将被锁死直到任务结束
Task { [weak self] in
guard let self = self else { return } // 1. 这里强行“弱转强”
print("⏳ 上传中...")
let url = await logic.uploadImage() // 2. 协程挂起,强引用 self 存入堆中
self.show(url)
}
此时的引用链:
Task (堆内存) ➔ 局部变量 self (强引用) ➔ ViewController
即便用户关了页面,Task 不结束,VC 就释放不了。这就是 “内存延迟释放” 。
04. 满分范本:后置解包法 💡
我们要做的其实很简单:让任务先跑,人若还在,再干活。
Swift
// ✅ 工业级安全模版
Task { [weak self] in
print("⏳ 任务发起,此时 VC 自由了")
// 关键:不提前 guard,直接 await 发起异步
let url = await self?.logic.uploadImage()
// 任务回来了,此时再看“人还在不在”
guard let self = self else {
print("🛑 页面已关,逻辑优雅退出")
return
}
self.show(url) // 只有在这里,才产生瞬时的强引用
}
-
收益:用户关掉页面,VC 引用计数清零,立刻执行
deinit。
05. 进阶大师:不仅省内存,还要省流量 📡
既然观众离场了,戏台确实不该演了。对于耗时极长的任务,我们应该在 deinit 时主动取消。
Swift
class MasterVC: UIViewController {
private var loadTask: Task<Void, Never>?
func start() {
loadTask?.cancel()
loadTask = Task { [weak self] in
let res = await logic.longTimeRequest()
guard let self = self, !Task.isCancelled else { return }
self.done(res)
}
}
deinit {
loadTask?.cancel() // 页面关,网络停,流量省
print("✅ 完美释放!")
}
}
总结:我的避坑座右铭
-
挂起前(await)别解包:别在网络回来前,用
guard把 VC 锁死。 -
挂起时用可选链:
self?.logic.request()才是真正的弱引用。 - 回来后再检查:任务结束时,确认 VC 还在,再刷新 UI。
记住:ViewController 是“消耗品”,别让它成为你内存里的“传家宝”。
希望这篇文章能帮你理清 Swift 协程的引用谜团!如果你也曾被 deinit 延迟困扰过,欢迎在评论区分享你的经历。👇
下一步建议: 你可以去检查一下项目中那些处理大图上传或复杂计算的 Task,把 guard 后移,你会发现 App 的响应速度和内存表现会有质的飞跃。
#1 onLongPressGesture
《Flutter全栈开发实战指南:从零到高级》- 23 -混合开发与WebView
Swift 疑难杂想
Git Pull 策略完全指南:Merge、Rebase、Fast-forward 深度对比
前言
在使用 Git 进行版本控制时,我们经常会遇到这样的错误提示:
fatal: Need to specify how to reconcile divergent branches.
这个错误通常发生在执行 git pull 时,本地分支和远程分支出现了分歧。Git 需要你明确指定如何处理这种分歧。本文将深入解析 Git 的四种 pull 策略,帮助你根据实际场景选择最合适的方案。
四种 Pull 策略概览
| 策略 | 配置命令 | 历史图特征 | 适用场景 | 核心特点 |
|---|---|---|---|---|
| Merge(合并) | git config pull.rebase false |
有分叉和合并点 | 团队协作项目 | 安全、保留完整历史 |
| Rebase(变基) | git config pull.rebase true |
线性历史 | 个人功能分支 | 历史清晰、会改写提交 |
| Fast-forward Only | git config pull.ff only |
严格线性 | 严格流程控制 | 最严格、不允许分歧 |
| 默认行为 | 无配置 | 取决于选择 | 临时使用 | 灵活但需手动选择 |
一、Merge(合并)策略
工作原理
Merge 策略是 Git 最传统、最安全的合并方式。当本地和远程分支出现分歧时,Git 会创建一个合并提交(merge commit),将两个分支的历史整合在一起,同时保留所有分支信息。
示例场景
假设你正在开发一个功能,本地有提交 C,而远程有其他人提交的 D:
初始状态:
本地分支: A---B---C (你的提交)
远程分支: A---B---D (别人的提交)
执行 git pull (使用 merge 策略):
结果: A---B---C---M
\ /
D---┘
合并后,历史图中会显示一个合并点 M,清楚地记录了分支的合并过程。
适用场景
- ✅ 团队协作项目:多人同时开发同一分支
- ✅ 需要完整历史:要求保留所有分支和合并信息
- ✅ 代码审查流程:需要追踪代码的来源和合并路径
- ✅ 生产环境:需要安全可靠的合并方式
优点
- 安全性高:不会丢失任何提交,所有历史都被保留
- 信息完整:保留完整的分支信息,便于追踪和审计
- 冲突处理简单:只需解决一次冲突即可完成合并
- 适合协作:多人协作时不会造成混乱
缺点
- 产生合并提交:会创建额外的合并提交,可能让历史图变得复杂
- 历史不够线性:提交历史不是一条直线,可能影响可读性
- 提交记录增多:合并提交会增加提交记录的数量
配置方法
# 仅对当前仓库生效
git config pull.rebase false
# 全局配置(所有仓库)
git config --global pull.rebase false
二、Rebase(变基)策略
工作原理
Rebase 策略会将你的本地提交"重新应用"到远程最新提交之上,创建一个线性的提交历史。这个过程会改写提交历史,生成新的提交对象(commit hash 会改变)。
示例场景
同样的情况,使用 rebase 策略:
初始状态:
本地分支: A---B---C (你的提交)
远程分支: A---B---D (别人的提交)
执行 git pull (使用 rebase 策略):
结果: A---B---D---C' (C'是重新应用的提交,hash已改变)
可以看到,提交历史变成了一条直线,C 被重新应用为 C',放在了 D 之后。
适用场景
- ✅ 个人功能分支:在自己的分支上开发,未推送到共享分支
- ✅ 追求线性历史:希望提交历史保持线性,便于阅读
- ✅ 代码审查前整理:在提交 PR/MR 前整理提交历史
- ✅ 个人项目:不需要考虑多人协作的情况
优点
- 历史清晰:提交历史呈线性,易于阅读和理解
- 无合并提交:不会产生额外的合并提交
- 提交记录简洁:提交历史更加整洁
缺点
- 改写历史:会改变提交的 hash 值,可能影响已建立的引用
-
需要强制推送:如果提交已推送,需要使用
git push --force - 协作风险:多人协作时可能造成混乱,不推荐在共享分支使用
- 冲突处理复杂:可能需要多次解决冲突(每个提交都可能遇到冲突)
⚠️ 重要注意事项
不要在已推送到共享分支的提交上使用 rebase!
如果提交已经推送到远程并被其他人使用,使用 rebase 会改写历史,可能导致:
- 其他开发者的本地仓库出现混乱
- 需要强制推送,可能覆盖其他人的工作
- 破坏团队协作流程
配置方法
# 仅对当前仓库生效
git config pull.rebase true
# 全局配置(所有仓库)
git config --global pull.rebase true
三、Fast-forward Only(仅快进)策略
工作原理
Fast-forward Only 策略只允许"快进"合并。这意味着本地分支必须是远程分支的前缀(本地分支的所有提交都在远程分支的历史中)。如果存在分歧,pull 操作会直接失败,要求你先处理分歧。
示例场景
成功情况(可以快进):
本地分支: A---B---C
远程分支: A---B---C---D
执行 git pull (使用 fast-forward only):
结果: A---B---C---D ✅ (成功,可以快进)
失败情况(存在分歧):
本地分支: A---B---C (你的提交)
远程分支: A---B---D (别人的提交)
执行 git pull (使用 fast-forward only):
结果: ❌ 失败!需要先处理分歧
适用场景
- ✅ 严格的代码审查流程:要求所有合并都必须是快进的
- ✅ 主分支保护:维护主分支(如 main/master)的严格性
- ✅ 强制同步:要求开发者必须先同步远程代码再提交
- ✅ CI/CD 流程:配合自动化流程,确保代码质量
优点
- 历史最干净:确保提交历史严格线性,没有任何分叉
- 强制规范:强制开发者保持代码同步,避免意外的合并提交
- 流程清晰:明确的工作流程,减少混乱
缺点
- 不够灵活:遇到分歧时必须先手动处理(使用 rebase 或 merge)
- 增加复杂度:可能需要额外的步骤来处理分歧
- 可能失败:pull 操作可能失败,需要开发者主动处理
配置方法
# 仅对当前仓库生效
git config pull.ff only
# 全局配置(所有仓库)
git config --global pull.ff only
四、默认行为(未配置)
工作原理
如果你没有配置任何 pull 策略,Git 的行为取决于版本:
- Git 2.27+:会提示你选择如何处理分歧
- 旧版本:可能默认使用 merge 或根据情况自动选择
适用场景
- ✅ 临时使用:不确定使用哪种策略时
- ✅ 灵活需求:不同情况需要不同策略
- ✅ 学习阶段:想了解不同策略的效果
优点
- 灵活:可以根据具体情况选择最合适的策略
缺点
- 需要手动选择:每次遇到分歧都需要手动指定
- 可能忘记配置:容易忘记配置导致操作失败
- 不够自动化:无法实现自动化流程
实际使用建议
1. 团队协作项目(推荐:Merge)
git config pull.rebase false
为什么选择 Merge?
- 团队协作中最安全可靠的方式
- 保留完整的历史记录,便于追踪和审计
- 不会改写已推送的提交,避免影响其他开发者
- 冲突处理相对简单,只需解决一次
2. 个人功能分支(可选:Rebase)
git config pull.rebase true
使用前提:
- ⚠️ 仅用于未推送的提交
- ⚠️ 仅在自己的功能分支上使用
- ⚠️ 合并到主分支前可以整理提交历史
3. 严格流程控制(可选:Fast-forward Only)
git config pull.ff only
适用条件:
- 需要配合严格的代码审查流程
- 团队有明确的工作流程规范
- 主分支需要保持严格的线性历史
全局设置 vs 本地设置
全局设置(推荐用于个人偏好):
# 设置全局默认策略
git config --global pull.rebase false
本地设置(推荐用于项目规范):
# 仅对当前仓库生效
git config pull.rebase false
建议:
- 个人偏好使用全局设置
- 项目规范使用本地设置(可以提交到仓库的
.git/config)
实用技巧
查看当前配置
# 查看当前仓库的 pull 策略配置
git config pull.rebase
git config pull.ff
# 查看全局配置
git config --global pull.rebase
git config --global pull.ff
# 查看所有相关配置
git config --list | grep pull
临时覆盖配置
即使配置了默认策略,也可以在单次操作时临时覆盖:
# 临时使用 rebase(即使配置了 merge)
git pull --rebase
# 临时使用 merge(即使配置了 rebase)
git pull --no-rebase
# 临时使用 fast-forward only
git pull --ff-only
处理已出现的分歧
如果已经遇到了分歧错误,可以这样处理:
方法 1:使用 merge(推荐)
git pull --no-rebase
# 或
git pull --merge
方法 2:使用 rebase(需谨慎)
git pull --rebase
方法 3:先 fetch 再决定
git fetch origin
git log HEAD..origin/develop # 查看远程的新提交
git merge origin/develop # 或 git rebase origin/develop
总结
选择合适的 Git pull 策略取决于你的工作场景和团队规范:
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 团队协作项目 | Merge | 安全、可靠、保留完整历史 |
| 个人功能分支 | Rebase | 保持历史线性、提交前整理 |
| 严格流程控制 | Fast-forward Only | 强制规范、保持主分支干净 |
| 灵活需求 | 不配置 | 根据情况手动选择 |
核心要点
- 团队协作优先使用 Merge:最安全可靠,适合大多数场景
- Rebase 仅用于未推送的提交:避免影响其他开发者
- Fast-forward Only 需要配合流程:确保团队有明确的工作规范
- 可以临时覆盖配置:根据具体情况灵活调整
最佳实践
- ✅ 团队项目统一使用 Merge 策略
- ✅ 个人分支可以使用 Rebase 整理提交
- ✅ 主分支使用 Fast-forward Only 保持严格性
- ✅ 配置写入项目文档,确保团队成员了解
希望本文能帮助你更好地理解和使用 Git 的 pull 策略,选择最适合你项目需求的方案!
参考资源
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏和分享!如有问题或建议,欢迎在评论区讨论。
iOS内存映射技术:mmap如何用有限内存操控无限数据
当一个iOS应用需要处理比物理内存大10倍的文件时,传统方法束手无策,而mmap却能让它流畅运行。这种神奇能力背后,是虚拟内存与物理内存的精密舞蹈。
01 内存管理的双重世界:虚拟与物理的分离
每个iOS应用都生活在双重内存现实中。当你声明一个变量或读取文件时,你操作的是虚拟内存地址,这是iOS为每个应用精心编织的“平行宇宙”。
这个宇宙大小固定——在64位iOS设备上高达128TB的虚拟地址空间,远超任何物理内存容量。
虚拟内存的精妙之处在于:它只是一个巨大的、连续的地址范围清单,不直接对应物理内存芯片。操作系统通过内存管理单元(MMU)维护着一张“翻译表”(页表),将虚拟页映射到物理页框。这种设计使得应用可以假设自己拥有几乎无限的内存,而实际物理使用则由iOS动态管理。
这种分层架构是mmap处理超大文件的基础:应用程序可以在虚拟层面“拥有”整个文件,而只在物理层面加载需要部分。
02 传统文件操作的二重拷贝困境
要理解mmap的革命性,先看看传统文件I/O的“双重复制”问题:
// 传统方式:双重拷贝的典型代码
NSData *fileData = [NSData dataWithContentsOfFile:largeFile];
这个看似简单的操作背后,数据经历了漫长旅程:
磁盘文件数据
↓ (DMA拷贝,不经过CPU)
内核页缓存(Page Cache)
↓ (CPU参与拷贝,消耗资源)
用户空间缓冲区(NSData内部存储)
双重拷贝的代价:
- 时间开销:两次完整数据移动
- CPU消耗:拷贝操作占用宝贵计算资源
- 内存峰值:文件在内存中同时存在两份副本(内核缓存+用户缓冲区)
- 大文件限制:文件必须小于可用物理内存
对于100MB的文件,这还能接受。但对于2GB的视频文件,这种方法在1GB RAM的设备上直接崩溃。
03 mmap的魔法:一次映射,零次拷贝
mmap采用完全不同的哲学——如果数据必须在内存中,为什么不直接在那里访问它?
// mmap方式:建立直接通道
int fd = open(largeFile, O_RDONLY);
void *mapped = mmap(NULL, fileSize, PROT_READ, MAP_PRIVATE, fd, 0);
// 现在可以直接通过mapped指针访问文件内容
mmap建立的是直接通道而非数据副本:
磁盘文件数据
↓ (DMA直接拷贝)
物理内存页框
↖(直接映射)
进程虚拟地址空间
关键突破:
- 单次拷贝:数据从磁盘到内存仅通过DMA传输一次
- 零CPU拷贝:没有内核到用户空间的额外复制
- 内存效率:物理内存中只有一份数据副本
- 按需加载:仅在实际访问时加载对应页面
04 虚拟扩容术:如何用有限物理内存处理无限文件
这是mmap最反直觉的部分:虚拟地址空间允许“承诺”远多于物理内存的资源。
当映射一个5GB文件到2GB物理内存的设备时:
// 这在2GB RAM设备上完全可行
void *mapped = mmap(NULL, 5*1024*1024*1024ULL,
PROT_READ, MAP_PRIVATE, fd, 0);
按需加载机制确保只有实际访问的部分占用物理内存:
- 建立映射(瞬间完成):仅在进程页表中标记“此虚拟范围映射到某文件”
-
首次访问(触发加载):访问
mapped[offset]时触发缺页中断 - 按页加载(最小单位):内核加载包含目标数据的单个内存页(iOS通常16KB)
- 动态换页(透明管理):物理内存紧张时,iOS自动将不常用页面换出,需要时再换入
内存使用随时间变化:
时间轴: |---启动---|---浏览开始---|---跳转章节---|
物理内存: | 16KB | 48KB | 32KB |
虚拟占用: | 5GB | 5GB | 5GB |
应用“看到”的是完整的5GB文件空间,但物理内存中只保留最近访问的少量页面。
05 性能对比:数字说明一切
通过实际测试数据,揭示两种方式的性能差异:
| 操作场景 | 传统read() | mmap映射 | 优势比 |
|---|---|---|---|
| 首次打开500MB文件 | 1200ms | <10ms | 120倍 |
| 随机访问100处数据 | 850ms | 220ms | 3.9倍 |
| 内存峰值占用 | 500MB | 800KB | 625倍更优 |
| 处理2GB视频文件(1GB RAM) | 崩溃 | 正常播放 | 无限 |
| 多进程共享读取 | 每进程500MB | 共享物理页 | N倍节省 |
实际测试代码:
// 测试大文件随机访问性能
- (void)testRandomAccess {
// 传统方式
NSData *allData = [NSData dataWithContentsOfFile:largeFile];
start = clock();
for (int i = 0; i < 1000; i++) {
NSUInteger randomOffset = arc4random_uniform(fileSize-100);
[allData subdataWithRange:NSMakeRange(randomOffset, 100)];
}
traditionalTime = clock() - start;
// mmap方式
int fd = open([largeFile UTF8String], O_RDONLY);
void *mapped = mmap(NULL, fileSize, PROT_READ, MAP_PRIVATE, fd, 0);
start = clock();
for (int i = 0; i < 1000; i++) {
NSUInteger randomOffset = arc4random_uniform(fileSize-100);
memcpy(buffer, mapped + randomOffset, 100);
}
mmapTime = clock() - start;
}
06 iOS中的实践应用
mmap在iOS系统中无处不在:
系统级应用:
- 应用启动优化:iOS使用mmap加载可执行文件和动态库,实现懒加载
- 数据库引擎:SQLite的WAL模式依赖mmap实现原子提交
- 图像处理:大图像使用mmap避免一次性解码
开发实战示例:
// Swift中安全使用mmap处理大日志文件
class MappedFileReader {
private var fileHandle: FileHandle
private var mappedPointer: UnsafeMutableRawPointer?
private var mappedSize: Int = 0
init(fileURL: URL) throws {
self.fileHandle = try FileHandle(forReadingFrom: fileURL)
let fileSize = try fileURL.resourceValues(forKeys:[.fileSizeKey]).fileSize!
// 建立内存映射
mappedPointer = mmap(nil, fileSize, PROT_READ, MAP_PRIVATE,
fileHandle.fileDescriptor, 0)
guard mappedPointer != MAP_FAILED else {
throw POSIXError(.EINVAL)
}
mappedSize = fileSize
}
func readData(offset: Int, length: Int) -> Data {
guard let base = mappedPointer, offset + length <= mappedSize else {
return Data()
}
return Data(bytes: base.advanced(by: offset), count: length)
}
deinit {
if let pointer = mappedPointer {
munmap(pointer, mappedSize)
}
}
}
07 局限与最佳实践
适用场景:
- 大文件随机访问(视频编辑、数据库文件)
- 只读或低频写入的数据
- 需要进程间共享的只读资源
- 内存敏感的大数据应用
避免场景:
- 频繁小块随机写入(产生大量脏页)
- 网络文件系统或可移动存储
- 需要频繁调整大小的文件
iOS特别优化建议:
- 对齐访问:确保访问按16KB页面边界对齐
- 局部性原则:组织数据使相关部分在相近虚拟地址
-
预取提示:对顺序访问使用
madvise(..., MADV_SEQUENTIAL) -
及时清理:不再需要的区域使用
munmap释放
08 未来展望:统一内存架构下的mmap
随着Apple Silicon的演进,iOS内存架构正向更深度统一发展:
趋势一:CPU/GPU直接共享映射内存
- Metal API允许GPU直接访问mmap区域
- 视频处理无需CPU中介拷贝
趋势二:Swap压缩的智能化
- iOS 15+的Memory Compression更高效
- 不活跃mmap页面被高度压缩,而非写回磁盘
趋势三:持久化内存的兴起
- 未来设备可能配备非易失性RAM
- mmap可能实现真正“内存速度”的持久化存储
技术进化的本质是抽象层次的提升。mmap通过虚拟内存这一精妙抽象,将有限的物理内存转化为看似无限的资源池。在移动设备存储快速增长而内存相对有限的背景下,掌握mmap不是高级优化技巧,而是处理现代iOS应用中大型数据集的必备技能。
当你的应用下一次需要处理大型视频、数据库或机器学习模型时,记得这个简单的准则:不要搬运数据,要映射数据。让iOS的虚拟内存系统成为你的盟友,而非限制。
iOS 知识点 - 一篇文章弄清「输入事件系统」(【事件传递机制、响应链机制】以及相关知识点)
iOS 事件系统全景图(硬件 → UIKit → 控件)
一个用户手指触摸屏幕的事件,从硬件到应用层,大致的经历是:
[ 触摸屏幕 ]
↓
[ IOKit -> IOHIDEvent ] (硬件事件)
↓
[ SpringBoard / BackBoard / SystemServer ] (系统事件中转)
↓
[ UIApplication → RunLoop Source → _UIApplicationHandleEventQueue ] (App 事件入口)
↓
[ UIKit 生成触摸序列 ] (UITouch / UIEvent)
↓
[ UIWindow → UIView ] (事件传递机制: hitTest / pointInside)
↓
[ UIGestureRecognizer ] (手势识别 / 状态机 / 冲突处理)
↓
[ UIResponder ] (响应链: touchesBegan / nextResponder)
↓
[ UIcontrol → Target-Action ] (控件事件)
| 模块 | 关键词 | 代表类 |
|---|---|---|
| 硬件输入系统 | IOKit / HID / RunLoop Source | — |
| 触摸事件系统 | Touch / Phase / Event | UITouch / UIEvent |
| 事件传递机制 | hitTest / pointInside | UIView / UIWindow |
| 手势识别机制 | state / requireToFail / delegate | UIGestureRecognizer 系列 |
| 响应链机制 | nextResponder / touches | UIResponder / UIViewController |
| 控件事件系统 | target-action / sendActions | UIControl / UIButton |
| RunLoop驱动层(补充) | CFRunLoopSource, Observer | CFRunLoop, UIApplication |
一、硬件输入系统
-
IOKit / HID 驱动 负责把物理触摸信号转成
IOHIDEvent; - 这些
IOHIDEvent由 backboardd 转发给前台进程(Your App); - 主线程 RunLoop 注册了
_UIApplicationHandleEventQueue()作为输入源,接收事件。
二、触摸事件系统
iOS 的输入事件分为几种类型:
| 类型 | 描述 | 相关类 |
|---|---|---|
| Touch | 单指/多指触摸 | UITouch |
| Press | 按压 | UIPress |
| Motion | 摇一摇、重力加速度 | UIEventSubtypeMotionShake |
| Remote Control | 耳机线控 / 外设 | UIEventSubtypeRemoteControl |
-
UITouch- 每根手指独立对应一个
UITouch对象 - 保存触摸状态、位置、timestamp、phase、唯一 identifier
- phase 会随手指动作变化(Began → Moved → Ended/Cancelled)
- 每根手指独立对应一个
-
触摸序列 (Touch Sequence):一个概念(用来描述 “一次连续的触摸过程”)
- 单指连续触摸,从手指接触到抬起或取消
- 对应一个
UITouch对象的完整生命周期
-
多指触摸
- 每根手指都有自己的
UITouch→ 多个触摸序列并行 -
UIEvent封装同一时间点的所有触摸
- 每根手指都有自己的
-
UIEvent
- 一个
UIEvent对象封装一批同时发生的UITouch(或 presses/motion/remote 控件事件) - event.timestamp = 事件发生的时间点
- event.type = touches / presses / motion / remoteControl
- 一个
三、UIKit 分发层(事件传递机制)
UIKit 在接收到事件后开始做「命中检测」🎯
其 核心调用链 是:
UIApplication sendEvent:
↓
UIWindow sendEvent: // 从 window 开始
↓
hitTest:withEvent: // 做递归「命中检测」🎯
↓
pointInside:withEvent:
-
hitTest:规则(可交互条件): 1. view.userInteractionEnabled == YES 2. view.hidden == NO 3. view.alpha > 0.01 4. pointInside == YES- 倒序遍历 subviews,返回最上层命中的 view。
- 将得到的 view 作为 First Responder 候选人。
四、手势识别层(UIGestureRecognizer 系列)
- 核心思想:手势识别发生在 时间传递后、响应链前;手势识别器监听 触摸序列,根据预设规则判断是否满足手势条件。
每个手势识别器都有一套状态机和冲突调度逻辑(手势冲突)
状态机(UIGestureRecognizerState)
| 状态 | 含义 | 触发时机 |
|---|---|---|
| .Possible | 初始状态 | 等待识别开始 |
| .Began | 识别开始 | 手势识别成功,手势开始响应 |
| .Changed | 手势进行中 | 位置/角度变化中 |
| .Ended | 识别完成 | 手势完成(抬手、离开) |
| .Cancelled | 被系统或上层取消 | 如中断或手势冲突 |
| .Failed | 未识别成功 | 条件不满足(时间太短、移动太远) |
- 状态迁移 大致是:
Possible → Began → Changed → Ended
→ Failed
→ Cancelled
手势冲突与协调机制
多个手势可能同时附着在 同一视图/同一层级 上,系统需要协调 “谁可以先响应”。
-
手势关系:每个
UIGestureRecognizer都有一个「关系图」,由以下规则控制:
| 规则 | 方法 | 含义 |
|---|---|---|
| 失败依赖 | requireGestureRecognizerToFail: |
让某个手势必须等待另一个手势失败后再识别 |
| 同时识别 | gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: |
允许多个手势同时识别 |
| 禁止识别 | gestureRecognizer:shouldReceiveTouch: |
完全忽略某次触摸 |
| 优先识别 | gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer: |
指定优先级关系 |
-
优先级调度
- 根据依赖关系构建「手势图」;
- 同步触摸输入,驱动每个手势的状态机;
- 当有手势识别成功后,让互斥手势进入 .Failed。
-
举例:在 scrollview 上增加 tap 手势。 [tap requireGestureRecognizerToFail:scrollView.panGestureRecognizer];
- 表示「滚动优先于点击」;只有 pan 失败后,tap 才能触发。
手势与 Touch 的竞争关系
| 场景 | 结果 |
|---|---|
| ✅手势识别 success | 手势回调触发,touches 系列不会再调用 |
| ❌手势识别 failure | 事件进入响应链,触发 touches 系列方法(touchesBegan / Moved / Ended) |
| ❌手势识别 cancel | 调用touchesCancelled,touches 系列不会再调用 |
手势识别器接管 触摸序列 之后,UIKit 不会再把 touches 事件下发给视图层。
五、响应链机制(Responder Chain)
当手势识别失败后,触摸事件才能进入 UIResponder。
1️⃣ 事件流向(子 -> 父)
1 - UIView
→ 2 - UIViewController (若有)
→ 3 - UIWindow
→ 4 - UIApplication
→ 5 - AppDelegate
- 如果当前 responder 不处理事件,会传递给 nextResponder。
六、控件事件系统(UIControl)
UIControl → UIView → UIResponder → NSObject
UIKit 在响应链之上又封装了一层抽象机制:Target-Action。
-
UIButton/UISwitch/UISlider等继承自UIControl。 -
UIControl通过 touches 系列方法 监控触摸,然后触发事件。
流程:
[ 触摸序列 → UIView (touchesBegan/Moved/Ended) ]
↓
[ UIControl (拦截触摸) ]
↓
判断事件类型
↓
[sendActionsForControlEvents:]
↓
执行注册的 Target-Action 回调
控件事件类型 (常用):
| 类型 | 时机 |
|---|---|
| TouchDown | 手指按下 |
| TouchUpInside | 在控件内抬起(最常用) |
| ValueChanged | 值改变(Slider/Switch) |
思考🤔:为什么在 UIScrollView 上的 UIButton 事件响应有延迟?
现象:
- 点击按钮 → 高亮/触发 action 延迟约 100~200ms
- 滑动触发滚动时,按钮点击可能被“吃掉”
原因分析
| 控件 | 事件处理机制 |
|---|---|
| UIScrollView | 内部有 UIPanGestureRecognizer 判断拖动;默认 delaysContentTouches = YES,会延迟将 touchesBegan 传给子控件 |
| UIButton | 依赖 touchesBegan/Moved/Ended 来管理高亮和触发 action;无法立即处理 touches,如果手势被占用,可能收到 touchesCancelled |
✅ 核心点:
-
UIScrollView先抢占触摸 → 拖动手势触发 →UIButton延迟或取消事件。 -
UIButton事件依赖 触摸序列未被取消 才能触发 target-action。
为什么 UIScrollView 先抢占触摸 ?
- hitTest 结果
- 手指点击在
UIButton上 → 通过事件传递机制 → 设置UIButton为 First Responder 候选人 - 但是
UIScrollView内部的 panGestureRecognizer 也会监听同一触摸序列:- 手势识别器在
touchesBegan延迟期间观察手势意图;- 如果 panGesture 成功,
UIKit会将触摸序列会被标记 “被UIScrollView占用” → UIButton 收到touchesCancelled。 - 如果 panGesture 失败,触摸序列被
UIButton占有。
- 如果 panGesture 成功,
- 手势识别器在
- 手指点击在
这个延迟可以通过 UIScrollView 的 delaysContentTouches 字段取消掉。
Swift中Package Manager的使用
Swift中Package Manager的使用
一、Package文件构成
Swift Package Manager简称SPM是苹果官方为swift语言提供的强大的依赖管理工具。能够自动化地处理包的依赖下载、编译、链接和管理。
Products:在包中的每个target最终都可能构建成一个Library或者一个execute作为product,这是package编译后的产物,
Target:构建单元,包含一组源代码文件,可以是一个库,可执行文件等。可依赖其他目标,如library、executable。一个package可以包含多个target
Dependencies:package所依赖的其他package,SPM会自动下载并解析这些依赖,确保项目的所有库都能正确构建。
Tool Version:最低支持的Swift工具链版本。
二、SPM的优点
对比Cocoapods,SPM具有以下优点。
- 无需安装,Xcode11以上版本自带
- 苹果官方维护,不用担心和Cocoapods一样停止维护
- 安装第三方库的时候比Cocoapods快(依赖源在github,有些要翻墙)
- 使用SPM构建时比Cocoapods快
三、SPM缺点
- 每次打开App 都会重新拉取 所有依赖的库
- 更新时间长(访问github 还需要进行科学上网)
- 支持文档少,
- 远端仓库对网络要求高
四、创建Package的两种方式:
1、常用命令:
mkdir SwiftPackageTest # 生成的Package的名称
cd SwiftPackageTest
swift package init --type library # 初始化库包
swift build # 构建
swift test # 运行测试
swift run <executable-target> # 运行可执行目标
swift package resolve # 解析依赖
swift package update # 更新依赖
基本使用
通过命令可以快速
# 创建一个库包
swift package init --name MyLib --type library
# 创建一个可执行包
swift package init --name MyLib --type executable
这将在当前目录生成一个标准的库包结构:
MyLib/
├── Sources/
│ └── MyLib/
│ └── MyLib.swift
├── Tests/
│ └── MyLibTests/
│ └── MyLibTests.swift
└── Package.swift
Package.swift清单文件的内容通常如下:
MyLib.swift文件
Sources目录是实现代码的存放位置,MyLib.swift一般作为程序的入口,用于处理命令行参数并调用核心功能。
构建和测试
# 编译包
swift build
# 运行测试
swift test
# 运行包
swift run
2、使用Xcode界面创建
Xcode—> 工具栏File—>New—>Package—>Libary
五、Package的配置
// swift-tools-version:6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "MyLib",
platforms: [.iOS(.v18), .macOS(.v15)], // 指定包所支持的平台和最低版本
products: [
.library(name: "MyLib", targets: ["MyLib"]) // 指编译后的包,对外提供
],
dependencies: [ // 声明此包所依赖的外部包
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0")
],
targets: [ // 定义包的相关信息
.target(
name: "MyLib",
dependencies: ["Alamofire"],
resources: [.process("Resources")]
),
.testTarget(
name: "MyLibTests",
dependencies: ["MyLib"]
)
]
)
-
name: Swift包的名字,或者‘ nil ’使用包的Git URL来推断名字。
-
defaultLocalization:资源的默认本地化。
-
platforms:具有自定义部署目标的受支持平台列表。
- 支持的平台和对应的系统版本
- platforms:[
- .macOS(.v11), .iOS(.v12),.tvOS(.v12)
- ]
-
pkgConfig: C模块的名称。如果存在,Swift包管理器会搜索 <名称>。获取系统目标所需的附加标志。
-
providers:系统目标的包提供程序。
-
products:此包提供给客户使用的产品列表。
编译后的产物一般分为两种 可执行文件 静态库或动态库
- dependencies:包依赖列表。
-
添加依赖的包,一般指向包源的git路径和版本环境,或者包依赖的本地路径
-
依赖包的添加支持以下五种方式
- git源 + 确定的版本号
- git源 + 版本区间
- git源 + commit号
- git源 + 分支名
- 本地路径
.package(url: "https://github.com/Alamofire/Alamofire.git", .exact("1.2.3")),
.package(url:"https://github.com/Alamofire/Alamofire.git", .branch("master")),
.package(url:"https://github.com/Alamofire/Alamofire.git", from: "1.2.3"),
.package(url:"https://github.com/Alamofire/Alamofire.git",.revision("e74b07278b926c9ec6f9643455ea00d1ce04a021"),
.package(url: "https://github.com/Alamofire/Alamofire.git", "1.2.3"..."4.1.3"),
.package(path: "../Foo"),
- targets:作为这个包的一部分的目标列表。
-
target是Package的基本构建,和xcodeproject一样,Package可以有多个target
-
target分为三种类型
- 常规性 .regular
- 测试类型 .test
- 系统库类型 .system
- swiftLanguageModes:此包兼容的Swift语言模式列表。
六、在Xcode中导入包
- 在Xcode中打开你的项目。
- 选择菜单栏的File > Add Packages...。
- 在弹出的窗口中,选择Add Local添加本地的package,或输入包存在的网址。
- 选择完成后,点击Add Package,Xcode会自动解析并下载该包及其所有依赖项。
- 依赖的包会出现在项目导航器的Package Dependencies部分,然后可以在代码中直接import使用。
在Xcode中删除包 如果在Xcode中导入包后,无法在Package Dependencies部分删除包,可以在项目.xcodeproj包内内容下的project.pbxproj里进行包的删除,删除后保存文件即可。
iOS UIKit 全体系知识手册(Objective-C 版)
UIKit 是 iOS/iPadOS 开发的核心 UI 框架,基于 Objective-C 构建,封装了所有可视化界面、交互、布局、渲染相关的能力,是构建 iOS 应用的基础。以下从「基础架构→核心组件→布局→事件→渲染→适配→优化→调试」全维度拆解 UIKit 知识体系,覆盖开发全场景。
一、UIKit 基础核心(框架基石)
1. UIKit 定位与依赖
-
核心作用:提供 iOS 应用的可视化界面、用户交互、布局管理、事件处理等能力,是上层业务与底层系统(Core Graphics/Core Animation/Foundation)的桥梁。
-
依赖关系:
- 基于 Foundation(数据处理:NSString/NSDictionary 等);
- 依赖 Core Graphics(绘图)、Core Animation(动画/渲染)、Core Text(文本排版);
- 兼容 AppKit(macOS)部分逻辑,但针对移动设备做了轻量化适配。
-
核心设计思想:基于「响应者链」+「MVC 架构」,视图(UIView)负责展示,控制器(UIViewController)负责逻辑,模型(Model)负责数据。
2. 应用入口与生命周期
(1)应用级入口(UIApplication)
UIApplication 是应用的「单例管家」,管理应用生命周期、事件分发、状态栏、URL 跳转等:
// 获取应用单例
UIApplication *app = [UIApplication sharedApplication];
// 设置状态栏样式(iOS 13+ 需在 Info.plist 配置 View controller-based status bar appearance = NO)
app.statusBarStyle = UIStatusBarStyleLightContent;
// 打开URL
[app openURL:[NSURL URLWithString:@"https://www.apple.com"] options:@{} completionHandler:nil];
(2)应用代理(UIApplicationDelegate / SceneDelegate)
-
iOS 12 及以下:通过
UIApplicationDelegate管理应用生命周期(全局唯一); -
iOS 13+:引入
UISceneDelegate管理「场景(Scene)」生命周期(支持多窗口),UIApplicationDelegate仅负责应用级初始化。
| 核心生命周期方法(UIApplicationDelegate) | 说明 |
|---|---|
application:didFinishLaunchingWithOptions: |
应用启动完成(初始化根控制器) |
applicationDidBecomeActive: |
应用进入前台(可交互) |
applicationWillResignActive: |
应用退至后台(如来电、下拉通知) |
applicationDidEnterBackground: |
应用完全后台(需保存数据) |
applicationWillEnterForeground: |
应用即将前台(恢复界面) |
applicationWillTerminate: |
应用即将退出(最后清理) |
(3)UIViewController 生命周期(核心)
控制器是「视图的管理者」,其生命周期决定了视图的创建/销毁,OC 核心方法如下:
@interface ViewController ()
@end
@implementation ViewController
// 1. 初始化(代码创建时调用)
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// 初始化数据、配置
}
return self;
}
// 2. 加载视图(视图首次创建,懒加载)
- (void)loadView {
// 手动创建根视图(若不用XIB/Storyboard)
self.view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.view.backgroundColor = [UIColor whiteColor];
}
// 3. 视图加载完成(初始化控件、布局)
- (void)viewDidLoad {
[super viewDidLoad];
// 核心初始化逻辑(仅执行一次)
}
// 4. 视图即将显示(每次显示前调用,如跳转返回)
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// 更新界面数据、刷新布局
}
// 5. 视图已显示(可执行动画、网络请求)
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
}
// 6. 视图即将隐藏
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// 暂停动画、移除监听
}
// 7. 视图已隐藏
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
}
// 8. 内存警告(释放非必要资源)
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
// 9. 视图销毁(控制器释放前)
- (void)dealloc {
// 移除通知、释放强引用(避免内存泄漏)
NSLog(@"控制器销毁");
}
@end
3. 响应者体系(UIResponder)
UIKit 所有可交互元素都继承自 UIResponder(响应者),构成「响应者链」处理事件(触摸、手势、键盘等):
-
核心响应者:
UIApplication→UIWindow→UIViewController→UIView→ 子视图; -
核心方法
// 触摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
// 成为第一响应者(如输入框唤起键盘)
- (BOOL)becomeFirstResponder;
// 放弃第一响应者(如输入框收起键盘)
- (BOOL)resignFirstResponder;
- 事件传递规则:
1. 系统通过 hitTest:withEvent: 从父视图到子视图查找「最顶层可交互视图」;
2. 找到后调用该视图的事件方法(如 touchesBegan:);
3. 若该视图不处理,事件沿响应者链向上传递(子视图→父视图→控制器→Window→Application)。
4. UIView 核心(视图基础)
UIView 是所有可视化元素的基类,负责展示、布局、事件接收,核心属性/方法:
| 核心属性 | 说明 |
|---|---|
frame |
相对于父视图的位置+尺寸(CGRect),决定视图在父视图中的显示区域 |
bounds |
自身坐标系的位置+尺寸(origin 默认 (0,0),修改会偏移子视图) |
center |
相对于父视图的中心点坐标(CGPoint) |
transform |
形变(缩放、旋转、平移,基于 center) |
backgroundColor |
背景色(UIColor) |
alpha |
透明度(0~1,0 完全透明,1 不透明) |
hidden |
是否隐藏(YES 隐藏,不参与布局/事件) |
clipsToBounds |
是否裁剪超出自身边界的子视图(YES 裁剪) |
layer |
底层 CALayer(负责渲染,UIView 是 CALayer 的封装) |
二、UIKit 核心组件(常用控件)
1. 基础交互控件
| 控件类 | 用途 | 核心 OC 示例 |
|---|---|---|
UILabel |
文本展示 | UILabel *label = [[UILabel alloc] init]; label.text = @"Hello UIKit"; label.font = [UIFont systemFontOfSize:16]; |
UIButton |
按钮(点击交互) | UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem]; [btn setTitle:@"点击" forState:UIControlStateNormal]; [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside]; |
UITextField |
单行文本输入 | UITextField *tf = [[UITextField alloc] init]; tf.placeholder = @"请输入内容"; tf.keyboardType = UIKeyboardTypeDefault; |
UITextView |
多行文本输入/展示 | UITextView *tv = [[UITextView alloc] init]; tv.text = @"多行文本"; tv.editable = YES; |
UIImageView |
图片展示 | UIImageView *iv = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"icon"]]; iv.contentMode = UIViewContentModeScaleAspectFit; |
UISwitch |
开关(开/关) | UISwitch *sw = [[UISwitch alloc] init]; sw.on = YES; [sw addTarget:self action:@selector(switchChange:) forControlEvents:UIControlEventValueChanged]; |
UISlider |
滑块(数值调节) | UISlider *slider = [[UISlider alloc] init]; slider.minimumValue = 0; slider.maximumValue = 100; slider.value = 50; |
UISegmentedControl |
分段选择器 | UISegmentedControl *seg = [[UISegmentedControl alloc] initWithItems:@[@"选项1", @"选项2"]]; seg.selectedSegmentIndex = 0; |
2. 列表/集合控件(高频)
(1)UITableView(列表)
核心是「数据源+代理」模式,支持单行列表展示,OC 核心实现:
@interface TableViewController () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSArray *dataArray;
@end
@implementation TableViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.dataArray = @[@"行1", @"行2", @"行3"];
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
self.tableView.dataSource = self;
self.tableView.delegate = self;
// 注册单元格(复用)
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cellID"];
[self.view addSubview:self.tableView];
}
// 数据源:行数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataArray.count;
}
// 数据源:单元格内容
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cellID" forIndexPath:indexPath];
cell.textLabel.text = self.dataArray[indexPath.row];
return cell;
}
// 代理:单元格点击
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(@"点击第%ld行", indexPath.row);
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
@end
(2)UICollectionView(集合视图)
支持网格、瀑布流等自定义布局,OC 核心实现:
@interface CollectionViewController () <UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, strong) UICollectionView *collectionView;
@end
@implementation CollectionViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 布局配置(流式布局)
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.itemSize = CGSizeMake(100, 100); // 单元格尺寸
layout.minimumInteritemSpacing = 10; // 列间距
layout.minimumLineSpacing = 10; // 行间距
self.collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];
self.collectionView.dataSource = self;
self.collectionView.delegate = self;
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cellID"];
[self.view addSubview:self.collectionView];
}
// 数据源:单元格数量
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return 12;
}
// 数据源:单元格内容
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellID" forIndexPath:indexPath];
cell.backgroundColor = [UIColor lightGrayColor];
return cell;
}
@end
3. 容器控件(页面导航/布局)
| 控件类 | 用途 | 核心 OC 示例 |
|---|---|---|
UIScrollView |
可滚动视图(基础) | UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; scrollView.contentSize = CGSizeMake(375, 1000); scrollView.showsVerticalScrollIndicator = YES; |
UINavigationController |
导航控制器(页面栈) | UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; nav.navigationBar.barTintColor = [UIColor blueColor]; |
UITabBarController |
标签栏控制器(底部切换) | UITabBarController *tabBarVC = [[UITabBarController alloc] init]; tabBarVC.viewControllers = @[nav1, nav2]; tabBarVC.tabBar.tintColor = [UIColor redColor]; |
UIPageViewController |
分页控制器(左右滑切换) | UIPageViewController *pageVC = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil]; |
4. 弹窗/提示控件
| 控件类 | 用途 | 核心 OC 示例 |
|---|---|---|
UIAlertController |
弹窗(警告/操作) | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:@"确定删除?" preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {}]]; [self presentViewController:alert animated:YES completion:nil]; |
UIActivityIndicatorView |
加载指示器 | UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithStyle:UIActivityIndicatorViewStyleLarge]; [indicator startAnimating]; |
UIRefreshControl |
下拉刷新 | UIRefreshControl *refresh = [[UIRefreshControl alloc] init]; [refresh addTarget:self action:@selector(refreshData:) forControlEvents:UIControlEventValueChanged]; self.tableView.refreshControl = refresh; |
三、布局体系(UIKit 核心能力)
1. 基础布局(Frame/Bounds/Center)
手动控制视图位置,适合简单布局:
UIView *boxView = [[UIView alloc] init];
boxView.frame = CGRectMake(20, 100, 100, 100); // x:20, y:100, 宽100, 高100
boxView.center = CGPointMake(187.5, 150); // 中心点(父视图宽375,水平居中)
boxView.bounds = CGRectMake(-10, -10, 100, 100); // 自身坐标系偏移,子视图会右移/下移10pt
[self.view addSubview:boxView];
2. 自动布局(Auto Layout)
通过「约束」定义视图关系,适配多屏幕,OC 原生实现:
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.translatesAutoresizingMaskIntoConstraints = NO; // 必须关闭自动掩码
[self.view addSubview:btn];
// 创建约束:按钮左/右间距20pt,顶部100pt,高度44pt
NSLayoutConstraint *leading = [NSLayoutConstraint constraintWithItem:btn attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0 constant:20];
NSLayoutConstraint *trailing = [NSLayoutConstraint constraintWithItem:btn attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:-20];
NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:btn attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:100];
NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:btn attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:44];
// 添加约束
[self.view addConstraints:@[leading, trailing, top]];
[btn addConstraint:height];
Masonry 封装(OC 主流)
#import "Masonry.h"
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self.view).offset(20);
make.trailing.equalTo(self.view).offset(-20);
make.top.equalTo(self.view).offset(100);
make.height.mas_equalTo(44);
}];
3. 尺寸适配(Size Classes + Trait Collection)
适配多设备/横竖屏,OC 实现:
// 监听尺寸类变化
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
// 宽屏(如iPad/手机横屏)
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular) {
[self.btn mas_updateConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(60);
}];
} else { // 窄屏(手机竖屏)
[self.btn mas_updateConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(44);
}];
}
[self.view layoutIfNeeded];
}
4. 安全区域(Safe Area)
适配刘海屏/底部横条,OC 实现:
// 约束适配安全区域
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self.view.safeAreaLayoutGuide).offset(20);
make.trailing.equalTo(self.view.safeAreaLayoutGuide).offset(-20);
make.top.equalTo(self.view.safeAreaLayoutGuide).offset(20);
}];
四、事件处理(交互核心)
1. 触摸事件(UITouch)
自定义视图触摸处理:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.view]; // 触摸点坐标
NSLog(@"触摸位置:%@", NSStringFromCGPoint(point));
}
2. 手势识别(UIGestureRecognizer)
OC 核心示例(点击/长按/滑动):
// 点击手势
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)];
tap.numberOfTapsRequired = 1; // 点击次数
[self.view addGestureRecognizer:tap];
// 长按手势
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGesture:)];
longPress.minimumPressDuration = 1.0; // 长按时长(秒)
[self.view addGestureRecognizer:longPress];
// 滑动手势
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeGesture:)];
swipe.direction = UISwipeGestureRecognizerDirectionRight; // 滑动方向
[self.view addGestureRecognizer:swipe];
3. 响应者链拦截(hitTest:withEvent:)
自定义视图可点击区域:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 若视图隐藏/透明/不可交互,不响应事件
if (self.hidden || self.alpha <= 0.01 || !self.userInteractionEnabled) {
return nil;
}
// 检查点是否在视图范围内
if (![self pointInside:point withEvent:event]) {
return nil;
}
// 优先返回子视图(倒序遍历,顶层视图优先)
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint subPoint = [subview convertPoint:point fromView:self];
UIView *hitView = [subview hitTest:subPoint withEvent:event];
if (hitView) {
return hitView;
}
}
return self;
}
五、渲染与动画
1. 视图渲染(CALayer + drawRect:)
(1)CALayer 基础(UIView 底层渲染)
// 给视图添加圆角(通过layer)
self.view.layer.cornerRadius = 10;
self.view.layer.masksToBounds = YES; // 裁剪圆角
self.view.layer.borderWidth = 1.0;
self.view.layer.borderColor = [UIColor grayColor].CGColor;
// 阴影(注意:masksToBounds=NO 才生效)
self.view.layer.shadowColor = [UIColor blackColor].CGColor;
self.view.layer.shadowOffset = CGSizeMake(2, 2);
self.view.layer.shadowOpacity = 0.5;
self.view.layer.shadowRadius = 4;
(2)自定义绘制(drawRect:)
- (void)drawRect:(CGRect)rect {
// 获取绘图上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 设置画笔颜色
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
// 设置线宽
CGContextSetLineWidth(ctx, 2.0);
// 绘制矩形
CGContextAddRect(ctx, CGRectMake(20, 20, 100, 100));
// 绘制路径
CGContextStrokePath(ctx);
}
2. UIView 动画(基础动画)
// 平移动画
[UIView animateWithDuration:0.3 animations:^{
self.btn.center = CGPointMake(self.btn.center.x + 100, self.btn.center.y);
} completion:^(BOOL finished) {
// 动画完成回调
}];
// 组合动画(缩放+旋转)
[UIView animateWithDuration:0.5 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
self.btn.transform = CGAffineTransformMakeScale(1.5, 1.5); // 缩放
self.btn.transform = CGAffineTransformRotate(self.btn.transform, M_PI_4); // 旋转45度
} completion:nil];
// 转场动画
[UIView transitionWithView:self.view duration:0.5 options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{
[self.view addSubview:self.newView];
} completion:nil];
3. Core Animation(核心动画,底层)
// 关键帧动画(路径动画)
CAKeyframeAnimation *keyFrame = [CAKeyframeAnimation animationWithKeyPath:@"position"];
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 20, 100);
CGPathAddLineToPoint(path, NULL, 355, 100);
CGPathAddLineToPoint(path, NULL, 355, 500);
CGPathAddLineToPoint(path, NULL, 20, 500);
keyFrame.path = path;
keyFrame.duration = 2.0;
[self.btn.layer addAnimation:keyFrame forKey:@"keyFrameAnimation"];
六、多态适配(暗黑模式/动态字体)
1. 暗黑模式(iOS 13+)
// 动态颜色(适配暗黑/浅色模式)
UIColor *dynamicColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
return [UIColor whiteColor]; // 暗黑模式
} else {
return [UIColor blackColor]; // 浅色模式
}
}];
self.view.backgroundColor = dynamicColor;
// 监听模式变化
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
// 更新颜色/图片
self.label.textColor = dynamicColor;
}
}
2. 动态字体(适配字体大小)
// 动态字体(跟随系统字体大小)
UIFont *dynamicFont = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
self.label.font = dynamicFont;
// 监听字体大小变化
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fontSizeChanged:) name:UIContentSizeCategoryDidChangeNotification object:nil];
七、性能优化
1. 列表优化(UITableView/UICollectionView)
-
复用单元格(
dequeueReusableCellWithIdentifier:); -
缓存单元格高度(避免重复计算);
-
异步加载图片(SDWebImage 等);
-
减少离屏渲染(避免圆角+阴影同时设置);
-
禁用不必要的动画(
cell.selectionStyle = UITableViewCellSelectionStyleNone)。
2. 渲染优化
-
避免频繁调用
setNeedsLayout/layoutIfNeeded; -
自定义绘制优先用
CALayer而非drawRect:; -
开启光栅化(
layer.shouldRasterize = YES,仅适合静态视图); -
减少透明视图(alpha < 1 会触发离屏渲染)。
3. 内存优化
-
避免循环引用(block 中用
weakSelf); -
图片压缩(
UIImageJPEGRepresentation/UIImagePNGRepresentation); -
及时释放强引用(
dealloc中移除通知/定时器); -
懒加载控件(避免一次性创建大量视图)。
八、调试工具
1. Xcode 内置工具
-
Debug View Hierarchy:可视化查看视图层级、约束、frame;
-
Instruments:
- Core Animation:检测离屏渲染、帧率;
- Time Profiler:检测卡顿;
- Allocations:检测内存泄漏;
-
控制台日志:打印约束冲突、视图信息(
NSLog(@"frame: %@", NSStringFromCGRect(self.view.frame)))。
2. 约束冲突排查
-
控制台日志中定位「Unable to simultaneously satisfy constraints」;
-
降低非核心约束优先级(
constraint.priority = UILayoutPriorityDefaultHigh); -
动态激活/禁用约束(
constraint.active = YES/NO)。
九、进阶特性
1. 自定义控件
继承 UIView/UIControl 实现自定义交互控件:
@interface CustomControl : UIControl
@property (nonatomic, assign) CGFloat progress;
@end
@implementation CustomControl
- (void)setProgress:(CGFloat)progress {
_progress = progress;
[self setNeedsDisplay]; // 触发重绘
}
- (void)drawRect:(CGRect)rect {
// 绘制进度条
CGRect progressRect = CGRectMake(0, 0, rect.size.width * self.progress, rect.size.height);
[[UIColor blueColor] setFill];
UIRectFill(progressRect);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
self.progress = point.x / self.bounds.size.width;
[self sendActionsForControlEvents:UIControlEventValueChanged]; // 触发值变化事件
}
@end
2. 文本排版(NSAttributedString)
NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:@"富文本示例"];
// 设置字体
[attStr addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:18] range:NSMakeRange(0, 3)];
// 设置颜色
[attStr addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 3)];
// 设置下划线
[attStr addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:NSMakeRange(3, 2)];
self.label.attributedText = attStr;
十、最佳实践
-
MVC 分层:控制器仅负责逻辑调度,视图仅负责展示,模型仅负责数据;
-
控件封装:将重复的控件逻辑封装为分类/子类(如
UIButton+Custom); -
兼容性处理:通过
@available适配不同 iOS 版本:
if (@available(iOS 13.0, *)) {
// iOS 13+ 逻辑
} else {
// 低版本逻辑
}
-
避免生命周期陷阱:
viewDidLoad仅初始化,viewWillAppear处理每次显示的逻辑; -
响应链优化:减少不必要的
userInteractionEnabled = NO,避免事件传递卡顿。
总结
UIKit 是 iOS 开发的「基石框架」,核心围绕「视图(UIView)- 控制器(UIViewController)- 事件(UIResponder)」展开,掌握「布局体系」「事件处理」「渲染优化」是关键。实际开发中,优先用 Masonry 简化 Auto Layout,结合 Size Classes/暗黑模式适配多场景,通过 Instruments 定位性能问题,可高效构建稳定、适配性强的 iOS 界面。