iOS 面经
核心考点
1. 内存管理 (ARC)
- ARC(Automatic Reference Counting):编译器自动插入 retain/release
- 强引用循环:两个对象互相强引用导致无法释放
- 解决:使用
weak(不增加引用计数,置 nil)或unowned(不置 nil,对象必须比引用者活得长) - 闭包捕获列表:
[weak self]或[unowned self]避免循环
2. RunLoop
- 事件循环机制,有任务则处理,无任务则休眠
- Mode:主线程 RunLoop 有 Default、UITrackingRunLoopMode、Common Modes
- 应用场景:Timer、常驻线程、滚动期间暂停图片加载
3. 多线程
| 方式 | 特点 |
|---|---|
| GCD | 轻量,推荐,任务+队列模型 |
| OperationQueue | 面向对象,支持依赖关系和取消 |
| Thread | 底层,需手动管理 |
- 串行队列:任务顺序执行;并发队列:任务并发执行
- 主队列(串行):UI 操作必须在主线程
- 死锁:主线程同步派发任务到主队列
4. Swift 特性
- Optional:显式处理 nil,
?可选链,!强解包,guard let/if let安全解包 - Protocol + Extension:面向协议编程,替代继承实现代码复用
- Value Types vs Reference Types:struct/enum 是值类型(栈),class 是引用类型(堆)
- Codable:自动序列化/反序列化 JSON
- async/await:Swift 5.5 引入,结构化并发
5. UIKit vs SwiftUI
| 对比 | UIKit | SwiftUI |
|---|---|---|
| 范式 | 命令式 | 声明式 |
| 最低支持 | iOS 2+ | iOS 13+ |
| 生态 | 成熟完整 | 快速发展 |
| 调试 | 较复杂 | Preview 实时预览 |
6. 高频面试题
- Category vs Extension:Category 运行时添加方法(不能加存储属性);Extension 编译期扩展(可加存储属性,仅限当前模块)
- KVO / KVC:键值观察/编码,底层通过 isa-swizzling 实现
- App 生命周期:Not Running → Inactive → Active → Background → Suspended
- Instruments 性能分析:Time Profiler(CPU)、Allocations(内存)、Leaks
- Autolayout 原理:Cassowary 约束求解算法
iOS 性能优化专题
iOS 性能优化全攻略
1. 渲染性能
离屏渲染
- 触发条件:圆角(masksToBounds)、阴影(shadow)、遮罩(mask)、光栅化
- 代价:GPU 需额外开辟缓冲区,切换上下文耗性能
- 优化:
- 圆角 → 用图片遮罩或 Core Graphics 画圆角背景
- 阴影 → 设置
shadowPath避免实时计算 - 静态内容 →
shouldRasterize = true(开启缓存,但内存增加)
卡顿检测
- CADisplayLink 监听帧率
- Instruments → Core Animation 查看 FPS 和离屏渲染
- RunLoop 观察者监测主线程超时(> 16ms 则记录堆栈)
2. 列表优化(UITableView / UICollectionView)
- Cell 复用:必须使用
dequeueReusableCell - 高度缓存:计算后缓存
heightForRow,避免重复计算 - 异步渲染:图片/富文本在子线程渲染,主线程只做展示(Texture/AsyncDisplayKit)
- 预计算:
prefetchDataSource在 cell 滚动到可视区前提前加载数据 - 图片下载:SDWebImage / Kingfisher 自带磁盘+内存缓存
3. 启动优化
冷启动阶段
pre-main 阶段 (dyld)
→ 加载动态库(减少动态库数量,合并/静态化)
→ Rebase / Binding(减少 ObjC 元数据:类/方法/分类)
→ Initializer(+load 方法,改用 +initialize 或 dispatch_once)
main() 之后
→ didFinishLaunching(延迟/异步初始化三方 SDK)
→ 首屏渲染
- 用
DYLD_PRINT_STATISTICS测量 pre-main 耗时 - 减少
+load方法(每个都在启动时执行) - 二进制重排(Clang 插桩 + 收集 order 文件)减少 Page Fault
4. 内存优化
- 图片内存:大图按需缩放,
UIImage(named:)会缓存,UIImage(contentsOfFile:)不缓存 - 循环引用排查:Instruments → Leaks / Allocations
- autoreleasepool:循环处理大量对象时手动加
@autoreleasepool {} - NSCache:系统内存紧张时自动清理,比 NSMutableDictionary 更合适
5. 网络优化
- HTTP/2:URLSession 默认支持,多路复用
- 请求合并:批量接口减少请求次数
- 图片格式:WebP / HEIF 替代 JPEG
- 离线缓存:URLCache + ETag / Last-Modified 协商缓存
- 压缩:Gzip / Brotli 压缩响应体
6. 包体积优化
- 资源优化:Asset Catalog 压缩,移除未使用资源(LSUnusedResources)
- 代码优化:
-dead_strip删除未引用代码 - 动态库转静态库:减少包体积和启动时间
- On-Demand Resources:按需下载资源,减少初始包大小
- App Thinning:Slicing(按设备分包)+ Bitcode
7. 高频面试题
- 界面卡顿的本质? 主线程 RunLoop 一次循环超过 16.7ms,帧率低于 60fps
- 为什么图片圆角会触发离屏渲染? 需要裁剪内容,必须在离屏缓冲区合成
- Instruments 如何定位内存泄漏? Leaks 工具 → 查看泄漏对象 → Allocation 追踪引用链
- 二进制重排原理? 通过收集启动期函数调用顺序写入 link map,让相关代码页相邻,减少 Page Fault
SwiftUI 深入面经
SwiftUI 核心知识
1. 数据流与状态管理
| 属性包装器 | 用途 | 作用域 |
|---|---|---|
@State | 视图私有状态 | 当前视图 |
@Binding | 双向绑定父视图状态 | 父→子 |
@StateObject | 创建并持有 ObservableObject | 当前视图拥有 |
@ObservedObject | 观察外部 ObservableObject | 外部传入 |
@EnvironmentObject | 环境中注入的共享对象 | 整棵子树 |
@Environment | 读取系统环境值(colorScheme 等) | 整棵子树 |
class UserStore: ObservableObject {
@Published var name = "Alice"
@Published var isLoggedIn = false
}
struct ContentView: View {
@StateObject private var store = UserStore()
var body: some View {
NavigationView {
ProfileView()
}
.environmentObject(store)
}
}
2. View 更新机制
- SwiftUI 通过比较
View的值(struct)来决定是否重新渲染 @State变化 → 视图重新body计算 → diff → 更新必要部分- EquatableView:手动实现
Equatable,跳过不必要的重计算 - identity:通过
id(...)或条件视图的位置确定视图标识
3. Swift Data & Core Data 集成
// Swift Data(iOS 17+)
@Model class User {
var name: String
var age: Int
init(name: String, age: Int) { ... }
}
struct UserList: View {
@Query var users: [User]
var body: some View {
List(users) { user in Text(user.name) }
}
}
4. 动画系统
// 隐式动画
withAnimation(.spring(response: 0.3)) {
isExpanded.toggle()
}
// 显式动画(精确控制哪个值动画)
Text("Hello")
.scaleEffect(scale)
.animation(.easeInOut, value: scale)
// matchedGeometryEffect(英雄动画)
@Namespace var hero
Image(...).matchedGeometryEffect(id: "avatar", in: hero)
5. 性能优化
- 避免大 body:拆分子视图,SwiftUI 可精准更新子树
- lazy 容器:
LazyVStack/LazyVGrid— 按需加载 - task / onAppear 加载数据:
.task {}自动绑定到视图生命周期 - Instruments → SwiftUI:查看 body 调用次数和渲染时间
6. UIKit 互操作
// SwiftUI 使用 UIKit 组件
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView { MKMapView() }
func updateUIView(_ view: MKMapView, context: Context) { ... }
}
// UIKit 使用 SwiftUI
let vc = UIHostingController(rootView: ProfileView())
7. 高频面试题
- @StateObject 和 @ObservedObject 的区别?
@StateObject:视图创建并拥有对象,视图刷新不重建@ObservedObject:外部传入,视图刷新可能重建(慎用)
- SwiftUI 如何做列表性能优化?
LazyVStack+ForEach配合id参数精准更新 - 如何处理复杂的视图层级性能? 拆分子视图、使用
EquatableView、避免频繁读取环境值 - NavigationStack vs NavigationView? NavigationStack(iOS 16+)支持编程式路由和深链接,更推荐
iOS 网络、安全与编译优化面经
iOS 网络与安全深度
1. URLSession 深入
三种任务类型
let session = URLSession(configuration: .default)
// DataTask:内存中传输(小数据)
let (data, response) = try await session.data(for: request)
// DownloadTask:内容写入文件(大文件下载)
let (fileURL, _) = try await session.download(for: request)
// UploadTask:上传文件
let (data, _) = try await session.upload(for: request, from: fileData)
// WebSocket
let task = session.webSocketTask(with: URL(string: "wss://echo.test")!)
task.resume()
try await task.send(.string("Hello"))
let message = try await task.receive()
URLSession 配置
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 300
config.requestCachePolicy = .returnCacheDataElseLoad
config.httpMaximumConnectionsPerHost = 6
config.waitsForConnectivity = true // 无网络时等待而非立即失败
let session = URLSession(configuration: config, delegate: myDelegate, delegateQueue: nil)
缓存策略
// URLCache 配置
URLCache.shared = URLCache(
memoryCapacity: 20 * 1024 * 1024, // 20MB 内存
diskCapacity: 100 * 1024 * 1024, // 100MB 磁盘
directory: nil
)
// 请求级缓存控制
var request = URLRequest(url: url)
request.cachePolicy = .returnCacheDataElseLoad // 有缓存就用缓存
2. App Transport Security (ATS)
<!-- Info.plist 常见配置 -->
<key>NSAppTransportSecurity</key>
<dict>
<!-- 允许特定域名 HTTP -->
<key>NSExceptionDomains</key>
<dict>
<key>api.example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>
3. 证书锁定(SSL Pinning)
// URLSession delegate 实现证书锁定
class PinningDelegate: NSObject, URLSessionDelegate {
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// 比较公鑰
let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0)!
let serverKey = SecCertificateCopyKey(serverCert)!
let serverKeyData = SecKeyCopyExternalRepresentation(serverKey, nil)! as Data
if serverKeyData == expectedPublicKey {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
4. 安全存储
// Keychain 存储敬感信息
func save(token: String, for account: String) throws {
let data = token.data(using: .utf8)!
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: account,
kSecValueData: data,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked // 解锁后才可访问
]
SecItemDelete(query as CFDictionary) // 先删除旧值
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.saveFailed }
}
func read(for account: String) throws -> String {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: account,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let data = result as? Data else { throw KeychainError.readFailed }
return String(data: data, encoding: .utf8)!
}
5. 安全威胁与防御
逆向工程防御
// 1. 判断是否运行在模拟器上
func isRunningOnSimulator() -> Bool {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}
// 2. 判断是否进行了 Jailbreak
func isJailbroken() -> Bool {
let paths = ["/Applications/Cydia.app", "/usr/sbin/sshd", "/etc/apt"]
return paths.contains { FileManager.default.fileExists(atPath: $0) }
}
// 3. 代码混淆:使用 Swift/C 加密关键逻辑
// 4. 字符串加密:避免明文存在于二进制
数据保护
// CryptoKit 加解密
import CryptoKit
func encrypt(data: Data, key: SymmetricKey) throws -> Data {
let sealedBox = try AES.GCM.seal(data, using: key)
return sealedBox.combined!
}
func decrypt(data: Data, key: SymmetricKey) throws -> Data {
let sealedBox = try AES.GCM.SealedBox(combined: data)
return try AES.GCM.open(sealedBox, using: key)
}
// 安全生成密鑰
let key = SymmetricKey(size: .bits256) // 256位 AES 密鑰
6. 编译优化技巧
// 1. 全模块优化(WMO)
// Build Settings -> Whole Module Optimization -> Yes
// 编译器看到全模块代码,可内联跨文件函数
// 2. @inline(__always) 强制内联
@inline(__always)
func criticalPath() -> Int { /* ... */ }
// 3. final 关键字避免动态分发
final class UserService { /* ... */ }
// 4. private/fileprivate 提供优化提示
// 局部小的方法更容易被内联
// 5. Existential 类型 vs Generic 类型
// 下面的写法会有装箱开销
// bad: func process(animal: any Animal) { ... }
// good: func process<T: Animal>(animal: T) { ... }
7. 资深面试题
- HTTPS 中证书屏蔽和证书锁定的区别?
- 证书屏蔽:浏览器内置的屏蔽已到期或不受信任证书的机制
- 证书锁定:应用主动将公鑰打入到应用中,与服务器公鑰对比
- 锁定更严格、主动防证书替换攻击
- Keychain 在 App 删除后是否也删除?
- 默认不删除,重新安装后仍可访问
- 要清理需要在 App 首次启动时主动判断和清除
- App 如何防止投屏时泏露敲感信息?
UITextField.isSecureTextEntry = true- App 进入后台时主动覆盖或模糊番幕
- 什么是 App Attest?
- Apple 提供的设备证明服务,验证请求来自真实未篡改的 App
- 防止服务器上的 API 被未授权的客户端调用
iOS Clean Architecture 与架构模式
iOS 架构设计深度
1. Clean Architecture 分层
Presentation Layer
- ViewController / SwiftUI View
- ViewModel (MVVM 里) / Presenter (MVP 里)
Domain Layer <-- 纯 Swift, 无任何框架依赖
- Use Case(一个 Use Case 对应一个业务操作)
- Entity(核心业务模型)
- Repository Protocol(数据访问抽象)
Data Layer
- Repository Implementation
- Data Source(Remote API + Local DB)
- Data Model Mapper
依赖规则:层之间的依赖只能向内,Domain 层对外无依赖。
2. MVVM 实现
// Domain Layer
struct User {
let id: String
let name: String
let email: String
}
protocol UserRepository {
func fetchUser(id: String) async throws -> User
}
final class FetchUserUseCase {
private let repo: UserRepository
init(repo: UserRepository) { self.repo = repo }
func execute(id: String) async throws -> User {
try await repo.fetchUser(id: id)
}
}
// Presentation Layer
@MainActor
final class UserViewModel: ObservableObject {
@Published private(set) var state: ViewState = .idle
private let useCase: FetchUserUseCase
init(useCase: FetchUserUseCase) { self.useCase = useCase }
func loadUser(id: String) async {
state = .loading
do {
let user = try await useCase.execute(id: id)
state = .success(user)
} catch {
state = .error(error.localizedDescription)
}
}
}
enum ViewState {
case idle, loading
case success(User)
case error(String)
}
3. 依赖注入
// 构建器注入而非全局单例
struct AppDependencies {
let userRepository: UserRepository
let authService: AuthService
static func makeProd() -> AppDependencies {
AppDependencies(
userRepository: RemoteUserRepository(apiClient: URLSessionAPIClient()),
authService: AuthServiceImpl()
)
}
static func makeTest() -> AppDependencies {
AppDependencies(
userRepository: MockUserRepository(),
authService: MockAuthService()
)
}
}
// Factory 模式创建 ViewModel
final class UserViewModelFactory {
private let deps: AppDependencies
init(deps: AppDependencies) { self.deps = deps }
func make(userId: String) -> UserViewModel {
UserViewModel(
useCase: FetchUserUseCase(repo: deps.userRepository),
userId: userId
)
}
}
4. 导航架构设计
// Coordinator 模式解耦导航逻辑
protocol Coordinator: AnyObject {
var navigationController: UINavigationController { get }
func start()
}
final class AppCoordinator: Coordinator {
let navigationController: UINavigationController
private let deps: AppDependencies
private var childCoordinators: [Coordinator] = []
init(nav: UINavigationController, deps: AppDependencies) {
self.navigationController = nav
self.deps = deps
}
func start() {
showHome()
}
private func showHome() {
let vc = HomeViewController(
viewModel: HomeViewModel(deps: deps),
onUserTapped: { [weak self] userId in
self?.showUserDetail(userId: userId)
}
)
navigationController.setViewControllers([vc], animated: false)
}
private func showUserDetail(userId: String) {
let factory = UserViewModelFactory(deps: deps)
let vc = UserDetailViewController(viewModel: factory.make(userId: userId))
navigationController.pushViewController(vc, animated: true)
}
}
5. 错误处理设计
// 领域错误类型
enum UserError: LocalizedError {
case notFound
case unauthorized
case networkError(underlying: Error)
var errorDescription: String? {
switch self {
case .notFound: return "用户不存在"
case .unauthorized: return "没有权限"
case .networkError(let e): return "网络错误: \(e.localizedDescription)"
}
}
}
// Repository 层映射错误
extension RemoteUserRepository {
private func mapError(_ error: Error) -> UserError {
guard let apiError = error as? APIError else {
return .networkError(underlying: error)
}
switch apiError.statusCode {
case 404: return .notFound
case 401: return .unauthorized
default: return .networkError(underlying: apiError)
}
}
}
6. 单元测试
// Domain 层单独测试,无需模拟框架
final class FetchUserUseCaseTests: XCTestCase {
var sut: FetchUserUseCase!
var mockRepo: MockUserRepository!
override func setUp() {
mockRepo = MockUserRepository()
sut = FetchUserUseCase(repo: mockRepo)
}
func test_execute_whenUserExists_returnsUser() async throws {
mockRepo.stubbedUser = expected
let result = try await sut.execute(id: "1")
XCTAssertEqual(result.id, expected.id)
}
func test_execute_whenUserNotFound_throwsError() async {
mockRepo.stubbedError = UserError.notFound
await XCTAssertThrowsErrorAsync(try await sut.execute(id: "999")) { error in
XCTAssertEqual(error as? UserError, .notFound)
}
}
}
7. 资深面试题
- MVVM 和 MVP 的核心区别?
- MVP: Presenter 知道 View,通过接口调用;不依赖 UI 框架,库测试更简单
- MVVM:ViewModel 不知道 View,通过数据绑定展示;更适合 SwiftUI
- Coordinator 模式解决什么问题?
- ViewController 之间不再互相了解,导航逻辑集中在 Coordinator
- 导航逻辑集中易于测试和复用
- 如何解决 ViewModel 测试中的异步问题?
- 使用
async/await+XCTestExpectation或XCTAssertThrowsErrorAsync - 将依赖的 Clock 抽象,测试时注入可控时间
- 使用
- Use Case 是否一定需要?
- 简单 CRUD 可以省略,直接调用 Repository
- 包含业务规则、多步骤或需单独测试的操作建议抽取