iOS开发基础73-性能优化:从原理到实战的 30+ 优化技巧
iOS 性能优化完全指南:从原理到实战的 30+ 优化技巧
性能优化是 iOS 开发中永恒的话题。一个流畅的应用能显著提升用户体验,而卡顿、内存暴涨、启动慢则会直接导致用户流失。本文深入讲解多个优化点背后的原理,并补充启动优化、离屏渲染、Instruments 检测、卡顿排查等实战中更重要的内容,形成一套系统的性能优化知识体系。
一、性能优化概述
1. 性能优化的核心目标
| 目标 | 衡量指标 | 用户感知 |
|---|---|---|
| 快速启动 | 启动时间 < 400ms(冷启动) | 点开 App 秒开 |
| 流畅滑动 | FPS 稳定 60fps(每帧 16.7ms) | 列表滚动不卡顿 |
| 内存合理 | 无内存泄漏,峰值不超系统限制 | 不闪退、不被系统杀 |
| 响应及时 | 点击响应 < 100ms | 操作跟手不迟钝 |
| 功耗低 | CPU/GPU 占用合理 | 不发烫、不掉电快 |
2. 优化的原则
- 先测量,再优化:用 Instruments 定位瓶颈,不要凭感觉优化。
- 2/8 原则:80% 的性能问题来自 20% 的代码,优先优化热点。
- 不要过度优化:在性能达标后,可读性和可维护性更重要。
二、内存优化
1. ARC 自动引用计数
一句话原理:ARC 是编译器在编译时自动帮你插入 retain/release/autorelease,对象的引用计数归零时自动释放。
注意点:
- ARC 不是垃圾回收(GC),是编译时插入代码,运行时仍按引用计数管理。
- ARC 不能解决循环引用问题,需要开发者手动用
weak打破循环。 - Core Foundation 对象(CGImageRef、CFStringRef 等)不在 ARC 管理范围内,需要手动
CFRelease。
2. 循环引用检测与避免(高频问题)
常见循环引用场景:
- Block 循环引用:self 持有 block,block 内又强引用 self。
- Delegate 循环引用:delegate 属性用
strong修饰。 - NSTimer 循环引用:timer 强引用 target,target 强引用 timer。
- 通知/KVO 未移除:观察者未移除导致引用关系异常。
Block 循环引用的标准解决方式:
// 方式一:weak-strong dance
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomething];
}
};
// 方式二:@weakify/@strongify(ReactiveObjC 或自定义宏)
@weakify(self);
self.block = ^{
@strongify(self);
[self doSomething];
};
NSTimer 循环引用的解决方式:
// 方式一:用 block 形式的 timer(iOS 10+)
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf doSomething];
}];
// 方式二:在 dealloc 中 invalidate(注意必须在 dealloc 之前的时机调用,如 viewWillDisappear)
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
// 方式三:用 NSProxy 中间对象打破循环
Delegate 必须用 weak:
@property (nonatomic, weak) id<MyDelegate> delegate; // 必须 weak,不能 strong
3. Autorelease Pool 的正确使用
一句话原理:autorelease 对象会在当前 autorelease pool 销毁时释放。手动创建 pool 可以控制释放时机,避免内存峰值过高。
适用场景:
- 循环中创建大量临时对象(如遍历数组处理图片)。
- 非主线程(子线程默认没有 autorelease pool,需要手动创建)。
for (int i = 0; i < 10000; i++) {
@autoreleasepool {
// 每次循环创建的临时对象在本次 pool 结束时释放
NSString *str = [NSString stringWithFormat:@"%d", i];
UIImage *image = [UIImage imageNamed:str];
// ...
}
}
4. 图片加载:imageNamed vs imageWithContentsOfFile
| 方法 | 缓存 | 适用场景 | 内存表现 |
|---|---|---|---|
imageNamed: |
系统自动缓存,释放不立即回收 | 小图、频繁使用的图(如图标) | 缓存占用内存,但重复加载快 |
imageWithContentsOfFile: |
不缓存,用完即释放 | 大图、一次性使用的图(如启动图、全屏背景) | 不占缓存内存,但重复加载需重新读磁盘 |
// 频繁使用的小图标 → 用 imageNamed(缓存)
UIImage *icon = [UIImage imageNamed:@"icon_settings"];
// 一次性大图 → 用 imageWithContentsOfFile(不缓存)
NSString *path = [[NSBundle mainBundle] pathForResource:@"large_background" ofType:@"png"];
UIImage *bg = [UIImage imageWithContentsOfFile:path];
5. NSCache 代替 NSMutableDictionary 做缓存
一句话原理:NSCache 是系统提供的缓存类,内存不足时自动清理,线程安全,且 key 不会被 retain(NSDictionary 的 key 会被 copy)。
@property (nonatomic, strong) NSCache *imageCache;
- (NSCache *)imageCache {
if (!_imageCache) {
_imageCache = [[NSCache alloc] init];
_imageCache.countLimit = 100; // 最多缓存 100 个对象
_imageCache.totalCostLimit = 50 * 1024 * 1024; // 最多 50MB
}
return _imageCache;
}
// 存
[self.imageCache setObject:image forKey:urlString cost:image.size.width * image.size.height];
// 取
UIImage *cachedImage = [self.imageCache objectForKey:urlString];
6. 内存警告处理
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// 移除不可见的视图
// 清空图片缓存
[self.imageCache removeAllObjects];
// 释放可重建的大对象
}
注意:iOS 6 之后,系统在内存警告时会自动卸载不可见的 view,开发者主要负责清理缓存和可重建的数据。
三、UI 渲染优化(最影响流畅度)
1. Cell 复用(reuseIdentifier)
一句话原理:TableView/CollectionView 维护一个复用池,滚动时把滑出屏幕的 Cell 放入复用池,新进入屏幕的 Cell 从复用池取出重新配置,避免频繁创建销毁。
// 注册(必须)
[self.tableView registerClass:[MyCell class] forCellReuseIdentifier:@"MyCell"];
// 复用
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCell" forIndexPath:indexPath];
// 配置 cell...
return cell;
}
常见错误:每次
cellForRowAtIndexPath都[[UITableViewCell alloc] init],完全没用复用机制,滚动必卡。
2. 不透明视图(opaque)
一句话原理:视图不透明时,GPU 不需要做颜色混合(Blend),直接用该视图的颜色覆盖底层,减少 GPU 计算。
view.opaque = YES; // 视图完全不透明时设置
view.backgroundColor = [UIColor whiteColor]; // 必须设置不透明的背景色
检测工具:模拟器 → Debug → Color Blended Layers(红色表示有混合,绿色表示无混合)。
注意:
opaque = YES只是给 GPU 的一个优化提示,如果视图实际有透明区域(如 backgroundColor = clearColor),设置 opaque 也没用,反而可能导致渲染错误。
3. 离屏渲染优化(高频面试题)
一句话原理:离屏渲染(Offscreen Rendering)是指 GPU 在当前屏幕缓冲区之外,先开辟一个临时缓冲区进行渲染,再合成到屏幕。频繁的离屏渲染会导致 GPU 上下文切换,造成卡顿。
常见触发离屏渲染的操作:
| 操作 | 说明 |
|---|---|
| 圆角 + masksToBounds | layer.cornerRadius + layer.masksToBounds = YES |
| 阴影 | layer.shadowXXX(未设置 shadowPath) |
| 遮罩 | layer.mask |
| 光栅化 | layer.shouldRasterize = YES |
| 毛玻璃 | UIVisualEffectView(iOS 不可避免) |
(1)圆角优化
// ❌ 触发离屏渲染
imageView.layer.cornerRadius = 10;
imageView.layer.masksToBounds = YES;
// ✅ 方案一:用贝塞尔曲线裁剪(Core Graphics 绘制圆角,CPU 一次性处理)
- (UIImage *)circleImageWithImage:(UIImage *)image {
UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
[path addClip];
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
// ✅ 方案二:用一张中间镂空的圆角遮罩图片覆盖(4 个角是背景色,中间透明)
// ✅ 方案三:iOS 13+ 用 CALayerCornerCurve,或直接用 iOS 9+ 的 UIImageView 圆角(仅图片时不触发离屏)
注意:iOS 9 之后,
UIImageView设置cornerRadius且只设置图片(没有 backgroundColor)时,不会触发离屏渲染,因为 GPU 可以直接对纹理做圆角裁剪。但如果有 backgroundColor 或子视图,仍会触发离屏。
(2)阴影优化
// ❌ 触发离屏渲染(每次渲染都要计算阴影形状)
view.layer.shadowColor = [UIColor blackColor].CGColor;
view.layer.shadowOpacity = 0.5;
view.layer.shadowRadius = 5;
view.layer.shadowOffset = CGSizeMake(0, 2);
// ✅ 设置 shadowPath,告诉 GPU 阴影形状,避免每次重新计算
view.layer.shadowPath = [UIBezierPath bezierPathWithRect:view.bounds].CGPath;
(3)光栅化的正确使用
// shouldRasterize 会把视图渲染成位图缓存,适合静态不变化的复杂视图
view.layer.shouldRasterize = YES;
view.layer.rasterizationScale = [UIScreen mainScreen].scale; // 必须设置,否则模糊
注意:
shouldRasterize只适合静态不变化的视图。如果视图频繁变化(如动画),光栅化反而会降低性能,因为每次变化都要重新渲染位图。
检测工具:模拟器 → Debug → Color Offscreen-Rendered Yellow(黄色表示触发了离屏渲染)。
4. 图片大小与 UIImageView 匹配
一句话原理:如果图片尺寸大于 ImageView 尺寸,GPU 渲染时需要实时缩放,消耗性能。应该提前把图片缩放到目标尺寸。
- (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)size {
UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
[image drawInRect:CGRectMake(0, 0, size.width, size.height)];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
图片解码优化:
UIImage加载后是压缩格式(PNG/JPEG),首次渲染时才在主线程解码,大图解码耗时会导致卡顿。可以用CGImageSource异步解码,或用 SDWebImage 等库自动处理。
5. 减少 Subviews 数量
一句话原理:每个子视图都需要布局、渲染、事件处理,视图层级越深、数量越多,CPU/GPU 开销越大。
优化方法:
- 能用一个视图实现的,不要拆成多个。
- 复杂自定义视图用
drawRect:直接绘制,而不是加多个子视图。 - 隐藏的视图及时移除(
hidden = YES的视图仍会参与布局和渲染)。
6. 延迟加载(Lazy Load)
一句话原理:不需要立即显示的视图,等到需要时再创建,减少初始加载时间和内存占用。
@property (nonatomic, strong) UIButton *detailButton;
- (UIButton *)detailButton {
if (!_detailButton) {
_detailButton = [[UIButton alloc] init];
// 配置...
}
return _detailButton;
}
// 只有在需要显示时才访问 self.detailButton,触发创建
7. TableView/CollectionView 优化汇总
| 优化点 | 说明 |
|---|---|
| Cell 复用 | 必须 register + dequeue,不要每次 alloc |
| Cell 高度缓存 | 提前计算高度并缓存,避免滚动时重复计算 |
| 异步加载图片 | 用 SDWebImage/Kingfisher,不要在 cellForRow 里同步下载 |
| 减少离屏渲染 | 圆角用预裁剪图片,阴影用 shadowPath |
| 视图不透明 | opaque = YES + 不透明背景色 |
| 减少子视图 | 复杂 cell 用 drawRect 绘制 |
| 不要在 cellForRow 做耗时操作 | 数据解析、格式化提前做好 |
| 滑动时暂停加载 | scrollViewDidScroll 中判断,快速滑动时暂停图片加载 |
Cell 高度缓存示例:
@property (nonatomic, strong) NSMutableDictionary *heightCache; // key: indexPath, value: 高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber *cachedHeight = self.heightCache[@(indexPath.row)];
if (cachedHeight) {
return cachedHeight.floatValue;
}
CGFloat height = [self calculateHeightForRow:indexPath];
self.heightCache[@(indexPath.row)] = @(height);
return height;
}
8. 预渲染图片
一句话原理:对于复杂的、不变化的视图(如按钮背景、渐变),提前用 Core Graphics 绘制成位图(UIImage),渲染时直接贴图,比每次用代码绘制快。
// 预渲染一个渐变背景
- (UIImage *)preRenderGradientImage {
CGSize size = CGSizeMake(200, 50);
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGGradientRef gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)@[
(id)[UIColor redColor].CGColor,
(id)[UIColor blueColor].CGColor
], NULL);
CGContextDrawLinearGradient(context, gradient, CGPointZero, CGPointMake(size.width, 0), 0);
CGGradientRelease(gradient);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
四、启动优化
1. 启动流程
| 阶段 | 内容 | 优化方向 |
|---|---|---|
| Pre-main(t 之前) | 加载可执行文件、加载动态库、ObjC 类注册、+load 方法、C++ 构造函数 | 减少动态库、减少 +load、减少 C++ 静态对象 |
| main(t 之后) | main 函数 → AppDelegate 启动方法 → 首屏渲染 | 减少启动时任务、延迟非必要初始化、首屏轻量化 |
2. Pre-main 优化
- 减少动态库数量:动态库加载耗时,尽量用静态库。苹果建议动态库不超过 6 个。
- 减少 +load 方法:+load 在启动时执行,耗时。把逻辑移到 +initialize 或懒加载。
- 减少 C++ 静态对象:C++ 全局对象的构造函数在启动时执行。
- 合并 Category:大量 Category 会增加 ObjC 类注册时间。
3. main 阶段优化
- 首屏控制器轻量化:首屏只加载必须的 UI 和数据,其他延迟加载。
- 启动任务分级:
- 必须在启动完成前:注册推送、初始化统计 SDK、配置环境。
- 可以在首屏渲染后:用户信息加载、配置拉取、非首屏 SDK 初始化。
- 可以在空闲时:缓存清理、预加载下一页数据。
- 使用启动图占位:启动图到首屏之间避免白屏。
4. 启动时间测量
// 在 main.m 中记录启动时间
CFAbsoluteTime StartTime;
int main(int argc, char * argv[]) {
StartTime = CFAbsoluteTimeGetCurrent();
// ...
}
// 在 AppDelegate didFinishLaunching 中输出
NSLog(@"启动耗时: %f", CFAbsoluteTimeGetCurrent() - StartTime);
也可以用 Xcode → Product → Scheme → Edit → Run → Arguments → Environment Variables 添加
DYLD_PRINT_STATISTICS = 1,控制台会输出 Pre-main 各阶段耗时。
五、网络优化
1. GZIP 压缩
一句话原理:服务器返回的数据用 GZIP 压缩后体积减小 60%~80%,传输更快。iOS 的 NSURLSession/NSURLConnection 默认支持 GZIP 解压,只需服务器开启压缩即可。
2. 缓存策略
| 缓存类型 | 适用场景 | 实现 |
|---|---|---|
| 内存缓存 | 频繁访问的小数据 | NSCache |
| 磁盘缓存 | 不常变化的网络数据 | NSURLCache / 自定义 |
| 离线缓存 | 无网络时展示 | 数据库 + 时间戳 |
// 配置 NSURLCache
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:20 * 1024 * 1024
diskCapacity:100 * 1024 * 1024
diskPath:@"networkCache"];
[NSURLCache setSharedURLCache:cache];
3. 数据格式选择
| 格式 | 体积 | 解析速度 | 适用场景 |
|---|---|---|---|
| JSON | 小 | 快(iOS 5+ 原生 NSJSONSerialization) | 大多数 API |
| XML | 大 | 慢(需要 DOM/SAX 解析) | 配置文件、WebService、大批量数据 |
| Protocol Buffers | 最小 | 最快 | 高性能场景、IM 消息 |
移动端优先用 JSON,体积小、解析快。XML 仅在服务器强制要求时使用。
4. 减少网络请求
- 合并请求:把多个小请求合并成一个批量请求。
- 请求去重:相同请求同时发起时,只发一个,多个回调共享结果。
- 分页加载:列表用分页,不要一次拉全部数据。
- 增量更新:只拉取变化的数据,不要每次全量拉取。
六、数据存储优化
1. 选择合适的存储方式
| 存储方式 | 适用场景 | 不适用场景 |
|---|---|---|
| NSUserDefaults | 小量配置数据(BOOL、NSString、NSNumber) | 大数据、频繁读写、自定义对象 |
| Plist 文件 | 中小量结构化数据 | 大数据、频繁查询 |
| NSKeyedArchiver | 自定义对象归档(少量) | 大数据、频繁查询 |
| SQLite/FMDB | 大量数据、复杂查询、频繁读写 | 简单的小数据 |
| Core Data | 大量数据、关系复杂、需要 iCloud 同步 | 简单的小数据 |
| Keychain | 敏感数据(密码、Token) | 普通数据 |
2. 重用大开销对象
一句话原理:NSDateFormatter、NSCalendar 创建开销极大(比一般对象慢 100 倍以上),必须重用。
@property (nonatomic, strong) NSDateFormatter *dateFormatter;
- (NSDateFormatter *)dateFormatter {
if (!_dateFormatter) {
_dateFormatter = [[NSDateFormatter alloc] init];
_dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss";
_dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"zh_CN"];
}
return _dateFormatter;
}
注意:
NSDateFormatter不是线程安全的,多线程使用时要么每个线程一个,要么加锁。iOS 10+ 可以用NSISO8601DateFormatter(线程安全)。
3. 数据库优化(SQLite/FMDB)
- 用事务批量插入:
BEGIN TRANSACTION→ 批量插入 →COMMIT,比逐条插入快几十倍。 - 加索引:频繁查询的字段加索引(
CREATE INDEX)。 - 异步操作:数据库操作放后台线程,不要阻塞主线程。
- FMDB 用 FMDatabaseQueue:保证线程安全,避免多线程同时写数据库。
// 批量插入用事务
[self.dbQueue inDatabase:^(FMDatabase *db) {
[db executeUpdate:@"BEGIN TRANSACTION"];
for (NSDictionary *item in items) {
[db executeUpdate:@"INSERT INTO table (col1, col2) VALUES (?, ?)", item[@"col1"], item[@"col2"]];
}
[db executeUpdate:@"COMMIT"];
}];
七、代码质量与架构
1. 不要阻塞主线程
一句话原理:主线程负责 UI 渲染和事件响应,任何耗时操作(网络、文件 IO、复杂计算、大量数据解析)都必须放后台线程。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 后台线程:耗时操作
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
// 主线程:更新 UI
self.imageView.image = image;
});
});
常见阻塞主线程的操作:
- 同步网络请求
- 大量图片解码/缩放
- 复杂的 JSON 解析(几万条数据)
- 数据库查询(大表无索引)
- 文件读写(大文件)
- 正则表达式匹配(大文本)
2. 选择正确的集合类型
| 集合 | 特点 | 适用场景 |
|---|---|---|
| NSArray | 有序,索引查找 O(1),值查找 O(n) | 需要按顺序访问、通过索引取数据 |
| NSDictionary | 键值对,键查找 O(1) | 通过 key 快速取值 |
| NSSet | 无序,值查找 O(1),插入删除快 | 去重、判断是否包含、集合运算 |
| NSOrderedSet | 有序且值唯一 | 既要顺序又要去重(iOS 5+) |
高频错误:用数组遍历查找元素(O(n)),应该用字典或 Set(O(1))。
3. 避免反复处理数据
- 数据格式化提前做:从服务器拿到数据后,立即格式化成展示需要的格式(如时间戳转字符串),不要在
cellForRow里每次都转。 - 计算结果缓存:复杂计算(如文本高度、布局 frame)算一次缓存起来,不要重复计算。
- 用模型对象:不要到处传 NSDictionary,用模型对象提前解析好,属性访问比字典取值快且安全。
4. WKWebView 优化(原 UIWebView 已废弃)
WKWebView 优化点:
- 提前初始化:在启动时或进入页面前提前创建 WKWebView 实例,复用进程。
- 缓存:用
WKWebsiteDataStore管理缓存。 - JS 交互优化:用
WKScriptMessageHandler代替stringByEvaluatingJavaScriptFromString。 - 图片懒加载:网页中图片用懒加载,减少首屏渲染时间。
- 避免同步 JS 调用:
evaluateJavaScript是异步的,不要在主线程等待结果。
八、性能检测工具(Instruments)
1. 常用 Instruments 模板
| 模板 | 用途 | 检测什么 |
|---|---|---|
| Time Profiler | CPU 耗时分析 | 哪些方法占用 CPU 时间长、主线程是否被阻塞 |
| Allocations | 内存分配分析 | 对象创建/销毁、内存占用趋势、是否有内存泄漏 |
| Leaks | 内存泄漏检测 | 哪些对象泄漏了、泄漏的调用栈 |
| Core Animation | 渲染性能 | FPS、离屏渲染、颜色混合 |
| Network | 网络分析 | 请求耗时、流量、请求顺序 |
| Energy Log | 功耗分析 | CPU/网络/GPS 对电量的影响 |
2. Time Profiler 使用步骤
- Xcode → Product → Profile(或 Cmd+I)→ 选择 Time Profiler。
- 点击录制,操作 App 复现卡顿场景。
- 停止录制,在调用栈中找占用时间长的方法。
- 勾选 "Call Tree" → "Separate by Thread",区分主线程和后台线程。
- 主线程中耗时 > 16ms 的方法就是卡顿元凶。
3. 模拟器快速检测
| 检测项 | 路径 | 效果 |
|---|---|---|
| 颜色混合 | Debug → Color Blended Layers | 红色=有混合(需优化),绿色=无混合 |
| 离屏渲染 | Debug → Color Offscreen-Rendered Yellow | 黄色=触发离屏渲染 |
| 慢动画 | Debug → Slow Animations | 动画变慢,便于观察卡顿 |
| FPS | Debug → Show FPS | 显示帧率,60 为流畅 |
九、常见问题排查
问题 1:TableView 滚动卡顿
排查步骤:
- 用 Time Profiler 看
cellForRowAtIndexPath是否耗时。 - 用模拟器 Color Offscreen-Rendered Yellow 看是否有离屏渲染(圆角、阴影)。
- 用 Color Blended Layers 看是否有大量透明视图。
- 检查图片是否同步下载/解码。
- 检查 Cell 高度是否重复计算。
常见原因:
- Cell 没复用(每次 alloc)。
- 圆角用
cornerRadius + masksToBounds触发离屏渲染。 cellForRow里做了数据解析、格式化、图片下载。- 图片尺寸远大于 ImageView,实时缩放。
- Cell 高度每次重新计算(文本高度计算慢)。
问题 2:内存暴涨/泄漏
排查步骤:
- 用 Instruments → Leaks 检测泄漏对象。
- 用 Allocations 看内存增长趋势,操作前后对比。
- 检查 Block 是否循环引用(weak-strong dance)。
- 检查 NSTimer 是否 invalidate。
- 检查 delegate 是否用 weak。
- 检查通知/KVO 是否移除。
- 检查 Core Foundation 对象是否 CFRelease。
问题 3:启动慢
排查步骤:
- 用
DYLD_PRINT_STATISTICS看 Pre-main 各阶段耗时。 - 用 Time Profiler 从启动开始录制,看
didFinishLaunching中的耗时方法。 - 检查是否有大量 +load 方法。
- 检查启动时是否同步做了网络请求、数据库迁移。
- 检查首屏控制器是否过于复杂。
问题 4:点击响应慢
排查步骤:
- Time Profiler 看点击后主线程是否被耗时操作阻塞。
- 检查是否有手势冲突(多个手势识别器)。
- 检查视图层级是否过深(事件传递链长)。
- 检查是否在
viewWillAppear/viewDidAppear做了耗时操作。
十、Swift 版本对照
1. 弱引用与循环引用
// weak-strong dance
class ViewController: UIViewController {
var completion: (() -> Void)?
func setup() {
weak var weakSelf = self
completion = {
guard let strongSelf = weakSelf else { return }
strongSelf.doSomething()
}
}
}
2. 日期格式化器重用
class DateHelper {
static let shared = DateHelper()
private let formatter: DateFormatter
private init() {
formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.locale = Locale(identifier: "zh_CN")
}
func string(from date: Date) -> String {
return formatter.string(from: date)
}
}
3. 异步加载图片
DispatchQueue.global(qos: .userInitiated).async {
let data = try? Data(contentsOf: url)
let image = data.flatMap { UIImage(data: $0) }
DispatchQueue.main.async {
self.imageView.image = image
}
}
4. Autorelease Pool
for i in 0..<10000 {
autoreleasepool {
let str = String(i)
// ...
}
}
总结
- 内存优化:ARC 自动管理但需手动打破循环引用(Block/Timer/Delegate);图片用 imageNamed(小图缓存)或 imageWithContentsOfFile(大图不缓存);NSCache 代替 NSMutableDictionary;循环中用 @autoreleasepool;内存警告时清理缓存。
- UI 渲染优化(最影响流畅):Cell 必须复用;视图设 opaque 减少混合;避免离屏渲染(圆角用预裁剪图片、阴影设 shadowPath、光栅化仅用于静态视图);图片尺寸匹配 ImageView;减少 Subviews;延迟加载;TableView 缓存高度、异步加载图片。
- 启动优化:Pre-main 减少动态库/+load/C++ 静态对象;main 阶段首屏轻量化、启动任务分级、非必要初始化延迟。
- 网络优化:GZIP 压缩、合理缓存(NSCache/NSURLCache)、优先 JSON、合并请求/分页/增量更新。
- 数据存储:NSUserDefaults 存小配置,大量数据用 SQLite/FMDB(事务批量插入+索引),自定义对象用模型不用字典;NSDateFormatter/NSCalendar 必须重用。
- 代码质量:耗时操作放后台线程不阻塞主线程;选对集合类型(数组/字典/Set);数据格式化提前做不重复处理;WKWebView 提前初始化复用。
- 检测工具:Instruments(Time Profiler 查卡顿、Leaks 查泄漏、Core Animation 查渲染)、模拟器 Color Blended Layers/Color Offscreen-Rendered 快速定位。
- 核心原则:先测量再优化,用 Instruments 定位瓶颈,不要凭感觉优化;优先优化热点(2/8 原则);性能达标后可读性更重要。

浙公网安备 33010602011771号