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. 循环引用检测与避免(高频问题)

常见循环引用场景:

  1. Block 循环引用:self 持有 block,block 内又强引用 self。
  2. Delegate 循环引用:delegate 属性用 strong 修饰。
  3. NSTimer 循环引用:timer 强引用 target,target 强引用 timer。
  4. 通知/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 优化

  1. 减少动态库数量:动态库加载耗时,尽量用静态库。苹果建议动态库不超过 6 个。
  2. 减少 +load 方法:+load 在启动时执行,耗时。把逻辑移到 +initialize 或懒加载。
  3. 减少 C++ 静态对象:C++ 全局对象的构造函数在启动时执行。
  4. 合并 Category:大量 Category 会增加 ObjC 类注册时间。

3. main 阶段优化

  1. 首屏控制器轻量化:首屏只加载必须的 UI 和数据,其他延迟加载。
  2. 启动任务分级:
    • 必须在启动完成前:注册推送、初始化统计 SDK、配置环境。
    • 可以在首屏渲染后:用户信息加载、配置拉取、非首屏 SDK 初始化。
    • 可以在空闲时:缓存清理、预加载下一页数据。
  3. 使用启动图占位:启动图到首屏之间避免白屏。

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 使用步骤

  1. Xcode → Product → Profile(或 Cmd+I)→ 选择 Time Profiler。
  2. 点击录制,操作 App 复现卡顿场景。
  3. 停止录制,在调用栈中找占用时间长的方法。
  4. 勾选 "Call Tree" → "Separate by Thread",区分主线程和后台线程。
  5. 主线程中耗时 > 16ms 的方法就是卡顿元凶。

3. 模拟器快速检测

检测项 路径 效果
颜色混合 Debug → Color Blended Layers 红色=有混合(需优化),绿色=无混合
离屏渲染 Debug → Color Offscreen-Rendered Yellow 黄色=触发离屏渲染
慢动画 Debug → Slow Animations 动画变慢,便于观察卡顿
FPS Debug → Show FPS 显示帧率,60 为流畅

九、常见问题排查

问题 1:TableView 滚动卡顿

排查步骤:

  1. 用 Time Profiler 看 cellForRowAtIndexPath 是否耗时。
  2. 用模拟器 Color Offscreen-Rendered Yellow 看是否有离屏渲染(圆角、阴影)。
  3. 用 Color Blended Layers 看是否有大量透明视图。
  4. 检查图片是否同步下载/解码。
  5. 检查 Cell 高度是否重复计算。

常见原因:

  • Cell 没复用(每次 alloc)。
  • 圆角用 cornerRadius + masksToBounds 触发离屏渲染。
  • cellForRow 里做了数据解析、格式化、图片下载。
  • 图片尺寸远大于 ImageView,实时缩放。
  • Cell 高度每次重新计算(文本高度计算慢)。

问题 2:内存暴涨/泄漏

排查步骤:

  1. 用 Instruments → Leaks 检测泄漏对象。
  2. 用 Allocations 看内存增长趋势,操作前后对比。
  3. 检查 Block 是否循环引用(weak-strong dance)。
  4. 检查 NSTimer 是否 invalidate。
  5. 检查 delegate 是否用 weak。
  6. 检查通知/KVO 是否移除。
  7. 检查 Core Foundation 对象是否 CFRelease。

问题 3:启动慢

排查步骤:

  1. 用 DYLD_PRINT_STATISTICS 看 Pre-main 各阶段耗时。
  2. 用 Time Profiler 从启动开始录制,看 didFinishLaunching 中的耗时方法。
  3. 检查是否有大量 +load 方法。
  4. 检查启动时是否同步做了网络请求、数据库迁移。
  5. 检查首屏控制器是否过于复杂。

问题 4:点击响应慢

排查步骤:

  1. Time Profiler 看点击后主线程是否被耗时操作阻塞。
  2. 检查是否有手势冲突(多个手势识别器)。
  3. 检查视图层级是否过深(事件传递链长)。
  4. 检查是否在 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 原则);性能达标后可读性更重要。

posted @ 2015-10-29 21:24  Mr.陳  阅读(2261)  评论(0)    收藏  举报