iOS 性能调试

性能调优的方式:

 1、通过专门的性能调优工具

 2、通过代码优化

1. 性能调优工具:

下面针对iOS的性能调优工具进行一个介绍:

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释放的时机是不固定的。

Leaks顶部分为两栏:All Heap & Anonymous VM和Leaks Checks,All Heap & Anonymous VM中的曲线代表内存分配和内存泄漏曲线。

点击第二栏Leaks Checks展示内存泄漏,进行内存泄漏分析,将光标放置在上图的小红叉上可看到leak数量,右下方是leaks调试的选项:

 建议把Snapshot Interval间隔时间设置为10秒,勾选Automatic Snapshotting,Leaks会自动进行内存捕捉分析。

 在你怀疑有内存泄漏的操作前和操作后,可以点击Snapshot Now进行手动捕捉。

Leaked Object的表格中显示了内存泄漏的类型、数量及内存空间等。

点击具体的某个内存泄漏对象,在右侧Detail窗口中会出现导致泄漏可能的位置,其中黑色头像代表了最可能的位置。

Leaks已成功找出了友盟相关的函数:

 内存泄漏动态分析技巧

  熟练使用Leaks后会对内存泄漏判断更准确,在可能导致泄漏的操作里,多使用Snapshot Now手动捕捉。

  开始时如果设备性能较好,可以把自动捕捉间隔设置为5秒钟。

  使用ARC的项目,一般内存泄漏都是malloc、自定义结构、资源引起的,多注意这些地方进行分析。

开启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收集足够的数据,尤其是你觉得那些有性能瓶颈的地方。

  2是扩展面板,用来跟踪显示堆栈;

  3里面有设置和详情,可以从这里对录制做些配置,detail下查看到cpu运行的时间都消耗在哪里;

 

通过对应用的操作,可以在详细面板中看到那些最耗时的操作是哪些,并可以逐行展开查看:

图标为黑色头像的就是Time Profiler给我们的提示,有可能存在性能瓶颈的地方,可以逐渐向下展开,找到产生的根本原因。

Time Profiler参数设置

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

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

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

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

  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更是如此。

重用就是模仿UITableViewUICollectionView的操作: 不要一次创建所有的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中使用<code>applicationDidReceiveMemoryWarning:</code> 的方法 在你的自定义UIViewController的子类(subclass)中覆盖<code>didReceiveMemoryWarning</code> 注册并接收 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格式的数据。在服务器端和客户端使用相同的数据结构很重要。在内存中操作数据使它们满足你的数据结构是开销很大的。

比如你需要数据来展示一个table view,最好直接从服务器取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需要有很好的滚动性能,不然用户会在滚动过程中发现动画的瑕疵。

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

  正确使用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 或者 Core Data比较好。使用这些技术你用特定的查询语句就能只加载你需要的对象。

在性能层面来讲,SQLite和Core Data是很相似的。他们的不同在于具体使用方法。Core Data代表一个对象的graph model,但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吧!

  注意,用Xcode debug时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对象。

原文链接

posted @ 2016-10-11 11:50  张张_z  阅读(1301)  评论(0编辑  收藏  举报