iOS性能调优总结

iOS性能调优总结

性能调优的方式:

    1、通过专门的性能调优工具
    2、通过优化代码

1. 性能调优工具:

1.1 静态分析工具Analyze

相信IOS开发者在App进行Build或Archive时,会产生很多编译警告,这些警告是编译时产生的,静态分析的过程也类似,在XCode Product菜单下,点击Analyze对App进行静态分析。
Analyze主要分析以下四种问题:

1. 逻辑错误:访问空指针或未初始化的变量等;
2. 内存管理错误:如内存泄漏等;
3. 声明错误:从未使用过的变量;
4. Api调用错误:未包含使用的库和框架。
1.2 内存泄漏分析工具–Leaks

点击XCode的Product菜单Profile启动Instruments,使用Leaks开始动态分析。

选择Leaks,会自动启动Leaks工具和IOS模拟器,Leaks启动后会开始录制,随着对模拟器运行的App的操作,可以在Leaks中查看内存占用的情况。

注意:其中黑色头像代表了最可能的位置

注:如果你的项目使用了ARC,随着你的操作,不断开启或关闭视图,内存可能持续上升,但这不一定表示存在内存泄漏,ARC释放的时机是不固定的。

开启ARC后,内存泄漏的原因:

开启了ARC并不是就不会存在内存问题,苹果有句名言:ARC is only for NSObject。

在IOS 中使用malloc分配的内存,ARC是不会处理的,需要自己进行处理。

例子中的 CGImageRef 也是一个Image的指针,ARC也不会进行处理。
1.3 不合理内存分析工具–Allocation

关于内存的问题,除了内存泄漏以外,还可能存在内存不合理使用的情况,也会导致IOS内存警告。

内存的不合理使用往往比内存泄漏更难发现,内存泄漏可以更多借助于工具的判断,
而内存的不合理运用更多需要开发者结合代码、架构来进行分析。

明确说明一下两者的区别:

  • 内存泄漏:是指内存被分配了,但程序中已经没有指向该内存的指针,导致该内存无法被释放,产生内存泄漏。
  • 内存不合理运用:苹果官方称这种情况为Abandoned Memory,也就是存在已分配内存的引用,但实际上程序中不会使用,比如图片等对象加入了缓存,但缓存中的对象一直没有被使用。
  • XCode提供的Instruments中的Allocation工具可以用来帮你了解内存的分配情况,当你的App收到内存警告时,首先应该用Allocation进行内存分析,了解哪些对象占用了太多内存。
1.4 干掉僵尸对象 Zombies
  • 僵尸对象,也就是我们会遇到的EXC_BAD_ACCESS错误,由于内存已经被释放,而这个对象仍旧保留这那个坏地址而导致的。

  • 在MRC的开发中,这个错误比较常见,ARC下面在使用到C++的代码也会遇到。

  • 不过这个工具比较简单,遇到这类错误,打开这个位于Instruments下的工具,直接就能帮你定位到,这里就不赘述了。

【重点】1.5 性能提升工具 Time Profile

既然是性能调优,那么怎么提升代码的运行效率其实才是我们程序员最直接的诉求,而这个工具可以辅助我们办到这件事。

Time Profiler分析原理:

它按照固定的时间间隔来跟踪每一个线程的堆栈信息,通过统计比较时间间隔之间的堆栈状态,来推算某个方法执行了多久,
并获得一个近似值。它将各个方法消耗的时间统计起来,形成了我们直接定位需要进行优化的代码的好帮手。

选择Time Profiler工具开始测试,这时会自动启动模拟器和Time Profiler录制。
先进行一些App的操作,让Time Profiler收集足够的数据,尤其是你觉得那些有性能瓶颈的地方。

需要关注的:显示的堆栈查看;看到cpu运行的时间都消耗在哪里;通过对应用的操作,可以在详细面板中看到那些最耗时的操作是哪些图标为黑色头像的就是Time Profiler给我们的提示,有可能存在性能瓶颈的地方,可以逐渐向下展开,找到产生的根本原因。

Time Profiler参数设置

这里边几个选项的含义如下:

  • Separate by Thread:

    每个线程应该分开考虑。只有这样你才能揪出那些大量占用CPU的”重”线程

  • Invert Call Tree:

    从上倒下跟踪堆栈,这意味着你看到的表中的方法,将已从第0帧开始取样,这通常你是想要的,只有这样你才能看到CPU中耗费时间最深的方法.也就是说FuncA{FunB{FunC}} 勾选此项后堆栈以C->B-A 把调用层级最深的C显示在最外面

  • Hide Missing Symbols:

    如果dSYM无法找到你的app或者系统框架的话,那么表中看不到方法名只能看到十六进制的数值,如果勾线此项可以隐藏这些符号,便于简化数据

  • Hide System Libraries:

    勾选此项你会显示你app的代码,这是非常有用的. 因为通常你只关心cpu花在自己代码上的时间不是系统上的

  • Show Obj-C Only:

    只显示oc代码,如果你的程序是像OpenGl这样的程序,不要勾选侧向因为他有可能是C++的 Flatten Recursion: 递归函数, 每个堆栈跟踪一个条目 Top Functions: 一个函数花费的时间直接在该函数中的总和,以及在函数调用该函数所花费的时间的总时间。因此,如果函数A调用B,那么A的时间报告在A花费的时间加上B花费的时间,这非常真有用,因为它可以让你每次下到调用堆栈时挑最大的时间数字,归零在你最耗时的方法。
    上面的参数在实践中合理设置,也没有什么太多技巧,就是通过数据的隐藏、显示让我们更关注于想找到的数据。

2. 性能调优的代码优化:

下面介绍一下在开发中可以直接进行的代码优化的方面。
2.1 views设置为不透明(opaque=yes)

(opaque)这个属性给渲染系统提供了一个如何处理这个view的提示。如果设为YES, 渲染系统就认为这个view是完全不透明的,这使得渲染系统优化一些渲染过程和提高性能。

如果设置为NO,渲染系统正常地和其它内容组成这个View。默认值是YES。如果这个属性为NO,GPU会利用图层颜色合成公式去合成真正的色值,这在ScrollView或者动画中是很消耗性能的。

2.2 不要阻塞主线程

永远不要使主线程承担过多。因为UIKit在主线程上做所有工作,渲染,管理触摸反应,回应输入等都需要在它上面完成。

一直使用主线程的风险就是如果你的代码真的block了主线程,你的app会失去反应。
大部分阻碍主进程的情形是你的app在做一些牵涉到读写外部资源的I/O操作,比如存储或者网络。
通常建议这些操作都使用GCD的方式直接异步执行,并将UI相关操作在主线程进行回调,像这样:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 切换到全局队列,异步执行耗时操作 dispatch_async(dispatch_get_main_queue(), ^{
// 切换到主线程,更新你的UI。 }); });
2.3 提前调整ImageView中的图片大小(同图片和动画的渲染)

如果要在UIImageView中显示一个图片,你应保证图片的大小和UIImageView的大小相同。
因为在运行中缩放图片是很耗费资源的,特别是UIImageView嵌套在UIScrollView中的情况下。

如果图片是从远端服务加载的你不能控制图片大小,比如在下载前调整到合适大小的话,你可以在下载完成后,最好是用background thread,缩放一次,然后在UIImageView中使用缩放后的图片。
这个类比到图片和动画的渲染中,是通用的。
具体方法参考上面的GCD操作。

2.4 正确使用容器的特性

Arrays: 有序的一组值。使用index来查找很快,使用value 查找很慢, 插入/删除很慢。

Dictionaries: 存储键值对。 用键来查找比较快。

Sets: 无序的一组值。用值来查找很快,插入/删除很快。

2.5 大文件传输使用gzip

大量app依赖于远端资源和第三方API,你可能会开发一个需要从远端下载XML, JSON, HTML或者其它格式的app。
问题是我们的目标是移动设备,因此你就不能指望网络状况有多好。一个用户现在还在edge网络,下一分钟可能就切换到了3G。不论什么场景,你肯定不想让你的用户等太长时间。

减小文档的一个方式就是在服务端和你的app中打开gzip。这对于文字这种能有更高压缩率的数据来说会有更显著的效用。
当然,现在苹果已经自动支持了,你只需要告诉你们服务端的同学,传输大文件的时候记得用gzip就完了。

2.6 View的重用和懒加载

更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的app更是如此。

重用就是模仿UITableView和UICollectionView的操作: 不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。
当需要使用View的时候,去可重用队列里面找一找有没有可以被复用的View。

这里我的一份框架中曾经使用过类似的方法去创建一个图片浏览器,大家可以稍做参考。View的重用
懒加载就是在程序启动时并不进行加载,只有当用到这个对象的时候,才进行加载。
这个不仅在属性中可以进行这样的使用,在View上面也是一样,不过实现稍有不同。
懒加载会消耗更少内存,但是在View的显示上会稍有滞后。

2.7 Cache

一个极好的原则就是,缓存所需要的,也就是那些不大可能改变但是需要经常读取的东西。
我们能缓存些什么呢?一些选项是,远端服务器的响应,图片,甚至计算结果,比如UITableView的行高。
NSURLConnection默认会缓存资源在内存或者存储中根据它所加载的HTTP Headers。你甚至可以手动创建一个NSURLRequest然后使它只加载缓存的值。
下面是一个可用的代码段,你可以可以用它去为一个基本不会改变的图片创建一个NSURLRequest并缓存它:

+ (NSMutableURLRequest *)imageRequestWithURL:(NSURL *)url {
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
    // this will make sure the request always     returns the cached image
request.HTTPShouldHandleCookies = NO;
    request.HTTPShouldUsePipelining = YES;
    [request addValue:@"image/*" forHTTPHeaderField:@"Accept"];
    return request;
}

注意你可以通过 NSURLConnection 获取一个URL request, AFNetworking也一样的。这样你就不必为采用这条tip而改变所有的networking代码了。
如果你需要缓存其它不是HTTP Request的东西,你可以用NSCache。

2.8 记得处理内存警告

一旦系统内存过低,iOS会通知所有运行中app。在官方文档中是这样记述:

- 如果你的app收到了内存警告,它就需要尽可能释放更多的内存。最佳方式是移除对缓存,图片object和其他一些可以重创建的objects的strong references.

幸运的是,UIKit提供了几种收集低内存警告的方法:

在app delegate中使用applicationDidReceiveMemoryWarning:的方法在你的
自定义UIViewController的子类(subclass)中覆盖didReceiveMemoryWarning 注册并接收
UIApplicationDidReceiveMemoryWarningNotification 的通知
一旦收到这类通知,你就需要释放任何不必要的内存使用。

例如,UIViewController的默认行为是移除一些不可见的view, 它的一些子类则可以补充这个方法,删掉一些额外的数据结构。一个有图片缓存的app可以移除不在屏幕上显示的图片。
这样对内存警报的处理是很必要的,若不重视,你的app就可能被系统杀掉。
然而,当你一定要确认你所选择的object是可以被重现创建的来释放内存。一定要在开发中用模拟器中的内存提醒模拟去测试一下。

2.9 重用大的开销对象

这里的大开销是指一些初始化很慢的objects,如:NSDateFormatter和NSCalendar。但是,你又不可避免地需要使用它们,比如从JSON或者XML中解析数据。

想要避免使用这个对象的瓶颈你就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。
注意如果你要选择第二种方法,对象会在你的app运行时一直存在于内存中,和单例(singleton)很相似。

下面的代码说明了使用一个属性来延迟加载一个date formatter. 第一次调用时它会创建一个新的实例,以后的调用则将返回已经创建的实例:

// in your .h or inside a class extension
@property (nonatomic, strong) NSDateFormatter *formatter;
// inside the implementation (.m)
// When you need, just use self.formatter -
(NSDateFormatter *)formatter {
    if (! _formatter) {
        _formatter = [[NSDateFormatter alloc] init];
        _formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";// twitter date format
        }
    return _formatter;
}

还需要注意的是,其实设置一个NSDateFormatter的速度差不多是和创建新的一样慢的!所以如果你的app需要经常进行日期格式处理的话,你会从这个方法中得到不小的性能提升。

2.10 避免反复的处理数据

许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要。在内存中操作数据使它们满足你的数据结构是开销很大的。

比如你需要数据来展示一个tableview,最好直接从服务器取array结构的数据以避免额外的中间数据结构改变。
类似的,如果需要从特定key中取数据,那么就使用键值对的dictionary。

2.11 正确设定背景图片

在View里放背景图片就像很多其它iOS编程一样有很多方法:

  • 使用UIColor的 colorWithPatternImage来设置背景色;
  • 在view中添加一个UIImageView作为一个子View。
  • 如果你使用全画幅的背景图,你就必须使用UIImageView因为UIColor的colorWithPatternImage是用来创建小的重复的图片作为背景的。这种情形下使用UIImageView可以节约不少的内存:
// You could also achieve the same result in Interface Builder
UIImageView *backgroundView = [ [UIImageView alloc] initWithImage:[UIImage imageNamed:@"background"]];
[self.view addSubview:backgroundView];
  • 如果你用小图平铺来创建背景,你就需要用UIColor的colorWithPatternImage来做了,它会更快地渲染也不会花费很多内存:
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];
2.12 试试苹果最新的WKWebView来处理web

UIWebView很有用,用它来展示网页内容或者创建UIKit很难做到的动画效果是很简单的一件事。

但是你可能有注意到UIWebView并不像驱动Safari的那么快。这是由于以JIT compilation 为特色的Webkit的Nitro Engine的限制。
所以想要更高的性能你就要调整下你的HTML了。

第一件要做的事就是尽可能移除不必要的javascript,避免使用过大的框架。能只用原生js就更好了。另外,尽可能异步加载例如用户行为统计script这种不影响页面表达的javascript。最后,永远要注意你使用的图片,保证图片的符合你使用的大小。使用Sprite sheet提高加载速度和节约内存。

当然,上面是针对你在使用UIWebView的情况下,需要尽量减少使用web的特性,而苹果最近刚推出的Safari的底层框架WKWebView也许能帮我们规避掉很多这样的性能问题。

2.13 优化你的TableView

Table view需要有很好的滚动性能,不然用户会在滚动过程中发现动画的瑕疵。

为了保证tableview平滑滚动,确保你采取了以下的措施:

    正确使用reuseIdentifier来重用cells
    尽量使所有的view opaque,包括cell自身
    避免渐变,图片缩放,后台选人 缓存行高
    如果cell内现实的内容来自web,使用异步加载,
    缓存请求结果 使用shadowPath来画阴影
    减少subviews的数量
    尽量不适用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果
    使用正确的数据结构来存储数据 使用rowHeight, sectionFooterHeight 和 sectionHeaderHeight来设定固定的高,不要请求delegate
2.14 选择正确的数据存储方式

存储方式:

使用NSUerDefaults,使用XML, JSON,或者 plist 使用NSCoding存档,使用类似SQLite的本地SQL数据库 使用 Core Data
  • NSUserDefaults的问题是什么?

    虽然它很nice也很便捷,但是它只适用于小数据,比如一些简单的布尔型的设置选项,再大点你就要考虑其它方式了

  • XML这种结构化档案呢?

    总体来说,你需要读取整个文件到内存里去解析,这样是很不经济的。使用SAX又是一个很麻烦的事情。
    NSCoding?不幸的是,它也需要读写文件,所以也有以上问题。

  • 使用SQLite 或者CoreData比较好

    使用这些技术你用特定的查询语句就能只加载你需要的对象。

    在性能层面来讲,SQLite和Core Data是很相似的。他们的不同在于具体使用方法。Core Data代表一个对象的graphmodel,但SQLite就是一个DBMS。
    Apple在一般情况下建议使用Core Data,但是如果你有理由不使用它,那么就去使用更加底层的SQLite吧。
    如果你使用SQLite,你可以用FMDB(https://github.com/ccgus/fmdb)这个库来简化SQLite的操作,这样你就不用花很多经历了解SQLite的C API了。

2.15 把Xib换成Storyboard吧

当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。

Storyboards就是另一码事儿了,storyboard仅在需要时实例化一个view controller.

当加载XIB时,所有图片都被缓存,如果你在做OS X开发的话,声音文件也是。Apple在相关文档中的记述是:

当你加载一个引用了图片或者声音资源的nib时,nib加载代码会把图片和声音文件写进内存。
在OS X中,图片和声音资源被缓存在named cache中以便将来用到时获取。在iOS中,
仅图片资源会被存进named caches。取决于你所在的平台,使用NSImage 或UIImage 的`imageNamed:`方法来获取图片资源。

很明显,同样的事情也发生在storyboards中,但我并没有找到任何支持这个结论的文档。另外,快速打开app是很重要的,特别是用户第一次打开它时,对app来讲,第一印象太太太重要了。你能做的就是使它尽可能做更多的异步任务,比如加载远端或者数据库数据,解析数据。

还是那句话,避免过于庞大的XIB,因为他们是在主线程上加载的。所以尽量使用没有这个问题的Storyboards吧!注意,用Xcodedebug时watchdog并不运行,一定要把设备从Xcode断开来测试启动速度

2.16 学会手动创建Autorelease Pool

NSAutoreleasePool负责释放block中的autoreleased objects。一般情况下它会自动被UIKit调用。但是有些状况下你也需要手动去创建它。

假如你创建很多临时对象,你会发现内存一直在减少直到这些对象被release的时候。这是因为只有当UIKit用光了autorelease pool的时候memory才会被释放。

好消息是你可以在你自己的@autoreleasepool里创建临时的对象来避免这个行为:

NSArray *urls = [@"url1",@"url2"];
for (NSURL *url in urls){
    @autoreleasepool{
    NSError *error;
    NSString *fileContents = [NSString stringWithContentsOfURL: url encoding: NSUTF8StringEncoding error: &error];
    /* Process the string, creating and autoreleasing more objects. */
    }
}

这段代码在每次遍历后释放所有autorelease对象

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,830评论 25 707
  • 写在前面本文来自iOS Tutorial Team 的 Marcelo Fabri,他是Movile的一名 iOS...
    升级打怪啊怪阅读 597评论 1 6
  • 初学者性能提升 这个部分致力于一些能提高性能的基本改变。但所有层次的开发者都有可能会从这个记录了一些被忽视的项目的...
    Dream_Maker阅读 387评论 0 6
  • 背后的月亮惨淡极了——我,做了一个梦。 我闭上了眼,再而睁开眼,惨淡的月光不见了,这是一个未曾相见的世界,又一个似...
    秦北山阅读 558评论 8 1
  • 今天餐饮课的主题是创造美味,我在想是不是一个难吃的东西加一个好吃的东西,会不会特别好吃呢?一会儿助教阿姨们给我们,...
    贤惠的煎蛋饼阅读 351评论 0 1