iOS面试八股文
本文摘要: Objective-C内存管理机制解析:MRC时代需手动管理对象生命周期,通过retain增加引用计数,release减少计数,autorelease延迟释放。ARC时代引入weak弱引用自动置nil机制,通过全局弱引用表实现。线程安全单例推荐使用dispatch_once,其通过原子操作和双重检查锁确保唯一性。RunLoop管理线程事件循环,可用于性能优化如卡顿监控和异步渲染。KVO
1:Objective-C 中的 MRC (Manual Reference Counting) 是如何工作的?详细说明 retain
, release
, autorelease
的作用和区别?
答案详解:
在MRC时代,开发者需手动管理对象内存生命周期。核心是引用计数。
-
retain
:-
作用:增加对象的引用计数 (+1)。
-
使用场景:当你需要一个对象在当前作用域之外继续存活时(如将其赋值给一个强指针属性、加入数组等)。
-
本质:
[obj retain]
向对象发送消息,使其retainCount++
。
-
-
release
:-
作用:减少对象的引用计数 (-1)。当计数减为0时,立即销毁对象并释放内存。
-
使用场景:当你不再需要持有该对象时(如局部变量超出作用域、属性被赋新值、从集合中移除等)。
-
注意:不要过度
release
,否则会导致EXC_BAD_ACCESS
崩溃。
-
-
autorelease
:-
作用:将对象加入当前
AutoreleasePool
,延迟release
。当pool drain时,会对池中所有对象发送一次release
。 -
使用场景:方法需要返回一个新创建的对象,且调用者不立即
retain
它时(如工厂方法[NSString stringWithFormat:]
)。 -
原理:
[obj autorelease]
不立即改变计数,而是将对象注册到pool。pool drain时调用[obj release]
。 -
与
release
区别:release
立即生效;autorelease
延迟到pool drain生效。
-
MRC黄金法则:
-
谁
retain
/alloc
/copy
/mutableCopy
,谁负责release
或autorelease
。 -
保持
retain
和release
次数平衡。
示例:
// MRC 示例
- (void)mrcExample {
// 1. alloc 创建,引用计数=1 (需负责release)
NSObject *obj1 = [[NSObject alloc] init];
// 2. retain,引用计数=2
[obj1 retain];
// 3. release,引用计数=1 (未销毁)
[obj1 release];
// 4. autorelease,加入pool,当前计数仍为1
NSObject *obj2 = [[[NSObject alloc] init] autorelease];
// 5. 方法结束,obj1 需release一次平衡alloc
[obj1 release];
// obj2 会在AutoreleasePool drain时被release
}
2:ARC 下 weak
关键字的作用是什么?它是如何实现自动置 nil 的?__unsafe_unretained
和 weak
有何区别?
答案详解:
-
weak
的作用:-
声明一个弱引用,不增加对象的引用计数。
-
当对象被销毁时,所有指向它的
weak
指针自动被置为nil
。 -
主要用途:解决循环引用问题(如delegate、block内部捕获self)。
-
-
自动置 nil 原理 (Runtime底层):
-
系统维护一个全局的弱引用表(
SideTables
,本质是散列表)。 -
当对象被创建时:
-
系统为其分配内存。
-
在弱引用表中创建对应的条目(以对象地址为Key)。
-
-
当
weak
指针指向对象时:-
Runtime 将该指针地址注册到弱引用表中该对象的条目下。
-
-
当对象
dealloc
时:- 调用
objc_destructInstance
。 - 从弱引用表中查找该对象的所有
weak
指针地址。 - 将所有指向该地址的指针设为
nil
。 - 从弱引用表中删除该条目。
- 调用
free
释放内存。
- 调用
-
-
__unsafe_unretained
vsweak
:-
__unsafe_unretained
:-
也是弱引用,不增加引用计数。
-
关键区别:对象销毁后,指针不会自动置nil,成为野指针。访问它会导致崩溃。
-
使用场景:性能极端敏感且能确保对象生命周期长于指针时(极少用)。
-
风险:极易产生悬垂指针,不安全。
-
-
weak
:-
对象销毁后自动置nil,安全。
-
轻微性能开销:需要操作弱引用表。
-
首选方案:绝大多数弱引用场景。
-
-
总结: 在ARC中,优先使用weak
。只在非常特殊场景(如兼容旧代码或性能瓶颈已证明)才考虑__unsafe_unretained
。
3:如何实现一个线程安全的单例?解释 dispatch_once
的原理?
答案详解:
线程安全单例实现 (Objective-C):
+ (instancetype)sharedInstance {
static MyClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[super allocWithZone:NULL] init]; // 或 [[self alloc] init]
});
return sharedInstance;
}
// 防止通过alloc/new创建新实例
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self sharedInstance];
}
// 防止copy产生新实例 (如果遵守NSCopying协议)
- (id)copyWithZone:(NSZone *)zone {
return self;
}
dispatch_once
原理:
-
目的:保证代码块在应用生命周期内仅执行一次,且线程安全。
-
核心机制:
-
底层依赖一个静态变量 (
onceToken
) 和一个底层原子操作/锁。 -
首次调用:
-
检查
onceToken
状态(通常为0)。 -
获取一个底层锁(如信号量或互斥锁),防止其他线程同时进入。
-
再次检查
onceToken
状态(双重检查锁,Double-Checked Locking)。 -
执行代码块。
-
将
onceToken
标记为已完成(设置为非0值)。 -
释放锁。
-
-
后续调用:
-
检查
onceToken
状态(已非0)。 -
直接跳过代码块,立即返回。
-
-
-
关键特性:
-
线程安全:锁机制确保多线程环境下只有第一个线程能执行代码块。
-
原子性:对
onceToken
的检查和设置是原子的。 -
高效:后续调用只有一次原子读操作,性能极高。
-
无饥饿:所有线程最终都能得到结果。
-
为什么是线程安全单例的最佳实践?
-
简洁、高效、安全。
-
编译器会保证
onceToken
和sharedInstance
的静态初始化是线程安全的。 -
避免了手动锁的复杂性和潜在错误。
(Swift实现):
class MySingleton {
static let shared = MySingleton() // let保证只分配一次,static保证延迟加载且线程安全
private init() {} // 防止外部创建实例
}
4:RunLoop 的主要作用是什么?它与线程的关系是怎样的?如何利用 RunLoop 进行性能优化?
答案详解:
-
RunLoop 的主要作用:
-
事件处理循环:管理线程接收和处理事件/消息(触摸、定时器、网络回调、PerformSelector等)。
-
线程保活:让线程在无事可做时休眠(节省CPU资源),有事件时唤醒工作。
-
调度任务:安排
Timer
、Source
、Observer
的执行。 -
避免线程结束:防止执行完任务的线程立刻退出。
-
-
RunLoop 与线程的关系:
-
一一对应:每个线程(包括主线程)都有且仅有一个专属的RunLoop对象(
NSRunLoop
/CFRunLoopRef
)。 -
主线程RunLoop:默认创建并启动,处理所有App事件(UI更新、交互)。
-
子线程RunLoop:
-
默认不创建(
[NSRunLoop currentRunLoop]
访问时按需创建)。 -
默认不启动(需显式调用
run
方法启动循环)。
-
-
存储方式:RunLoop以线程局部存储的方式保存,通过
[NSRunLoop currentRunLoop]
或CFRunLoopGetCurrent()
获取当前线程的RunLoop。
-
-
利用 RunLoop 进行性能优化:
-
主线程卡顿监控:
-
向主线程RunLoop添加一个
CFRunLoopObserver
。 -
监听
kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
活动。 -
计算这两个状态之间的耗时。如果超过阈值(如16ms或50ms),则判定为一次卡顿,记录堆栈信息用于分析。
-
-
异步渲染:
-
在子线程进行复杂的UI绘制(如Core Graphics绘图、文本布局计算)。
-
将绘制结果封装成图片。
-
在主线程RunLoop进入
kCFRunLoopBeforeWaiting
(即将休眠)时,将图片提交给主线程更新UI(通过CFRunLoopObserver
监听该时机)。这样能确保UI更新集中处理,减少CPU峰值。
-
-
任务批处理:
-
收集需要执行的非紧急任务。
-
在RunLoop进入
kCFRunLoopBeforeWaiting
或kCFRunLoopExit
时,一次性执行这些任务。减少唤醒次数,提高效率。
-
-
优化
NSTimer
:-
将耗时的定时器任务放到子线程RunLoop的特定Mode(如
NSDefaultRunLoopMode
)中运行,避免阻塞主线程。
-
-
常驻线程:
-
为特定后台任务(如网络、数据库)创建子线程。
-
在该线程的RunLoop中添加一个
Port
/Source
防止其退出。 -
将任务提交到这个线程执行。避免频繁创建销毁线程的开销。
-
-
5:KVO (Key-Value Observing) 的实现原理是什么?isa-swizzling
具体做了什么?
答案详解:
-
KVO 实现原理核心:
isa-swizzling
(类指针交换)-
目的:动态创建一个被观察对象所属类的派生类,并重写被观察属性的
setter
方法,插入通知逻辑。
-
-
详细过程:
-
Step 1: 注册观察者 (
addObserver:forKeyPath:options:context:
):-
检查类:检查对象的类 (
ClassA
) 是否已有基于KVO
的派生类(如NSKVONotifying_ClassA
)。如果没有,动态创建这个派生类。 -
isa-swizzling
:修改对象的isa
指针,使其指向新创建的派生类 (NSKVONotifying_ClassA
)。此时对象“变成”了这个派生类的实例。 -
重写
setter
:在派生类中重写被观察属性(如keyPath
)的setter
方法。新的setter
方法会:-
调用原来的实现 (
[super setKeyPath:newValue]
)。 -
触发通知:调用
willChangeValueForKey:
和didChangeValueForKey:
方法(或它们的变体),最终回调观察者的observeValueForKeyPath:ofObject:change:context:
。
-
-
重写
class
方法:在派生类中重写class
方法,使其返回原始类 (ClassA
)。这是为了隐藏派生类的存在,让外界调用[obj class]
时仍得到ClassA
。 -
重写
dealloc
:管理KVO相关资源的清理。
-
-
Step 2: 属性值改变:
-
当通过
setter
修改被观察属性时,由于对象的isa
指向派生类,实际调用的是派生类重写的setter
。 -
新
setter
调用原始实现修改值,然后触发通知。
-
-
Step 3: 通知观察者:
-
在
didChangeValueForKey:
方法中,会查找所有注册的观察者,并调用它们的observeValueForKeyPath:...
方法。
-
-
Step 4: 移除观察者 (
removeObserver:forKeyPath:context:
):-
将观察者信息从注册表中移除。
-
如果对象不再有任何观察者,系统可能会(非立即)将对象的
isa
指针指回原始类 (ClassA
),并尝试销毁派生类(但实际销毁时机由系统管理)。
-
-
-
isa-swizzling
做了什么?-
它修改了对象实例的
isa
指针,使其指向一个动态生成的、专门用于KVO的派生类。 -
这个操作在运行时完成,对开发者透明。
-
结果是:对象在运行时“改变”了它的类(不是编译时的类),从而能够响应重写的
setter
方法,实现属性变更的通知。
-
注意事项:
-
直接修改成员变量 (
Ivar
) 不会触发KVO,因为绕过了setter
方法。需手动调用willChange...
/didChange...
。 -
移除观察者:必须移除,否则观察者释放后可能导致野指针崩溃。
-
context
使用:建议使用唯一context
指针(如static void *myContext = &myContext;
)避免父/子类观察相同keyPath时的歧义。 -
性能:动态创建类、
isa-swizzling
有一定开销,避免过度使用或观察非常频繁变化的属性。
6.Runtime 是什么?Objective-C 的消息发送机制是如何工作的?请详细说明消息发送的完整流程(包括动态方法解析、备用接收者、消息转发)。
答案详解:
Runtime 是什么?
-
Objective-C 的动态运行时系统,由C和汇编编写的库
-
核心功能:
-
动态创建类/修改类结构
-
方法交换(Method Swizzling)
-
消息转发机制
-
关联对象(Associated Objects)
-
KVO/KVC 底层支持
-
消息发送完整流程:
1.消息发送阶段:
[receiver message];
// 编译后转换为:
objc_msgSend(receiver, @selector(message));
-
查找缓存:在 receiver 的类方法缓存中快速查找 IMP
-
方法列表遍历:若缓存未命中,遍历类的方法列表(包括分类方法)
-
继承链查找:沿父类链向上查找,直到 NSObject
-
未找到:进入消息转发机制
2.动态方法解析(第一次补救机会):
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(missingMethod)) {
// 动态添加方法实现
class_addMethod(self, sel, (IMP)dynamicIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
-
系统调用
resolveInstanceMethod:
或resolveClassMethod:
-
可在此动态添加方法实现(
class_addMethod
) -
返回 YES 会重启消息发送流程
3.备用接收者(第二次补救机会):
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(missingMethod)) {
return _backupReceiver; // 让其他对象处理
}
return nil;
}
-
系统调用
forwardingTargetForSelector:
-
可返回能处理该消息的其他对象
-
仅重定向消息,不能修改参数/返回值
4.完整消息转发(最后机会):
// 1. 返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(missingMethod)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return nil;
}
// 2. 处理转发
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([_backupReceiver respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_backupReceiver];
} else {
[super forwardInvocation:anInvocation]; // 崩溃
}
}
-
需要实现两个方法:
-
methodSignatureForSelector:
:返回有效的方法签名 -
forwardInvocation:
:处理实际的消息转发
-
-
可修改消息的接收者、参数、返回值
-
未处理最终调用
doesNotRecognizeSelector:
崩溃
7.Block 的本质是什么?Block 有哪几种类型?为什么 Block 需要避免循环引用?如何安全地避免?
答案详解:
Block 的本质:
-
编译后为结构体对象(含函数指针和捕获的变量)
-
底层结构示例:
struct __block_impl {
void *isa; // 指向Block类型
int Flags; // 标志位
int Reserved; // 保留字段
void *FuncPtr; // 函数指针
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
int capturedValue; // 捕获的外部变量
};
Block 的三种类型:
-
全局 Block(_NSConcreteGlobalBlock):
-
存储于全局区
-
未捕获任何局部变量
-
生命周期与程序相同
-
-
栈 Block(_NSConcreteStackBlock):
-
存储于栈内存
-
捕获了局部变量
-
函数返回后可能被销毁(危险!)
-
int local = 10; void (^__weak stackBlock)(void) = ^{ NSLog(@"%d", local); // 栈Block };
-
- 堆 Block(_NSConcreteMallocBlock):
- 存储于堆内存
- 栈Block调用
copy
后升级而来 - 由ARC管理生命周期
void (^heapBlock)(void) = [^{
NSLog(@"Safe!");
} copy];
循环引用与解决方案:
// 典型循环引用场景
self.block = ^{
[self doSomething]; // 强持有self
};
// 解决方案1:__weak + __strong
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf doSomething];
};
// 解决方案2:传递self为参数
self.block = ^(MyClass *obj) {
[obj doSomething];
};
self.block(self);
// 解决方案3:执行后手动置nil
__block id tmp = self;
self.block = ^{
[tmp doSomething];
tmp = nil; // 打破循环
};
8.GCD 中 dispatch_barrier_async
的作用是什么?请举例说明其使用场景。
答案详解:
核心作用:
-
在并发队列中创建同步点
-
确保栅栏前的任务全部完成后执行栅栏任务
-
栅栏任务单独执行完毕后再执行后续任务
使用场景:线程安全的字典读写
// 1. 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("com.dictionary.queue", DISPATCH_QUEUE_CONCURRENT);
// 2. 共享字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
// 3. 读操作(并发执行)
- (id)objectForKey:(NSString *)key {
__block id value;
dispatch_sync(queue, ^{
value = dict[key]; // 并发读
});
return value;
}
// 4. 写操作(栅栏保证独占)
- (void)setObject:(id)value forKey:(NSString *)key {
dispatch_barrier_async(queue, ^{
dict[key] = value; // 独占写
});
}
关键说明:
-
必须使用自定义并发队列(全局队列栅栏无效)
-
栅栏任务执行时队列暂停并发,变为串行
-
读写比大于100:1时性能显著优于锁
-
写操作使用
async
避免阻塞当前线程
9.NSOperation
相比 GCD 有哪些优势?如何实现 NSOperation
之间的依赖和取消?
答案详解:
NSOperation 核心优势:
特性 | GCD | NSOperation |
---|---|---|
任务依赖 | 需手动同步 | 内置依赖管理 |
任务取消 | 需自己实现取消逻辑 | 内置取消状态 |
任务优先级 | 仅队列优先级 | 操作级优先级(8个级别) |
状态监听 | 无 | KVO 监听 isFinished等属性 |
最大并发数 | 信号量模拟 | 直接设置 maxConcurrentCount |
依赖管理示例:
NSOperation *downloadOp = [NSBlockOperation blockOperationWithBlock:^{
// 下载数据
}];
NSOperation *parseOp = [NSBlockOperation blockOperationWithBlock:^{
// 解析数据
}];
NSOperation *saveOp = [NSBlockOperation blockOperationWithBlock:^{
// 保存数据
}];
// 设置依赖链
[parseOp addDependency:downloadOp];
[saveOp addDependency:parseOp];
// 添加到队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[downloadOp, parseOp, saveOp] waitUntilFinished:NO];
取消操作要点:
// 1. 自定义NSOperation需定期检查isCancelled
- (void)main {
while (!self.isCancelled && !self.isFinished) {
// 执行任务
if (self.isCancelled) break;
}
}
// 2. 取消单个操作
[parseOp cancel];
// 3. 取消队列所有操作
[queue cancelAllOperations];
// 4. 依赖的取消行为:
// - 取消A操作后,依赖A的B操作会立即开始执行
// - B操作需自行检查A是否被取消
10.ARC 在底层是如何管理对象生命周期的?AutoreleasePool 的作用是什么?它是如何工作的?
答案详解:
ARC 底层管理:
-
引用计数存储:
-
64位系统使用
SideTable
散列表存储 -
每个对象对应一个
SideTableEntry
-
struct SideTable { spinlock_t lock; RefcountMap refcnts; // 引用计数表 weak_table_t weak_table; // 弱引用表 };
-
-
内存管理调用:
-
objc_retain()
:引用计数+1 -
objc_release()
:引用计数-1,为0时调用dealloc
-
objc_autorelease()
:加入自动释放池
-
AutoreleasePool 工作原理:
-
数据结构:
-
由
AutoreleasePoolPage
组成的双向链表 -
每个Page大小4096字节(一页内存)
-
存储对象指针和POOL_BOUNDARY哨兵
-
-
工作流程:
@autoreleasepool { // Push:插入哨兵 id obj1 = [NSObject new]; // objc_autorelease(obj1) id obj2 = [NSObject new]; // objc_autorelease(obj2) } // Pop:释放哨兵之前所有对象
-
RunLoop 协同:
-
主线程RunLoop在事件循环前后自动创建/释放Pool
最佳实践:
// 场景:循环内创建大量临时对象
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
NSString *temp = [NSString stringWithFormat:@"index_%d", i];
// temp会在每次循环结束时释放
}
}
11.请详细说明 iOS 界面渲染流程。什么是离屏渲染?为什么要避免离屏渲染?如何避免?
答案详解:
iOS 界面渲染流程:
-
布局(Layout):计算视图层级中每个视图的 frame(
layoutSubviews
、setNeedsLayout
) -
显示(Display):绘制内容(
drawRect:
、setNeedsDisplay
,生成位图) -
准备(Prepare):Core Animation 准备动画数据(图片解码等)
-
提交(Commit):将图层层级打包并通过 IPC(进程间通信)发送到渲染服务(render server)
-
渲染:
-
Render Server:将图层数据转换为 OpenGL/Metal 指令
-
GPU:执行顶点着色、光栅化、片段处理等操作
-
显示:将渲染缓冲区内容显示到屏幕
-
垂直同步(V-Sync)与双缓冲机制:
-
iOS 使用双缓冲机制:前帧缓冲区(当前显示)和后帧缓冲区(GPU渲染)
-
V-Sync 信号确保在屏幕刷新时才交换缓冲区,避免屏幕撕裂
-
如果 GPU 未能在 V-Sync 信号前完成渲染,就会造成掉帧(卡顿)
离屏渲染(Off-Screen Rendering):
-
定义:GPU 无法直接在当前屏幕缓冲区绘制,需要额外创建离屏缓冲区进行处理
-
触发条件:
-
使用圆角(
cornerRadius
+masksToBounds = YES
) -
图层遮罩(
mask
) -
阴影(
shadow
) -
高斯模糊(
UIVisualEffectView
) -
组透明度(
allowsGroupOpacity = YES
+alpha < 1
)
-
-
为什么避免:
-
创建额外缓冲区消耗内存
-
上下文切换增加 GPU 负担(需合并多层内容)
-
可能导致帧率下降和功耗增加
-
避免离屏渲染的方案:
- 圆角优化:
// 避免:同时设置 cornerRadius 和 masksToBounds
view.layer.cornerRadius = 10
view.layer.masksToBounds = true // 触发离屏渲染
// 方案1:使用 CAShapeLayer 绘制圆角(不触发离屏)
let path = UIBezierPath(roundedRect: view.bounds,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: 10, height: 10))
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
view.layer.mask = shapeLayer
// 方案2:预渲染为图片(后台线程处理)
UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, UIScreen.main.scale)
let context = UIGraphicsGetCurrentContext()
view.layer.render(in: context)
let roundedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let imageView = UIImageView(image: roundedImage)
- 阴影优化:
// 避免:直接设置 shadow view.layer.shadowOpacity = 0.5 // 触发离屏 // 方案:指定 shadowPath view.layer.shadowPath = UIBezierPath(rect: view.bounds).cgPath
-
异步渲染:将复杂绘制放到后台线程,结果在主线程更新
12.iOS 事件传递机制是怎样的?如何实现点击穿透?point(inside:with:)
和 hitTest(_:with:)
的关系是什么?
答案详解:
事件传递流程:
-
系统接收事件:IOKit 捕获触摸事件,封装为 UIEvent 传递给 UIApplication
-
查找第一响应者(Hit-Testing):
-
从 keyWindow 开始,递归调用
hitTest(_:with:)
-
遍历顺序:后添加的视图优先(从后向前)
-
判断逻辑:
point(inside:with:)
→ 遍历子视图 → 返回最终响应视图
-
-
响应链传递:
-
如果第一响应者不处理,沿响应链向上传递(nextResponder)
-
顺序:View → ViewController → Window → Application
-
hitTest
与 point(inside:)
的关系:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 1. 检查是否可交互
if !isUserInteractionEnabled || isHidden || alpha <= 0.01 {
return nil
}
// 2. 检查点是否在范围内
if self.point(inside: point, with: event) {
// 3. 从后向前遍历子视图
for subview in subviews.reversed() {
let convertedPoint = subview.convert(point, from: self)
if let hitView = subview.hitTest(convertedPoint, with: event) {
return hitView
}
}
// 4. 无子视图处理,返回自身
return self
}
return nil
}
实现点击穿透的几种方式:
- 重写
point(inside:with:)
:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return false // 不响应事件,传递给下层视图
}
- 重写
hitTest(_:with:)
:override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let hitView = super.hitTest(point, with: event) return hitView == self ? nil : hitView // 自身不响应 }
-
使用
isUserInteractionEnabled
:设置为 false 阻止事件响应
13.如何监控和优化 iOS App 的性能?请说明卡顿、耗电和内存优化的具体方案。
答案详解:
卡顿监控与优化:
-
监控方案:
-
CADisplayLink:监测帧率,计算两次刷新间隔
-
主线程卡顿监控:通过子线程 ping 主线程,检测响应时间
class PerformanceMonitor { static let shared = PerformanceMonitor() private var pingThread: PingThread? func start() { pingThread = PingThread() pingThread?.start() } } class PingThread: Thread { override func main() { while !isCancelled { DispatchQueue.main.async { self.semaphore.signal() } // 等待50ms(3帧时间) if self.semaphore.wait(timeout: .now() + 0.05) == .timedOut { // 记录堆栈(主线程卡顿) } } } }
s
-
-
优化方案:
-
CPU 优化:
-
异步渲染(Core Graphics 绘制在后台线程)
-
视图层级简化(减少不必要的视图)
-
对象创建优化(复用池、懒加载)
-
-
GPU 优化:
-
避免离屏渲染
-
纹理优化(大小匹配、格式选择)
-
减少过度绘制
-
-
耗电优化:
-
CPU 优化:
-
减少不必要的计算(算法优化)
-
适时降低帧率(非交互场景)
-
-
网络优化:
-
批量请求(减少网络唤醒次数)
-
使用缓存(避免重复下载)
-
-
定位优化:
-
根据需要选择精度(
desiredAccuracy
) -
及时停止定位(
stopUpdatingLocation
)
-
-
后台任务优化:
-
使用后台任务标识(
beginBackgroundTask
) -
优化后台刷新频率
-
内存优化:
-
内存泄漏检测:
-
Instruments Leaks
-
MLeaksFinder(线上监控)
-
-
图片优化:
-
使用合适尺寸(避免大图缩略)
-
格式选择(HEIC/WebP > JPEG > PNG)
-
-
缓存管理:
-
NSCache
替代字典(自动释放) -
响应内存警告(
didReceiveMemoryWarning
)
-
-
** autoreleasepool**:循环内创建大量对象时使用
14.HTTPS 的握手过程是怎样的?如何防止中间人攻击?什么是证书锁定(SSL Pinning)?
答案详解:
HTTPS 握手过程(TLS 1.2):
-
Client Hello:客户端发送支持的加密套件、随机数
-
Server Hello:服务端选择加密套件、发送随机数和证书
-
验证证书:客户端验证证书有效性(颁发机构、有效期等)
-
密钥交换:客户端生成预主密钥,用服务器公钥加密发送
-
生成会话密钥:双方通过随机数和预主密钥生成对称密钥
-
加密通信:使用对称密钥进行高效加密通信
防止中间人攻击的方案:
-
证书校验(默认机制):
-
验证证书链(信任的CA签发)
-
验证域名匹配(CN/SAN)
-
验证有效期
-
-
证书锁定(SSL Pinning):
-
原理:客户端内置服务器公钥或证书,只接受特定证书
-
实现方式:
// NSURLSession 证书锁定示例 class PinningDelegate: NSObject, URLSessionDelegate { func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { guard let serverTrust = challenge.protectionSpace.serverTrust else { completionHandler(.cancelAuthenticationChallenge, nil) return } // 比较证书 let policy = SecPolicyCreateSSL(true, challenge.protectionSpace.host as CFString) SecTrustSetPolicies(serverTrust, policy) var error: CFError? if SecTrustEvaluateWithError(serverTrust, &error) { // 比较本地证书与服务器证书 if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverData = SecCertificateCopyData(serverCertificate) as Data let localData = loadLocalCertificateData() // 加载本地证书 if serverData == localData { completionHandler(.useCredential, URLCredential(trust: serverTrust)) return } } } completionHandler(.cancelAuthenticationChallenge, nil) } }
-
双向认证:客户端也提供证书,服务端验证客户端身份
-
证书锁定的优缺点:
-
优点:有效防止中间人攻击,即使系统CA被信任也无法破解
-
缺点:证书更新需要发版,灵活性降低
15.MVC、MVVM 和 VIPER 架构有什么区别?你在项目中如何选择架构?
答案详解:
架构对比:
方面 | MVC | MVVM | VIPER |
---|---|---|---|
职责分离 | 差(Controller臃肿) | 较好 | 优秀 |
可测试性 | 差 | 较好(VM可测试) | 优秀 |
代码量 | 少 | 中等 | 多 |
学习成本 | 低 | 中等 | 高 |
适用场景 | 简单页面 | 中等复杂度 | 大型复杂项目 |
MVC 问题(Massive ViewController):
-
Controller 承担太多职责:网络请求、数据处理、业务逻辑、视图更新
-
单元测试困难
MVVM 核心思想:
-
View:显示UI,绑定ViewModel属性
-
ViewModel:处理业务逻辑,提供数据绑定接口
-
Model:数据模型
// MVVM 示例 class UserViewModel { var userName: Observable<String> = Observable("") var avatarURL: Observable<URL?> = Observable(nil) func loadUser() { API.getUser { [weak self] user in self?.userName.value = user.name self?.avatarURL.value = URL(string: user.avatarUrl) } } } class UserViewController: UIViewController { var viewModel: UserViewModel! @IBOutlet weak var nameLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() bindViewModel() viewModel.loadUser() } private func bindViewModel() { viewModel.userName.bind { [weak self] name in self?.nameLabel.text = name } } }
VIPER 模块化架构:
-
View:显示界面,将用户输入传递给Presenter
-
Interactor:业务逻辑,数据获取
-
Presenter:协调View和Interactor,处理展示逻辑
-
Entity:数据模型
-
Router:界面跳转
架构选择原则:
-
项目规模:
-
小项目:MVC
-
中型项目:MVVM
-
大型项目:VIPER
-
-
团队经验:选择团队熟悉的架构
-
测试要求:高测试覆盖率选择MVVM或VIPER
-
维护性:长期维护项目选择更清晰的架构
16.Swift 中的 defer
关键字有什么作用?它的执行顺序是怎样的?请举例说明。
答案详解:
defer
关键字用于定义一段代码块,这个代码块会在当前作用域结束前被延迟执行,无论当前作用域是通过正常返回、抛出异常还是其他方式结束的。
执行顺序:
-
后进先出 (LIFO):在同一个作用域内,如果有多个
defer
语句,它们会被压入一个栈中。最后一个定义的defer
块会第一个执行,第一个定义的defer
块会最后一个执行。 -
作用域限制:
defer
的执行严格依赖于其所在的作用域(如函数、循环、条件语句等)。
示例与使用场景:
func fileOperation() {
let file = openFile("test.txt")
defer {
closeFile(file) // 这个defer会保证文件最终被关闭
print("File closed") // 这个也会在函数返回前执行
}
defer {
print("This runs first before the function exits") // 后定义的defer先执行
}
if someCondition {
return // 即使提前返回,defer也会执行
}
// ... 其他文件操作
print("Function end")
}
// 输出顺序:
// Function end
// This runs first before the function exits
// File closed
主要用途:
-
资源清理:确保文件句柄、数据库连接、网络连接等资源在使用后得到正确释放,类似于 Java 的
try-with-resources
或 C++ 的 RAII。 -
加锁解锁配对:保证锁能在作用域结束时解锁,避免死锁。
func synchronizedOperation() { lock.lock() defer { lock.unlock() // 确保在任何退出路径上都能解锁 } // 执行需要线程安全的操作 }
-
简化错误处理:在处理多个错误条件时,可以将共同的清理逻辑放在
defer
中,避免在每个错误分支重复编写清理代码。
注意事项: defer
中捕获的变量是当前值的引用。如果变量是值类型,defer
会捕获它执行那一刻的值(因为在 Swift 中,值类型在 defer
捕获时会发生拷贝)。如果变量是引用类型,defer
会捕获其引用,后续对引用所指向对象内容的修改会反映到 defer
块中。
17.Swift 为什么将 String
、Array
、Dictionary
等类型设计为值类型(Value Type)?这与 Objective-C 的对应类型有何本质区别?
答案详解:
设计为值类型的原因:
-
安全性(Safety):值类型通过拷贝(Copy) 来确保数据的独立性。当你将一个值类型的实例赋值给另一个变量,或将其传递给函数时,系统会创建该实例的一个副本。这意味着修改新变量不会影响原始数据,避免了意料之外的副作用(Unintended Side Effects)和共享状态引发的 Bug(如在多线程环境下同时修改同一个数组)。
var originalArray = [1, 2, 3] var copiedArray = originalArray // 发生拷贝,copiedArray是originalArray的一个独立副本 copiedArray.append(4) print(originalArray) // [1, 2, 3] print(copiedArray) // [1, 2, 3, 4] (修改副本不影响原始数据)
-
性能优化(Copy-on-Write, COW):为了避免每次赋值都进行昂贵的深层拷贝,Swift 的标准库为
String
,Array
,Dictionary
等值类型实现了 写时复制 优化。多个变量可以共享同一份底层数据缓冲区,直到其中某个变量需要修改(写入)这个数据时,才会真正发生拷贝。这使得值类型在多数只读场景下具有接近引用类型的性能。 -
可预测性(Predictability):值类型的行为更直观,更容易推理。你不需要担心某个地方的修改会“暗地里”影响程序其他部分的状态。
与 Objective-C 对应类型的本质区别:
特性 | Swift (值类型) | Objective-C (引用类型) |
---|---|---|
内存管理 | 通常存储在栈上(受COW优化影响) | 存储在堆上,依赖引用计数 (MRC/ARC) |
赋值/传递 | 拷贝内容(COW优化延迟了实际拷贝) | 传递指针(地址),多个引用指向同一实例 |
修改行为 | 修改副本不影响原始实例 | 通过任一引用修改,所有指向该实例的引用都会“看到”变化 |
相等性比较 | == 比较内容是否相等 |
isEqual: 比较内容,== 比较指针地址 |
-
Objective-C:
NSString
,NSArray
,NSDictionary
是引用类型(类)。它们分配在堆上,通过指针进行传递和赋值。多个变量可以指向同一个NSMutableArray
,修改它是危险的。NSMutableArray *original = [@[@1, @2, @3] mutableCopy]; NSMutableArray *copy = original; // 只是指针拷贝,指向同一个对象 [copy addObject:@4]; NSLog(@"%@", original); // (1, 2, 3, 4) 原始数组也被修改了!
-
Swift:
String
,Array
,Dictionary
是值类型(结构体)。赋值和参数传递默认是拷贝,但系统通过 COW 智能地管理内存,保证性能和安全的平衡。
选择建议:
-
需要独立、安全的数据副本时,优先使用 Swift 的值类型。
-
需要共享和动态修改状态,或与 Objective-C 代码交互时,可以使用引用类型(类),或者使用
NSArray
,NSDictionary
的 Swift 桥接类型。
18.Swift 中的 throws
和 rethrows
有什么区别?它们分别用于什么场景?
答案详解:
throws
:
-
含义:声明一个函数(或方法)本身可能会抛出错误。
-
用法:在函数参数列表后、返回箭头前使用
throws
关键字。函数体内可以使用throw
关键字抛出符合Error
协议的异常。 -
调用:调用
throws
函数时,必须使用try
、try?
或try!
来明确处理可能出现的错误。 -
场景:适用于任何可能失败的操作,如网络请求、文件读写、数据解析等。
enum NetworkError: Error { case invalidURL case requestFailed } func fetchData(from urlString: String) throws -> Data { guard let url = URL(string: urlString) else { throw NetworkError.invalidURL } // ... 模拟网络请求 if requestFailed { throw NetworkError.requestFailed } return Data() }
rethrows
:
-
含义:声明一个函数本身不会抛出错误,但如果它接收的函数参数抛出了错误,它会将这个错误重新抛出。
-
用法:通常用于高阶函数(接受函数作为参数的函数),例如
map
,filter
,forEach
等标准库函数。 -
限制:
rethrows
函数必须至少接受一个会抛出错误的函数参数(这个参数本身需要被标记为throws
)。 -
场景:用于编写接受可能抛出错误的闭包作为参数的通用函数。
// 一个简单的 rethrows 示例 func processArray<T>(_ array: [T], using processor: (T) throws -> Void) rethrows { for item in array { try processor(item) // 如果processor抛出,则重新抛出 } } let numbers = [1, 2, 3, 4, 5] // 使用不抛出错误的闭包 processArray(numbers) { print($0) } // 不需要try // 使用可能抛出错误的闭包 try processArray(numbers) { element in if element == 3 { throw SomeError() } print(element) }
关键区别:
特性 | throws |
rethrows |
---|---|---|
错误来源 | 函数自身操作可能抛出错误 | 错误来源于传入的函数参数(闭包) |
调用要求 | 调用时必须用 try 处理 |
只有当传入的闭包参数抛出错误时,调用才需要 try |
应用范围 | 任何可能失败的函数 | 主要用于接受闭包参数的高阶函数 |
灵活性 | 函数体内部直接 throw |
依赖传入的闭包行为 |
简单记:rethrows
是 throws
的一个子集。所有 rethrows
函数都可以用 throws
来写(但反过来不行),但使用 rethrows
能向调用者提供更精确的语义:只有当你的闭包会抛错时,才需要 try
。
19.在 Swift 中,associatedtype
的作用是什么?它在协议中如何工作?
答案详解:
associatedtype
的作用:associatedtype
(关联类型)用于在协议(Protocol)中定义一个占位符类型。它允许协议中的方法、属性或下标等声明使用这个占位类型,而无需指定其具体类型,直到遵循该协议的类型(如结构体、类)中来明确这个类型是什么。这本质上是为协议提供了泛型(Generics)能力。
工作原理与使用:
-
在协议中声明:
protocol Container { associatedtype Item // 声明一个关联类型 Item var count: Int { get } mutating func append(_ item: Item) // 使用关联类型作为参数类型 subscript(i: Int) -> Item { get } // 使用关联类型作为返回值类型 }
这里,
Item
是一个抽象类型,代表容器中元素的类型。 -
在遵循协议的类型中确定具体类型:遵循协议的类型可以通过多种方式来确定
Item
的具体类型:
1.类型推断(Type Inference):Swift 编译器通常能通过分析实现中的用法自动推断出关联类型。struct IntStack: Container { var items: [Int] = [] var count: Int { return items.count } // Swift 推断出 Item 为 Int mutating func append(_ item: Int) { items.append(item) } subscript(i: Int) -> Int { return items[i] } }
2.显式指定(使用
typealias
):也可以显式地指明关联类型。struct GenericStack<Element>: Container { typealias Item = Element // 显式指定关联类型Item为泛型参数Element var items: [Element] = [] // ... 其他实现,使用Element }
3.使用泛型参数:这是最常见的方式,让遵循协议的类型本身是泛型的,并将其泛型参数绑定到协议的关联类型。
struct Stack<Element>: Container { // 实现协议的方法和属性时,使用泛型参数Element // 编译器自动认定关联类型Item就是Element var items: [Element] = [] var count: Int { return items.count } mutating func append(_ item: Element) { items.append(item) } subscript(i: Int) -> Element { return items[i] } }
为什么使用 associatedtype
?
-
增强协议表达能力:使协议能够抽象地描述操作,而不关心操作的具体数据类型,大大提高了协议的灵活性和复用性。例如,
Sequence
和Collection
协议都大量使用关联类型来定义元素类型、索引类型等。 -
类型安全:与使用
Any
类型相比,关联类型在编译时就能确定具体类型,保证了类型安全。
约束关联类型:
可以使用 :
或 where
子句为关联类型添加约束(例如要求它遵循某些协议或继承自特定类)。
protocol ComparableContainer {
associatedtype Item: Comparable // 约束Item必须遵循Comparable协议
func sort() -> [Item]
}
20.请描述 Swift 中的访问控制级别(open
, public
, internal
, fileprivate
, private
),并说明它们之间的区别和使用场景。
答案详解:
Swift 提供了五个级别的访问控制,从最开放到最严格依次是:open
> public
> internal
> fileprivate
> private
。它们决定了实体(类、结构体、枚举、属性、方法等)可以被哪些源文件或模块访问。
访问级别 | 描述 | 使用场景 |
---|---|---|
open |
最高权限。允许在定义模块外被访问、继承和重写。 | 主要为框架或库的公共API设计,旨在让其他模块可以继承和定制。class 和 class member 专用。 |
public |
允许在定义模块外被访问。但在其他模块中,不能被继承或重写(除非在本模块内)。 | 用于框架或库的公共接口,希望其他模块能使用但不想被继承或修改的实现。 |
internal |
默认级别。实体可以在定义它们的模块内任意访问,但在模块外完全不可见。 | 绝大多数应用程序或框架的内部结构。因为模块内部通常需要紧密协作,但对外部隐藏实现细节。 |
fileprivate |
将实体的使用限制在其定义所在的源文件内。 | 当同一个文件中的多个类型或函数需要共享一些细节,但不想让文件外的代码看到时使用。 |
private |
最严格。将实体的使用限制在其封闭声明作用域(以及同一文件内的扩展)内。 | 隐藏实现细节,只允许在同一个类型(或扩展)内访问。常用于私有属性、工具方法等,避免外部误用。 |
核心区别与规则:
-
open
vspublic
:这是最关键的区别。open
只适用于类及其成员。-
open class
:可以在其他模块中被继承。 -
public class
:在其他模块中只能被使用,不能被继承。 -
open method
:可以在其他模块中被重写。 -
public method
:在其他模块中不能被重写。
-
-
模块(Module):一个独立的代码分发单元,如一个 Swift 包、一个框架(Framework),或一个应用程序(Application Target)。
internal
是模块内访问的默认级别。 -
子类重写:子类的访问级别不能低于其父类。重写可以使成员比父类版本更开放(例如,将
public
方法重写为open
),但不能更严格。 -
泛型与元组:泛型类型或函数的访问级别是其类型参数或函数参数访问级别的最小值。元组的访问级别是其所有元素类型访问级别的最小值。
-
默认级别:如果不显式指定,默认为
internal
。
使用原则:
-
最小权限原则:总是从最严格的
private
开始,只有在确实需要暴露更多访问权限时才逐步放宽。这有助于封装和减少耦合。 -
API 设计:精心设计你的
open
和public
API,因为它们构成了你模块的长期承诺(API Contract),后续修改成本很高。 -
测试:如果你需要从测试目标(Test Target)访问应用程序中的实体,可以将它们标记为
internal
(默认)并在导入应用程序模块时使用@testable import
。
21.Swift 的现代并发模型(Async/Await)相比传统的 GCD 和 Completion Handler 有哪些优势?什么是结构化并发(Structured Concurrency)?
答案详解:
传统方式的痛点(GCD & Completion Handlers):
-
回调地狱(Callback Hell):嵌套多个异步操作时,代码缩进严重,可读性差。
-
错误处理困难:需要在每个回调中手动检查错误,难以在外部统一捕获。
-
容易遗忘调用 Completion Handler:导致线程或任务永远挂起。
-
线程爆炸(Thread Explosion):手动管理
dispatch_async
可能创建过多线程,导致上下文切换开销大。 -
优先级反转(Priority Inversion):手动管理队列的优先级关系复杂,容易出错。
Async/Await 的优势:
-
线性书写,逻辑清晰:异步代码看起来和同步代码一样直观。
// 传统方式(回调地狱) func fetchData(completion: @escaping (Result<Data, Error>) -> Void) { requestToken { result in switch result { case .success(let token): requestData(with: token) { result in completion(result) } case .failure(let error): completion(.failure(error)) } } } // Async/Await 方式(线性逻辑) func fetchData() async throws -> Data { let token = try await requestToken() // 挂起,而非阻塞 let data = try await requestData(with: token) return data }
-
天然的错误传播:使用
try
和throw
即可处理异步错误,无需在回调中手动判断。 -
结构化并发:任务的生命周期与作用域绑定,避免了任务泄露(Task Leakage)。
-
高效的协程(Coroutine)模型:挂起(Suspend)而非阻塞(Block)线程,由运行时系统在适当线程恢复执行,极大提高了线程利用率,避免了线程爆炸。
-
内置的优先级管理和取消机制:任务继承父任务的优先级和取消状态,易于管理。
什么是结构化并发(Structured Concurrency)?
结构化并发是一种编程范式,它要求并发任务的生命周期必须被严格嵌套在一个定义好的作用域内。父任务必须等待所有子任务完成才能结束,并且可以方便地将取消信号传播给所有子任务。
在 Swift 中,主要通过 Task
和 TaskGroup
来实现:
func processImages(_ images: [UIImage]) async throws -> [ProcessedImage] {
// 所有子任务都必须在 withTaskGroup 或 withThrowingTaskGroup 的作用域内创建
try await withThrowingTaskGroup(of: ProcessedImage.self) { group in
for image in images {
// 添加子任务
group.addTask {
// 每个子任务都是一个独立的并发单元
return try await heavyProcessing(of: image)
}
}
// 收集结果。当作用域结束时,会隐式 await 所有子任务完成。
var results: [ProcessedImage] = []
for try await result in group {
results.append(result)
}
return results // 父任务返回前,保证所有子任务都已完成或抛出错误。
}
}
// 如果外部取消父任务,所有 group 中的子任务都会自动被取消。
优势:
-
安全:避免了“fire-and-forget”任务,防止任务在后台意外泄漏。
-
可控:提供了统一的取消和错误传播机制。
-
可推理:代码的执行流程和生命周期更加清晰。
22.在 Swift 和 Objective-C 混编的项目中,内存管理是如何协同工作的?什么是 unowned(unsafe)
?它与 weak
和 unowned(safe)
有何区别?
答案详解:
Swift/OC 混编内存管理:
Swift 的 ARC 和 Objective-C 的 ARC 在底层是兼容和协同的。Swift 编译器会自动桥接两种语言的对象引用计数管理。
-
Swift 类继承 NSObject:当一个 Swift 类继承自
NSObject
,它的实例在混编环境中就是一个完整的 Objective-C 对象,使用 Objective-C 的引用计数表。 -
纯 Swift 类:不继承
NSObject
的 Swift 类,使用 Swift 的引用计数机制(通常更高效)。但当它们暴露给 Objective-C 代码时(例如用@objc
修饰),编译器会为其创建一个透明的包装器(Thunk),使其能够被 Objective-C 运行时管理。 -
引用计数操作:无论对象原本是 Swift 还是 Objective-C 对象,当它们跨语言边界传递时,编译器会自动插入必要的
retain
/release
(或 Swift 的等效操作)调用,确保引用计数的正确性。
unowned(unsafe)
vs weak
vs unowned(safe)
:
这三种都是用于打破循环引用的无主(Unowned)或弱(Weak)引用。
特性 | weak |
unowned(safe) (默认) |
unowned(unsafe) |
---|---|---|---|
所有权 | 无主,不增加引用计数。 | 无主,不增加引用计数。 | 无主,不增加引用计数。 |
可选性 | 是,必须是可选类型 (T? )。 |
否,是非可选类型 (T )。 |
否,是非可选类型 (T )。 |
自动置 nil | 是。对象释放后,引用自动变为 nil 。 |
否。但访问已释放的对象会触发运行时错误(陷阱),提供清晰的错误信息。 | 否。访问已释放的对象是未定义行为(UB),通常导致野指针崩溃,难以调试。 |
性能 | 稍有开销(需要操作弱引用表)。 | 几乎无开销(只是一个普通指针)。 | 无任何开销(只是一个普通指针)。 |
安全性 | 最安全。 | 较安全。至少能提供错误信息。 | 极不安全。等同于 OC 的 __unsafe_unretained 。 |
使用场景 | 引用对象的生命周期可能短于当前对象。 | 引用对象的生命周期肯定长于或等于当前对象(例如,delegate 通常强持有对象,其生命周期更长)。 | 极罕见。仅在性能极端敏感,且你能绝对保证对象生命周期时使用(例如,某些底层 C 互操作)。 |
示例与选择:
class Parent {
// child 的生命周期被 parent 控制,parent 肯定比 child 活得久。
// 使用 unowned(safe) 是合适且高效的。
unowned let child: Child
init(child: Child) {
self.child = child
}
}
class NetworkHandler {
// 对 viewController 的引用,其生命周期不可控(可能被用户关闭)。
// 使用 weak 是安全的选择。
weak var viewController: UIViewController?
}
// 永远优先考虑 weak,除非你能像 Parent-Child 例子那样证明生命周期关系。
// 尽量避免使用 unowned(unsafe)。
23.Swift 的动态性体现在哪些方面?与 Objective-C 的动态性有何不同?什么情况下会用到 @dynamic
和 @dynamicCallable
?
答案详解:
Swift 的动态性(有限且安全):
Swift 默认是静态语言,但为了与 Objective-C 运行时交互和支持某些特定场景,它提供了有限的动态特性。
-
通过
@objc
暴露给 Objective-C 运行时:类、方法、属性可以用@objc
修饰,使其能被 Objective-C 的消息机制和 KVO/KVC 等动态特性所使用。 -
键路径(Key Path):
#keyPath()
提供了类型安全的键路径访问,在编译时检查有效性。 -
选择性方法交换:虽然不鼓励,但可以通过
class_getInstanceMethod
和method_exchangeImplementations
进行,通常仅限于调试。 -
反射(Mirror):
Mirror(reflecting:)
可以在运行时检查一个实例的内部结构(属性名、类型、值),但能力有限(只能读取,不能修改)。let mirror = Mirror(reflecting: someInstance) for child in mirror.children { print("Property: \(child.label ?? ""), Value: \(child.value)") }
与 Objective-C 动态性的区别:
特性 | Objective-C | Swift |
---|---|---|
默认行为 | 高度动态。方法调用是消息发送,在运行时解析。 | 高度静态。方法调用是静态派发或虚表派发,性能更高。 |
运行时修改 | 可以动态添加类、方法、协议(Runtime API)。 | 不能动态添加(除非通过 @objc 桥接到 OC Runtime)。 |
安全性 | 灵活但危险( unrecognized selector 崩溃)。 | 安全,但灵活性受限。 |
@dynamic
和 @dynamicCallable
:
-
@dynamic
:-
告诉 Swift 编译器,该成员的实现将由运行时环境提供,而不是在 Swift 中显式定义。
-
主要用途:
-
与 Objective-C 的兼容:
@dynamic
有时用于修饰使用 KVO 依赖键(Dependency Keys)的属性,表示其 getter/setter 会在运行时动态提供。 -
SwiftUI 中的数据流:
@Environment
,@FetchRequest
等属性包装器在底层使用了dynamic
特性,让 SwiftUI 框架来管理其存储和更新。
-
-
它本身不实现任何功能,只是一个标记,表示“不要管这个的实现”。
-
-
@dynamicCallable
:-
允许一个类型的实例被像函数一样调用(使用
instance(arg1, arg2...)
的语法)。 -
这是一个语法糖,而非真正的动态性。它要求类型实现
dynamicallyCall(withArguments:)
或dynamicallyCall(withKeywordArguments:)
方法。 -
使用场景:主要用于创建领域特定语言(DSL)或与其他动态语言(如 Python、JavaScript)桥接。
@dynamicCallable struct CallableStruct { func dynamicallyCall(withArguments args: [Int]) -> Int { return args.reduce(0, +) } } let addable = CallableStruct() let result = addable(1, 2, 3) // 编译后转换为 addable.dynamicallyCall(withArguments: [1, 2, 3]) print(result) // 6
-
24.请描述 Mach-O 文件格式的组成部分(如 Header、Load Commands、Data)及其作用。dyld 的加载过程是怎样的?
答案详解:
Mach-O (Mach Object) 文件格式:
它是 macOS 和 iOS 系统上可执行文件、对象代码、动态库和核心转储的标准文件格式。主要由三部分组成:
-
Header:
-
包含文件的元信息,如魔数(标识是 32 位还是 64 位)、CPU 架构类型(如 arm64)、文件类型(如可执行文件、动态库)、加载命令(Load Commands)的数量和大小。
-
使用
otool -h <binary>
可以查看头部信息。
-
-
Load Commands:
-
这是一张指令表,告诉内核和动态链接器(dyld)如何设置和加载二进制文件。
-
它描述了文件中的数据在虚拟内存中的布局,包括:
-
LC_SEGMENT_64
:定义了一个段(如__TEXT
,__DATA
)应被映射到内存的哪个位置。 -
LC_LOAD_DYLIB
:指明了该文件所依赖的动态库(如UIKit.framework
)。 -
LC_MAIN
:指明了程序的入口点(main 函数的偏移地址)。 -
LC_CODE_SIGNATURE
:代码签名的位置信息。
-
-
使用
otool -l <binary>
可以查看所有的加载命令。
-
-
Data:
-
包含了 Load Commands 所指示的实际数据,被组织在不同的段(Segment)和节(Section)中。
-
常见的段:
-
__TEXT
:只读的代码和常量。包含__text
(机器码)、__cstring
(字符串常量)、__const
等。 -
__DATA
:可读可写的数据。包含__data
(已初始化的全局变量)、__bss
(未初始化的静态变量)、__objc_*
(Objective-C 的元数据)等。 -
__LINKEDIT
:包含供动态链接器使用的原始数据,如符号表、字符串表、重定位项、代码签名信息。
-
-
dyld (Dynamic Link Editor) 的加载过程:
dyld 负责将应用程序及其所有依赖的动态库加载到内存中,并完成链接过程。从 iOS 13 开始,主要采用启动闭包(Launch Closures) 优化启动速度,但其逻辑依然可以概括为以下步骤:
-
加载主程序 Mach-O:解析主程序的 Header 和 Load Commands。
-
加载依赖库:根据 Load Commands 中的
LC_LOAD_DYLIB
,递归地加载所有依赖的动态库,形成一个依赖图。 -
Rebase & Bind:
-
Rebase:由于 ASLR(地址空间布局随机化),动态库每次加载的起始地址(slide)都不同。Rebase 负责修正内部的指针(指向当前二进制镜像内部的地址),给它们加上一个 slide 偏移量。
-
Bind:处理外部符号的引用(如调用
printf
函数)。将这些符号的地址绑定到动态库中的真实地址上。
-
-
运行初始化器:
-
执行所有
__mod_init_func
节中的函数,包括 C++ 的静态构造器、+load
方法(在 Objective-C 中,但现在不鼓励使用)。 -
在 Swift 中,会运行所有全局对象的初始化代码。
-
-
查找并调用 main():dyld 完成后,跳转到
LC_MAIN
指定的地址,即程序的main
函数,进入应用程序的正式生命周期。
优化:启动闭包(Launch Closure)将第 2-4 步的结果(依赖关系、Rebase/Bind 信息等)序列化缓存起来,下次启动时直接使用,避免了耗时的解析过程,极大加快了启动速度。
25.什么是响应式编程(Reactive Programming)?比较 Combine 和 RxSwift 的异同和优缺点。
答案详解:
响应式编程(Reactive Programming):
它是一种面向数据流和变化传播的编程范式。其核心思想是:当数据发生变化时,依赖于它的逻辑会自动更新。在 iOS 中,它通常用异步事件流(如网络响应、用户输入、定时器、属性变化)来进行编程,并使用声明式的语法来组合这些流。
核心概念:
-
发布者 (Publisher) / 可观察序列 (Observable):事件的产生源。
-
订阅者 (Subscriber) / 观察者 (Observer):事件的接收和处理方。
-
操作符 (Operator):用于在事件流上进行转换、过滤、组合等操作(如
map
,filter
,merge
,flatMap
)。
Combine vs RxSwift:
方面 | Combine (Apple 官方) | RxSwift (社区) |
---|---|---|
来源与支持 | Apple 官方框架,与 Swift 系统层级集成。 | 社区驱动的开源项目,是 ReactiveX 在 Swift 上的实现。 |
平台要求 | iOS 13+ / macOS 10.15+。对旧版本系统支持是硬伤。 | iOS 8.0+。支持范围更广。 |
API 设计 | 更贴近 Swift 的命名和设计风格(Publisher , Subscriber )。与 SwiftUI 深度绑定(@Published )。 |
沿用 ReactiveX 的统一 API(Observable , Observer ),熟悉其他语言版本(RxJava, RxJS)的开发者更容易上手。 |
性能与集成 | 与系统框架(如 Foundation, SwiftUI)有更深层次的优化和集成(如 URLSession 直接提供 dataTaskPublisher )。 |
性能优秀,但毕竟是第三方库,与系统新特性的集成会有延迟。 |
生态与学习 | 较新,社区资源和第三方库支持还在发展中。文档是苹果风格。 | 生态极其丰富,拥有大量现成的扩展(RxCocoa, RxDataSources)和社区资源,学习资料多。 |
后台调度 | 使用 Scheduler 协议(如 DispatchQueue , RunLoop )。 |
使用 SchedulerType 协议,概念类似。 |
错误处理 | 紧密集成 Swift 的 Error 类型,Publisher 的 Failure 类型是类型系统的一部分。 |
同样集成 Error ,但 Observable 通常不发出错误(Driver , Signal 等特质用于永不错误的流)。 |
如何选择?
-
新项目,且最低版本 >= iOS 13:无脑选择 Combine。它是未来,与 SwiftUI 是天作之合,能获得最好的性能和官方支持。
-
需要支持 iOS 13 以下的现有项目:继续使用 RxSwift。它是经过实战检验的、功能丰富的成熟方案。
-
大型项目或团队有 ReactiveX 经验:RxSwift 的统一 API 可能更有优势。
-
简单需求:Combine 可能更轻量(无需引入大型第三方依赖)。
Combine 简单示例(SwiftUI 中):
import Combine
import SwiftUI
class ViewModel: ObservableObject {
@Published var searchText: String = ""
@Published var results: [String] = []
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main) // 防抖
.removeDuplicates() // 去重
.flatMap { query -> AnyPublisher<[String], Never> in
// 将用户输入转换为网络请求的发布者
return APIClient.fetchResults(for: query)
.catch { _ in Just([]) } // 错误处理
.eraseToAnyPublisher()
}
.assign(to: \.results, on: self) // 将结果赋值给属性
.store(in: &cancellables) // 管理生命周期
}
}
26.请解释 Swift 中的 Actor 是如何解决数据竞争(Data Race)的?它与 Serial DispatchQueue 有何异同?MainActor 有什么特殊作用?
答案详解:
数据竞争(Data Race) 是指当多个线程并发地访问同一块内存数据,且至少有一个是写操作时,导致未定义行为。这是并发编程中最常见且最难调试的问题之一。
Actor 的解决方案:
Swift 的 Actor 是一种同步机制,它通过数据隔离(Data Isolation) 来保证状态安全。其核心规则是:Actor 内部的可变状态(存储属性)只能由 Actor 自身(通过其方法)来修改。
-
状态隔离:Actor 将其状态封装起来,外部代码不能直接访问其属性。
-
异步访问:要读取或修改 Actor 内部的状态,外部调用者必须使用
await
来异步地“跳入” Actor 的执行环境。这可能会挂起调用方线程。 -
内部同步:Actor 内部有一个隐式的任务执行器(Executor)。它负责以串行方式处理所有“跳入”的异步调用。这意味着,即使有多个调用方同时请求,Actor 也会一次只处理一个请求,从而彻底避免了数据竞争。
actor BankAccount { private var balance: Double = 0.0 // 状态被隔离保护 func deposit(_ amount: Double) { balance += amount // 只能在 actor 内部直接修改 } func withdraw(_ amount: Double) -> Bool { if balance >= amount { balance -= amount return true } return false } // 读取也需要 await,因为可能涉及内部同步 func getBalance() -> Double { return balance } } // 外部使用 Task { let account = BankAccount() await account.deposit(100) // 异步“跳入” actor let currentBalance = await account.getBalance() // 可能需要等待其他任务完成 print(currentBalance) }
Actor vs. Serial DispatchQueue:
特性 | Actor | Serial DispatchQueue |
---|---|---|
机制 | 语言级别的原语,编译器强制检查。 | 库级别的解决方案,依赖开发者自觉使用。 |
安全性 | 编译时保证(Compiler-checked)。如果你尝试从外部直接访问 account.balance ,编译器会报错。 |
运行时保证。容易误用,例如在队列外错误地访问状态。 |
性能 | 更高效。挂起是协作式的,由 Swift 并发运行时管理,开销小。 | 基于阻塞线程的锁/信号量,上下文切换开销更大。 |
死锁 | 不易死锁。Actor 内部是可重入的,await 挂起时允许处理新请求。 |
容易死锁。在队列中同步派发(sync )到同一个队列会导致死锁。 |
优先级 | 支持优先级继承(Priority Inheritance),高优先级任务不会被低优先级任务长时间阻塞。 | 队列本身无优先级概念,依赖目标队列的优先级,管理复杂。 |
MainActor:
-
是什么:一个全局唯一的特殊 Actor,其执行器与主队列(Main Dispatch Queue) 绑定。
-
作用:将代码执行切换到主线程。它是 Swift 并发中执行 UI 更新的推荐方式。
-
用法:
-
标记声明:用
@MainActor
标记一个类、方法或属性,确保其所有相关操作都在主线程执行。 -
显式调用:使用
await MainActor.run { ... }
在任意上下文中将一段代码块派发到主线程。@MainActor // 保证这个 ViewModel 的更新都在主线程 class MyViewModel: ObservableObject { @Published var text: String = "" // UI 绑定的属性必须主线程更新 func fetchData() async { let data = await networkRequest() // 在后台线程执行 // 自动切回主线程更新 UI self.text = "Data received: \(data)" } } // 或者在任意地方: Task { let result = await heavyComputation() await MainActor.run { // 显式切换到主线程 self.label.text = result } }
-
总结:Actor 是 Swift 并发模型提供的类型安全、高性能、现代化的共享状态管理工具,是替代 Serial DispatchQueue
进行同步的首选方案。MainActor
则是专门为 UI 操作设计的语法糖。
27.什么是内存碎片化?在 iOS 中,它主要由什么引起?有哪些手段可以预防或缓解?
答案详解:
内存碎片化 是指虽然总的空闲内存足够,但由于这些空闲内存被分散成许多不连续的小块,导致无法分配一块较大的连续内存区域。它分为:
-
外部碎片:空闲内存块分散在已分配内存块之间。
-
内部碎片:分配器分配的内存块略大于请求的大小,导致块内部分内存被浪费。
iOS 中的主要诱因:
-
大量小对象的频繁分配和释放:这是最常见的原因。例如,在循环中创建大量的临时字符串、字典、数组或自定义对象,尤其是大小不一的对象,会迅速加剧堆的内存碎片化。
-
不均匀的对象生命周期:长时间存活的大对象(“长命对象”)周围散布着被频繁分配和释放的小对象空间。当长命对象最终被释放时,它留下的“空洞”可能因为周围都是小对象而难以被合并利用。
预防与缓解手段:
-
对象复用(Object Pooling):
-
原理:创建可复用对象的池子,从池中获取对象,用完后归还,而不是直接释放。
-
适用场景:频繁创建销毁的、成本较高的对象,如
UITableViewCell
、CALayer
、网络连接等。 -
实现:
UITableView
/UICollectionView
的 cell 复用机制就是最经典的例子。对于自定义对象,可以自己实现一个复用池。
-
-
使用更少、更大的对象替代大量小对象:
-
重新审视数据模型。例如,用一个包含数组的结构体来代替大量分散的小对象。
-
使用值类型(Struct)可能有助于减少堆分配,但需注意值类型内包含引用类型时的情况。
-
-
避免不均衡的分配模式:
-
如果业务逻辑允许,尽量让分配的对象大小保持在一定规模,而不是忽大忽小。
-
-
使用合适的分配器(Allocator):
-
大多数开发者不直接控制,但系统层的分配器(如
malloc
)会使用多种策略(如不同尺寸的 size class)来减少碎片。iOS 使用的是高度优化的malloc_zone_t
。
-
-
监控与检测:
-
Instruments - Allocations:关注“All Heap & Anonymous VM”和“Dirty Size”。如果“Dirty Size”持续增长而活跃对象数稳定,可能暗示碎片化。
-
Instruments - Virtual Memory:查看“Fragmentation”指标。
-
注意观察是否频繁出现 “高内存使用率却分配失败” 的日志或现象,这可能是碎片化的直接表现。
-
高级技巧(谨慎使用):
-
对于极其敏感的场景,可以考虑使用自定义内存分配器或在一块预先分配的大内存(内存池)中进行对象分配,完全避开系统的堆分配。但这带来了极大的复杂性和维护成本。
28.Method Swizzling 的原理是什么?它有哪些潜在的风险?在现代 Swift 开发中还有哪些替代方案?
答案详解:
原理:
Method Swizzling 是 Objective-C Runtime 提供的一种在运行时动态交换两个方法实现(IMP) 的技术。它的核心是修改类的方法列表(method_list) 中方法名(SEL)与方法实现(IMP)的映射关系。
步骤(以交换 viewDidLoad
为例):
-
获取方法:使用
class_getInstanceMethod
获取原始方法 (originalMethod
) 和要交换的方法 (swizzledMethod
)。 -
尝试添加:尝试将
swizzledMethod
的实现添加到原始类中。如果成功,说明原始类没有这个方法,现在有了。 -
交换实现:使用
method_exchangeImplementations
函数交换两个方法的 IMP。+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(viewDidLoad); SEL swizzledSelector = @selector(my_viewDidLoad); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); // 尝试给原始类添加新方法,如果成功,说明原始方法不存在于本类(可能在于父类) BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { // 添加成功,说明原始方法在父类。现在将新方法的 SEL 指向原始方法的 IMP。 class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { // 添加失败,说明原始方法存在于本类,直接交换。 method_exchangeImplementations(originalMethod, swizzledMethod); } }); }
潜在风险:
-
难以维护和调试:代码行为变得“神奇”,破坏了可预测性,后续开发者难以理解。
-
命名冲突:你的
my_viewDidLoad
可能和其他第三方库的 swizzling 方法冲突。 -
破坏封装性:依赖于类的内部实现细节,一旦类内部实现改变,Swizzling 可能失效甚至引发崩溃。
-
线程安全:必须在
+load
中配合dispatch_once
确保只交换一次,否则后果严重。 -
不适用于 Swift 纯类:仅对继承自
NSObject
的类或使用@objc dynamic
暴露给 Runtime 的 Swift 方法有效。
现代替代方案:
-
子类化(Subclassing):最经典、最安全的方法。通过继承并重写方法来改变行为。
-
组合(Composition):使用代理(Delegate)、装饰器(Decorator)模式。持有原对象,在其方法调用前后添加自定义行为。
class SafeViewControllerDecorator { private let wrapped: UIViewController init(_ viewController: UIViewController) { self.wrapped = viewController } func viewDidLoad() { // 自定义逻辑 print("Before viewDidLoad") wrapped.viewDidLoad() // 调用原方法 // 自定义逻辑 print("After viewDidLoad") } }
-
依赖注入(Dependency Injection):通过外部传入配置或行为对象,而不是在内部硬编码或Hook。
-
Swift 协议扩展(Protocol Extension):为协议提供默认实现,允许类型选择性地重写。
-
Aspects 等更高级的库:一些库提供了比手动 Swizzling 更安全和声明式的 API,但它们底层依然基于 Swizzling。
结论:在现代开发中,应极其谨慎地使用 Method Swizzling,仅将其作为最后的手段,并且仅限于调试、日志记录等非核心业务逻辑。优先考虑子类化、组合等设计模式。
29.解释一下 dyld 3 相比 dyld 2 的改进。什么是启动闭包(Launch Closure)?它是如何优化 App 启动速度的?
答案详解:
dyld 2 vs dyld 3:
-
dyld 2:iOS 12 及之前使用的动态链接器。它的工作大部分在每次应用启动时在设备上即时完成,包括解析 Mach-O 文件、查找依赖、符号查找、重定位、绑定等。这是一个计算密集型的过程。
-
dyld 3:从 iOS 13 开始成为系统默认。它将动态链接过程分为预计算和运行时两个阶段,核心思想是尽可能多地将工作提前做完。
启动闭包(Launch Closure)及其优化原理:
启动闭包是 dyld 3 的核心创新。它是一个预计算好的缓存文件,包含了应用启动所需的所有“代码签名(Code Signing)”和“链接(Linking)”信息。
dyld 3 的工作流程:
-
预计算阶段(Precomputation):
-
这个阶段发生一次,通常是在 App 首次启动、更新后首次启动或系统更新后。
-
dyld 3 在后台进程中执行原来 dyld 2 在启动时做的大部分工作:分析 Mach-O 主二进制文件和所有依赖的动态库,处理所有符号绑定(Bind)和重定位(Rebase),验证代码签名。
-
将这些结果序列化成一个启动闭包(Launch Closure) 文件,存储在当前 App 的
tmp/com.apple.dyld
目录下。这个文件是经过代码签名的,保证了其内容的安全性。
-
-
运行时阶段(Runtime):
-
在 App 后续的每一次启动时,dyld 3 的工作变得非常简单:
a. 检查是否存在可用的、有效的启动闭包。
b. 如果存在,直接映射这个启动闭包到内存中。因为它包含了所有预链接好的地址,所以无需再进行复杂的符号解析和重定位。
c. 跳转到main()
函数。 -
这个过程极其迅速,因为大部分繁重的工作已经被提前做好了,运行时几乎只是加载一个现成的结果。
-
带来的优化效果:
-
启动速度显著提升:尤其是对于暖启动(Warm Launch) 和热启动(Hot Launch),速度提升非常明显,因为省去了大量的解析和计算开销。
-
安全性增强:所有预计算都在一个独立的、特权受限的沙盒进程中完成,并且最终的启动闭包是经过签名的,防止了篡改。
-
可靠性提高:预计算阶段如果出现问题(如镜像缺失、代码签名无效),可以更早、更清晰地报告错误,而不是在启动中途崩溃。
注意:启动闭包是每个设备、每个App版本唯一的。如果App的依赖关系发生变化(如更新了嵌入式动态库),或者系统库更新了,就需要重新生成启动闭包。
30.Swift Package Manager (SPM) 与 CocoaPods 和 Carthage 相比有哪些优势和劣势?在什么情况下应该选择 SPM?
答案详解:
三大依赖管理工具对比:
特性 | Swift Package Manager (SPM) | CocoaPods | Carthage |
---|---|---|---|
官方支持 | Apple 官方,与 Xcode 深度集成。 | 社区 | 社区 |
集成方式 | 源码集成。直接编译到项目中。 | 源码集成。创建 Pods project 和工作区。 |
二进制框架集成。下载预编译的 .framework 文件,手动链接。 |
配置文件 | Package.swift (Swift) |
Podfile (Ruby DSL) |
Cartfile (自定义语法) |
依赖解析 | 集成在 Xcode 中,自动解析和下载。 | 需要命令行运行 pod install ,生成 Podfile.lock 。 |
需要命令行运行 carthage update ,生成 Cartfile.resolved 。 |
对项目影响 | 最小。无缝融入现有项目结构。 | 大。创建新的 workspace,修改项目配置。 | 中等。需要手动在项目配置中添加框架和链接。 |
多平台支持 | 优秀。原生支持 iOS, macOS, tvOS, watchOS, Linux, Windows 等。 | 良好。主要聚焦 Apple 平台。 | 良好。主要聚焦 Apple 平台。 |
二进制依赖 | 支持(需要特定版本和条件)。 | 良好支持(通过 :podspec 指定)。 |
核心特性。本身就是二进制依赖管理。 |
速度 | 快(尤其是增量编译)。 | 慢(首次安装需要生成整个 Pods project)。 | 快(更新后直接使用二进制,无需编译)。 |
生态 | 快速增长,是未来的趋势。 | 历史最悠久,生态最庞大。 | 逐渐萎缩,SPM 正在取代其地位。 |
SPM 的优势:
-
原生集成:是 Xcode 的“一等公民”,管理和更新依赖非常方便,无需切换上下文到命令行。
-
编译速度快:依赖包作为原生 Swift 模块集成,支持模块化编译和增量编译。
-
安全可靠:依赖包版本被锁定在
Package.resolved
文件中,保证团队一致性。 -
未来方向:是 Apple 力推的方案,会持续获得最新特性的支持(如 Binary Targets、Resources、Plugins)。
SPM 的劣势(正在改善):
-
对复杂资源的支持:历史上对
.xib
,.storyboard
, 资产目录(Asset Catalog)的支持不如 CocoaPods 完善,但现在已大大改进。 -
对二进制依赖的支持:虽然已支持,但成熟度和灵活性上仍略逊于 Carthage 和 CocoaPods。
-
对混合语言库的支持:对于包含大量 Objective-C 代码的库,配置可能比 CocoaPods 稍复杂。
如何选择?
-
新项目,纯 Swift 或 Swift 为主:毫无疑问选择 SPM。它是未来,体验最好。
-
现有大型项目,已深度使用 CocoaPods:迁移到 SPM 可能成本较高,可以继续使用 CocoaPods,或逐步将新依赖改为 SPM。
-
需要分发二进制框架以减少编译时间:可以考虑 Carthage,或者使用 SPM 的 Binary Target。
-
依赖的库仅支持 CocoaPods:那就只能使用 CocoaPods。
趋势:SPM 正在成为 iOS 生态的依赖管理标准,绝大多数主流库都已支持 SPM。除非有强力的历史原因或特定需求(如严格的二进制依赖),否则应优先采用 SPM。
持续更新中。。。
更多推荐
所有评论(0)