前言

iOS性能优化系列篇之“优化总体原则”

  • 不要提前过度优化
  • 要找到性能瓶颈
  • 要在不同性能指标间权衡
  • 要理解优化任务的底层运行机制
  • 要有技术保障体系

一、启动速度优化

1.1 学习文章

1.2 操作步骤

  1. 查看启动时间
    配置 Xcode 环境变量在日志中打印启动时间:
  • 打开工程 -> Edit Scheme -> Run -> Environment Variables

根据需要添加 DYLD_PRINT_STATISTICSDYLD_PRINT_STATISTICS_DETAILS 环境变量。value 设置为 1(表示 YES),开启这个功能。

Total pre-main time: 617.58 milliseconds (100.0%)
         dylib loading time: 472.75 milliseconds (76.5%)

        rebase/binding time:  27.01 milliseconds (4.3%)

            ObjC setup time:  28.90 milliseconds (4.6%)

           initializer time:  88.76 milliseconds (14.3%)

           slowest intializers :

             libSystem.B.dylib :   8.81 milliseconds (1.4%)

    libMainThreadChecker.dylib :  14.42 milliseconds (2.3%)

                  AFNetworking :  18.43 milliseconds (2.9%)

                         Realm :  20.98 milliseconds (3.3%)

                     CYKJBasic :  12.96 milliseconds (2.0%)

包括执行以下步骤的所有时间:

  1. 解析镜像
  2. 映射镜像
  3. Rebase 镜像
  4. Bind 镜像
  5. 镜像初始化
  6. 调用 main()方法
  7. 调用 UIApplicationMain() 方法
  8. 调用 applicationWillFinishLaunching 回调
  1. 优化 main() 函数之后的执行时间
  • 使用代码绘制 UI,减少或者不用 xib 和 storyboard
  • 延迟初始化和加载不必要的 UIViewController 和 View。
  • 使用后台线程处理耗时的任务
  • 能延迟初始化的尽量延迟初始化
  1. 优化 main() 函数之前的执行时间
    Session 所要传达的内容:
  • 使用 DYLD_PRINT_STATISTICS 测试启动加载时间
  • 减少自定义的动态库集成
  • 精简原有的 Objective-C 类和代码
  • 移除静态的初始化操作
  • 使用更多的 Swift 代码

优化:

  • loading dylib
    减少动态库的数量。尽量保证将 App 现有的非系统级的动态库个数保证在 6 个以内。采取的方式:1、合并动态库;2、使用静态库。
  • rebase/binding
    Rebase 和 Binding 都是为了解决指针引用的问题。对于 Objective-C 开发来说,主要的时间消耗在 Class/Method 的符号加载上,所以常见的优化方案是:
  • 减少 __DATA 段中的指针数量。
  • 合并 Category 和功能类似的类,减少唯一 Selector 的个数。
  • 删除无用的方法和类。
  • 多用 Swift Structs,因为 Swfit Structs 是静态分发的。
  • 减少 c++ 虚函数
  • ObjC setup time
    尽量减少类的数量,可以达到减少这一部分的时间。
  • Initializers
  • 用 initialize 替代 load。(注意:要根据实际情况替换)
  • 减少使用 c/c++ 的 __atribute__((constructor))。推荐使用 dispatch_once()、pthread_once()、std:once()等方法;
  • 推荐使用 swift
  • 不要在初始化中调用 dlopen() 方法,因为加载过程是单线程,无锁,如果调用 dlopen 则会变成多线程,会开启锁的消耗,同时有可能死锁
  • 不要在初始化中创建线程

1.3 实际处理

实际处理时因为要考虑团队的原因,所以只采用了一下三个步骤:

  1. 删除不需要的三方库,因为工程中用 use_frameworks! pod 进来的库是动态库。
  2. 将大部分 +load 方法改为 +initialize 方法,保留部分必要的 +load 方法;
  3. 动态库 -> 静态库,技术上实现了,讨论后最终弃用。
  4. 删除无用的方法和类,减少 rebase/binding 时间。。

1.4 处理效果

慈云 app 启动时间有 700 ~ 850ms,降为 600 ~ 700ms。

如果采用静态方式 pod,可以降为 450 ~ 550ms。

二、接口请求优化

主要集中处理一级 tab 的切换体验,频繁的调用接口,频繁的刷新界面显然是影响用户体验的。优化的思路有以下几点:

  1. 使用 loading 框 + 默认灰色矩形视图;
  2. 使用本地缓存
  3. 每隔 15s(或者 10s) 以上才请求一次,防止频繁触发请求
@property (nonatomic, assign) CFTimeInterval lastTi;
- (void)viewWillAppear:(BOOL)animated

{

    [super viewWillAppear:animated];

    CFTimeInterval nowTi = CACurrentMediaTime();

    // 10 秒内不请求

    if (nowTi - self.lastTi > 10) {

        self.reqData.pageNo = 1;

        [self.service requestPatientConsultList:self.reqData];

        self.lastTi = nowTi;

    }

}

- (void)makeRequestAction

{

    // 接口请求

}

CACurrentMediaTime() 在退到后台、手动修改设备时间后没有影响。

  1. 对数据进行判断,数据没有更新不需要刷新界面。
@property (nonatomic, copy) NSString * primaryCompareMD5;
// 初始值 @""

self.primaryCompareMD5 = @"";

SELF_WEEK;

// 异步执行

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    @synchronized (self) {

        // 这里的 data 是要判断的接口数据

        NSString * string = [data componentsJoinedByString:@""];

        if (!string) {

            string = @"";

        }

        NSString * md5 = [CYDXUtil doMd5:string];

        SELF_STRONG;

        // 数据未发生改变,直接返回

        if ([strongSelf.primaryCompareMD5 isEqualToString:md5]) {

            return;

        }

        strongSelf.primaryCompareMD5 = md5;

      // 主线程刷新界面

        dispatch_sync(dispatch_get_main_queue(), ^{

            [strongSelf.tableView reloadData];

        });

    }

});

这里的策略还是存在点问题:

  1. 不能处理好未读标识。看实际情况,处理从子界面返回时的逻辑。

三、界面滑动优化

3.1 监测界面卡顿工具

  1. 使用系统的 Instrument - CoreAnimation
  2. 使用系统的 Instrument - Time Profiler
  3. YYFPSLabel

在未滑动时,Instrument - CoreAnimation 显示的是 0 FPS。

YYFPSLabel 显示的是 60FPS。

卡顿问题修改后,Instrument - CoreAnimation 需要在设备上运行一遍,然后重新用工具检测;YYFPSLabel 可以直接加入工程,可视化,更加的友好,方便。

Time Profiler 可以查看哪些方法占用时间多,优化那些可以优化的。

3.2 优化处理

Instrument之Core Animation工具

  1. 图层混合

Xcode 顶部菜单栏 -> Debug -> View Debugging -> Rendering -> Color Blended Layers

  1. 正常的为绿色;出现图层混合时为红色。
  • 确保控件的 opaque 属性设置为 true,确保 backgroundColor 和父视图颜色一致且不透明;
  • 如无特殊需要,不要设置低于 1 的 alpha 值;
  • 确保 UIImage 没有 alpha 通道;
  1. 图片缩放

Xcode 顶部菜单栏 -> Debug -> View Debugging -> Rendering -> Color Misaligned Images

  1. 如果图片需要缩放则标记为黄色,如果没有像素对齐则标记为紫色。
    这个看情况是否优化,是否需要 UI 配合。
  • 确保图片大小和frame一致,不要在滑动时缩放图片;
  • 确保图片颜色格式被 GPU 支持,避免劳烦 CPU 转换;
  1. 离屏渲染

Xcode 顶部菜单栏 -> Debug -> View Debugging -> Rendering -> Color Offscreen-Rendered Yellow

  1. 触发离屏渲染的地方标记为黄色。
    下面的情况或操作会引发离屏渲染:
  • 为图层设置遮罩(layer.mask)
  • 同时设置 layer.masksToBounds 和 corneRadius 属性设置为 true
  • 将图层的 layer.allowsGroupOpacity 属性设置为 YES 和 layer.opacity < 1.0
  • 为图层设置阴影(layer.shadow*)
  • 为图层设置 layer.shouldRasterize = true
  • 文本(任何种类,包括 UILabel、CATextLayer、CoreText 等)
  • 使用 CGContext 在 drawRect: 方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现

解决:

  • 使用 CoreGraphics 绘制圆角,不使用 mask 或 masksToBounds + corneRadius;
  • 设置阴影是使用 shadowPath;
  • UILabel 如果不是透明的,设置 opaque = YES,Clip To bounds = YES,backgroundColor;
  • 设置图片圆角可以让 UI 切图,也可以使用 CoreGraphics 绘制图片的方式。在 iOS 9.0 以上,图片设置圆角不会触发离屏渲染。

iOS 阴影,圆角,避免离屏渲染iOS - 圆角的设计策略 - 及绘画出没有离屏渲染的圆角

  1. UITableViewCell 优化
  • Cell 复用;
  • 提前计算并缓存 Cell 的高度;
  • 减少 subviews 的个数和层级;
  • 少用 subviews 的透明图层;
  • 如果不是透明视图,背景色不要使用 clearColor;
  • 注意离屏渲染问题;
  • 图片提前在子线程异步解码,SDWebImage 已经实现;
  • 异步绘制(自定义 Cell 绘制)VVeboTableViewDemoYYAsyncLayerAsyncDisplayKit
  • 滑动时,按需加载。注意:这个会导致滑动时出现大量空白,不友好。
  • 尽量显示“大小刚好合适的”图片资源
  • 避免同步的从网络、文件获取数据

四、内存占用

主要检查工程中的内存泄露、循环引用的问题。

  1. instrument - Allocations 动态分析
  2. Analyze 静态分析
  3. 腾讯 MLeaksFinder
  4. 脸书 FBMemoryProfiler

说明:

  1. MLeaksFinder 效果比较好,能查找工程中出现循环引用的的场景,在慈云找到十几处循环引用。
  2. 官方的 instrument 工具效果一般,在慈云 app 只检测出了 3 处循环引用,2 处内存泄露;
  3. Analyze 静态分析可检测出“创建了但未被使用的”变量,需要结合代码逻辑,小心处理。
  4. 脸书的 FBMemoryProfiler 工具不大好用,主要是记录内存的开辟与销毁。

如果要更多的处理内存占用问题,需要分析工程中的代码逻辑,通过使用不同的存储方式、容器类、加载数据方式等,达到减少内存使用的目的。

五、缩小 ipa 包大小

爱奇艺移动应用优化之路:如何让崩溃率小于千分之二

安装包大小的优化,主要包含两大块:资源大小的优化和二进制大小的优化。

5.1 资源大小的优化

资源大小的优化主要包括以下几个方面:

  1. 资源压缩
  2. 未使用、重复资源的删除
  3. 资源上云

具体步骤:

  1. 资源压缩
    Xcode 的编译选项中,提供了 Compress PNG FilesRemove Text MetaData From PNG Files

但是由于 PNG 是无损压缩,经过 Xcode 压缩后的图片资源,依然很大。

使用 pngquant 对大多数的 32 位图进行了处理,将其转为 8 位图,并且使用 Zopfli 进行了压缩,这样整体的 PNG 图片资源大概被压缩了 70% 左右。这里要注意,由于一些渐变背景的颜色覆盖范围较大,转为 8 位图颜色丢失较大,表现效果会差很多,所以这些图片要谨慎处理。

  1. 未使用、重复资源的删除
  2. 资源上云
    资源上云可以有效减少包内资源,唯一要注意的是这些资源由于是 lazy load,所以比较适合层级较深的页面使用。

5.2 其他处理

  1. 配置编译选项 Generate Debug Symbols 设置为 NO;
  2. 舍弃架构,如:armv7,根据实际选择。
  3. 编译的版本必须是 release 版本,
  4. 查找内部使用到的第三方库,一方面可以进行删减代码,用不到的类,直接删除,还有第三方库中的图片资源统统删除掉,如果能够自己手写实现的,那费功夫自己写吧

六、引用库升级及替换

持续更新的第三方库也会对性能做出优化,在替换前,需先确定不会给项目引入问题。

七、更多优化

iOS 性能调试iOS视图成像理论及性能优化iOS性能优化系列篇之“列表流畅度优化”