iOS开发 之 性能优化(翻译版)

本文源自这里, Demo代码参考这里的PerformanceOptimizationDemo

引言

终于来到了这里 -- iOS的性能优化

为什么要用"终于"呢? 对于我自己来说, 总算是入了iOS开发的门了

入门与iOS性能优化有关系么? 对于我自己来说, 有关系! 这意味着已经跨过只是熟悉某某开发的坎了, 因为优化的前提就是要更深入的理解

目录

  • 警示言

  • 小技巧

    • 图片, 图片, 还是图片

    • 尽量使view.opaque = YES

  • 缓存

    • imageNamed与imageWithContentsOfFile之争

    • 复用UITableViewCell

  • 策略

    • 延迟加载

    • 不要阻塞主线程

    • 避免重复处理

  • 通信

    • 配置GZIP

    • 通讯录的思考

  • 高阶

    • 使用Autorelease Pool

    • 处理内存警告

警示言

每次开始优化前, 我们需要把下面的金玉良言朗读10遍

不要养成"预优化"代码的错误习惯

测量哪一部分才是你代码中的瓶颈

小技巧

图片, 图片, 还是图片

为什么要先说图片呢? 因为实际开发中, 更耗资源的通常就是它了

小些的图片几KB, 大点的大到几百KB, 甚至还有上MB的, 对于移动设备来说, 不管是CPU还是内存都会被疯狂占用

我们要及时清理不用的图片, 推荐一个开源工具unused

它的使用非常简单, 下载完unused工程后运行即可, 设置好要检测的项目路径, 点击search开始

performance_optimization_01.png

BUT! 这个工具其实是有限制的, 因为unused的原理是这样

  • Step1: 使用find命令通过扩展名查找图片资源

  • Step2: 找到图片资源后, 通过grep在代码中检索, 检索不到则说明该图片资源unused

这个方式无法解决图片资源名是通过编码方式设置的情况, 所以会有误检的问题

其实保证良好的编码规范和代码审查的话, 上述问题也是可以被规避的

而下面这个关于图片资源的问题则是需要做额外检查的, 那就是图片压缩, 这里推荐一个开源工具imageoptim

其他不多说, 直接看下效果如何吧(我们从百度图片里随意找了10张图片, 总大小为2.8MB)

performance_optimization_02.png

在保证质量的前提下, 图片大小减少了208KB, 图片体积减少约7%, 效果很可观吧!

尽量使view.opaque = YES

你或许在想, 我怎么好像没设置过这个值啊? 没太大印象嘛!

这就对了, 因为这个是默认就是YES的

The default value of this property is YES

以下是Apple Documentation对该值的描述

If set to YES, the drawing system treats the view as fully opaque, which allows the drawing system to optimize some drawing operations and improve performance

在opaque = YES的情况下, 系统在绘制图形的时候就可以做一些优化

缓存

imageNamed与imageWithContentsOfFile之争

本节讨论的完整代码在这里

常见的加载UIImage的方法有两种

  • 方法1: imageNamed
+ (nullable UIImage *)imageNamed:(NSString *)name;      // load from main bundle
  • 方法2: imageWithContentsOfFile
+ (nullable UIImage *)imageWithContentsOfFile:(NSString *)path;

我们来看下这两种方法的实际效果如何(测试条件基于Xcode7.2, iPhone6, 用两种方法重复加载相同的40张图片10遍, 图片总大小约为7.7M)

  • 方法1: imageNamed调用前后内存占用分别为4.5M和7.4M, 耗时0.222266s

  • 方法2: imageWithContentsOfFile调用前后内存占用分别为4.5M和5.1M, 耗时0.349250s

可以看出

  • 方法1: imageNamed更占内存, 但加载同样的图片耗时更短

  • 方法1: imageWithContentsOfFile占用内存较低, 但加载同样的图片的耗时更长

为什么会这样呢? 这是因为imageNamed是基于缓存的

This method looks in the system caches for an image object with the specified name and returns that object if it exists. If a matching image object is not already in the cache, this method locates and loads the image data from disk or asset catalog, and then returns the resulting object.

而imageWithContentsOfFile是每次都要重新加载图片的

This method does not cache the image object

所以对于图片文件较小且使用较频繁时, 建议使用方法1: imageNamed来加载图片

复用UITableViewCell

为了文章的结构清晰, 这里将复用也放在了缓存这一节里, 那为什么复用UITableViewCell也可以算是缓存呢?

因为复用UITableViewCell就是基于缓存构建好的cell来实现的, 这样就不用每次都构建新的cell, 大大提高了运行效率(分配内存构建实例是耗时操作)

同时, 复用缓存的cell还可以大大降低所需构建的cell数量, 从而降低了资源的占用

复用UITableViewCell的典型写法如下

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *identifier = @"reuserIndentifier";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }

    return cell;
}

如果想深入了解UITableViewCell的复用机制, 大家可以看看这篇的问题1

策略

延迟加载

延迟加载在iOS开发中又叫懒加载(Lazy Load), 即在运行时真正需要一些资源时,再去加载这些资源

Objective-C原生支持Lazy Load机制: 属性的getter方法

我们来看下知名的开源库AFNetworking对Lazy Load的运用

// AFHTTPRequestOperation.h
@interface AFHTTPRequestOperation : AFURLConnectionOperation
@property (readonly, nonatomic, strong) id responseObject;
@end

// AFHTTPRequestOperation.m
@implementation AFHTTPRequestOperation
- (id)responseObject {
    [self.lock lock];
    if (!_responseObject && [self isFinished] && !self.error) {
        NSError *error = nil;
        self.responseObject = [self.responseSerializer responseObjectForResponse:self.response data:self.responseData error:&error];
        if (error) {
            self.responseSerializationError = error;
        }
    }
    [self.lock unlock];

    return _responseObject;
}
@end

其实延迟加载也体现在UI体验上, 如果想要提高App的启动速度, 那么可以延迟用户感知较弱的资源的加载

不要阻塞主线程

这点对于iOS, Android或是其他移动平台的开发者来说, 都是必须要遵守的

因为Main Thread是UI线程, 更新UI和响应用户事件的操作都是运行在Main Thread的

所以一些耗时的非UI操作, 如网络请求, 数据结构的更新等都必须在子线程中进行

大家经常使用的GCD接口, 常见的用法是这样的

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // switch to a background thread and perform your expensive operation
 
    dispatch_async(dispatch_get_main_queue(), ^{    
        // switch back to the main thread to update your UI
    });
});

其实对于这一点, 大多数开发者都已经能意识到, 但是我们会遇到一些该问题的变种

我们来看看常见的IM功能的需求: 账号长时间没有登录过, 登录后将一次性收到大量消息, 如1000条消息

这时候如果每获取到1条消息就更新UI, 那么Main Thread的负担会变得非常重, 因为更新UI的操作在且只能在Main Thread中进行

那么如何优化这种情况呢?

我们的策略就是在子线程中处理消息, 在每处理完N条消息后, 再通知Main Thread更新UI

这里N的取值是根据UI体验和经验来设置的, 因为不管性能也好UI体验也好都是为了提升产品和用户的体验

除了上述的变种问题, 还有一种误用多线程而导致的主线程阻塞问题

对于该问题的讨论, 大家可以看这篇的问题3

避免重复处理

这里的避免重复处理算是DRY(Don't Repeat Yourself)原则的延伸

例如许多应用需要从服务器加载数据, 这些数据通常通过JSON或者XML格式传输

如果客户端使用的数据结构是Dictionary, 而服务器发送的JSON数据结构是Array, 那么就需要额外的数据结构转换, 白白浪费了资源: CPU耗时和内存

我们再看一个多人电话会议的例子(不了解的可以参考阿里旗下钉钉的多人通话功能)

会议管理员在终端上通过HTTP请求服务器结束会议, 然后服务器将推送通知至所有参会人员的终端设备上, 参会人员终端在接受到服务器推送的通知后, 结束该会议

这里就有一个问题, 会议管理员即结束会议的这个终端, 是取得HTTP请求服务器的响应还是和其他普通参会人员一样在接受到服务器通知才结束会议呢?

没错! 应该在HTTP请求之后就做出响应, 为什么呢?

因为HTTP请求和服务器推送的通知是重复处理, 它们都是服务器在告诉会议管理员终端: 会议结束了

这样不仅避免了重复处理, 还可以减少终端和服务器的一次交互

通信

配置GZIP

客户端经常要从服务端接收JSON或者XML格式的数据

如果可以降低数据流量的话, 不仅可以帮助用户省钱, 还可以提升用户体验

办法就是对数据进行压缩(怎么看起来这么熟悉, 没错! 前面讨论的图片压缩也是相同的目的)

在服务器支持GZIP压缩的前提下, 常用的第三方网络库AFNetworking这样配置GZIP

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer = [AFJSONRequestSerializer new];
[manager.requestSerializer setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];

当然, 如果你懒得自己加的话, 也可以使用AFNetworking自己的扩展AFgzipRequestSerializer

通讯录的思考

对于一些基于IM服务的应用来说, 通讯录是一个基本的功能模块

我们假设通讯录有1W个联系人, 并且采用完全加载方案(即一次下载完所有的通讯录)

那么性能问题就会随之而来

  • 问题1: 同步1W个联系人的耗时

  • 问题2: 检查, 更新或选择联系人时的耗时

我们先来看下第一个问题的解决思路, 这里的通讯录同步采用的是一个第三方开源方案Funambol, 该方案基于SyncML协议

该方案要同步完1W个联系人并更新UI和操作数据库, 耗时约在15分钟左右, 而需求要求在5分钟内完成

最终我们采取了一种"重服务器"的方案, 即在第一次同步的时候, 跳过之前的同步方案, 由服务器准备好所有联系人的数据, 按照客户端db的设计, 准备好db文件发送客户端, 这样就省去了客户端的同步和数据库操作

这个解决方法其实也是"避免重复处理"的延伸, 将需要大量运算的操作交由服务器

接下来我们看看第二个问题, 这里解决方案可以算是"缓存"机制的延伸

即将所有的联系人数据放至内存中, 用方便检索和查找的数据结构配合查找算法, 来提高检索和操作的速度

第二个问题的解决方案是有限制的, 因为它的内存占用量更大, 可以说是顾此失彼, 大家可以考虑下是否有更好的优化方案, 欢迎拍砖

高阶

使用Autorelease Pool

autoreleasepool我用过么? 当然! Main Thread默认就是使用它的, 不信你可以查看main.m文件

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

那么autoreleasepool到底有什么用呢? 我们来做个试验(试验环境基于Xcode7.2 Simulator9.2, 试验工程的代码在这里)

运行工程后, 查看内存的占用量约为26.6MB

运行不使用autoreleasepool的test case

- (void)normalButtonClicked:(UIButton *)button {
    for (NSInteger i = 0; i < count; i++) {
        NSError *error = nil;
        NSString *fileContents = [NSString stringWithContentsOfURL:[NSURL URLWithString:@"http://www.baidu.com"] encoding:NSUTF8StringEncoding error:&error];
        NSLog(@"fileContents = [%@]", fileContents);
    }
    NSLog(@"end");
}

断点在打印"end"语句的这行, 执行完该test case后, 查看此时内存的占用量约为58.6MB

重新运行该工程, 接着运行使用autoreleasepool的test case, 来对比下结果

- (void)autoreleaseButtonClicked:(UIButton *)button {
    for (NSInteger i = 0; i < count; i++) {
        @autoreleasepool {
            NSError *error = nil;
            NSString *fileContents = [NSString stringWithContentsOfURL:[NSURL URLWithString:@"http://www.baidu.com"] encoding:NSUTF8StringEncoding error:&error];
            NSLog(@"fileContents = [%@]", fileContents);
        }
    }
    NSLog(@"end");
}

断点在打印"end"语句的这行, 执行完该test case后, 查看此时内存的占用量约为40.4MB

上述现象体现了ARC中对象的autorelease机制, autorelease是一种延迟释放的缓存机制, 这些对象会被添加到最近的一个autoreleasepool中, 在适当的时机(每一次Runloop结束)才真正release

所以在频繁申请和释放对象的时候, 且在没有优化方案的情况下, 我们需要使用autoreleasepool以避免占用内存峰值过高

最后小结一下, 如下两种情况下需要使用autoreleasepool, Apple Documentation的权威说明如下

Each thread in a Cocoa application maintains its own stack of autorelease pool blocks. If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.

If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks.

处理内存警告

当设备内存低时, 将会发送内存警告给所有运行的App, App可以通过如下方法获得该警告

  • applicationDidReceiveMemoryWarning in AppDelegate

  • didReceiveMemoryWarning in UIViewController

  • notification of UIApplicationDidReceiveMemoryWarningNotification

UIViewController默认会做内存优化处理: 移除一些不可见的view, 删掉一些额外的数据结构, 没用使用的图片缓存

为了进一步优化, 我们需要override该方法, 做如下操作

Remove strong references to caches, image objects, and other data objects that can be recreated later.

即使如此, 优化的幅度还是有限的, 所以当发生内存警告时, 我们应该考虑优化全局以降低峰值内存的占用量

如何优化全局? 额... 推荐你重新读一遍本文

小结

iOS性能优化是一个广阔的课题, 如果你有更多更好的方法, 欢迎一起讨论

最后, 祝大家的iOS开发之旅继续轻松+愉快

参考

更多文章, 请支持我的个人博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • Swift版本点击这里欢迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh阅读 24,864评论 7 249
  • 一、如何提高一个应用程序的性能?1、使用ARC减少内存失误,dealloc需要重写并对属性置nil。2、重用。3、...
    金歌漫舞阅读 936评论 2 6
  • 1. 用ARC管理内存 ARC(Automatic ReferenceCounting, 自动引用计数)和iOS5...
    乌七猫阅读 271评论 0 0
  • 我们都有爱的情愿 但我们沒有坚持爱下去的勇气和力量。 我们都希望被爱多于爱人, 我们总在得失取舍中寻找平衡, 其实...
    nxpb阅读 233评论 0 0
  • 这孩子是我的侄子小老虎,我同学说一般叫小老虎的孩子一定是机灵可爱的,说对了,我的这个小侄子从小就聪明伶俐,好学爱问...
    舒澜小筑阅读 613评论 0 1