跳到主要内容

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

对比UIKitSwiftUI
范式命令式声明式
最低支持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 {
let expected = User(id: "1", name: "Alice", email: "a@b.com")
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 + XCTestExpectationXCTAssertThrowsErrorAsync
    • 将依赖的 Clock 抽象,测试时注入可控时间
  • Use Case 是否一定需要?
    • 简单 CRUD 可以省略,直接调用 Repository
    • 包含业务规则、多步骤或需单独测试的操作建议抽取