转载

论iOS中的优化策略

前言

性能优化是永恒的话题,谁都想自己的程序如丝顺滑,而不是点开一个页面都卡顿几秒

关于优化

优化很重要,它确保了整个app能够以最高的效率来完成各项工作,但是在谈如何优化之前,必须注意一个问题,就是不要进行预优化。因为在需求以及工作尚未完成时就对某个模块进行优化,其代价是巨大而无用的,一方面是因为陆续增加的新功能可能会导致早前的优化毫无作用,另外可能因为过早的优化导致接下来的功能难以实现。

所以最好的优化时机,应该是在整个应用已经完成了绝大部分功能,需要对细节进行调整时再进行。同时优化可能意味着部分模块的重构与解耦,所以在实现功能的时候也要减少写出“坏味道”的代码。

优化的建议

在这里我会提出一部分针对iOS的优化建议,其中有部分建议可能是会使代码更复杂,可能会使我们花很多时间去应用到实例中,也有部分可能是一些我们被忽略的小细节,虽然不显眼但是会造成致命的问题。

善用调试工具

在Xcode的instrument里面提供给了我们大量的性能调优工具,我主要用到的工具会有:

  • Core Animation:界面渲染优化

  • Leaks:内存泄漏检测

  • Time Profiler:方法耗时检测

通过这些工具我们可以很直观的看到自己程序中出现的问题,让我们更方便的去定位问题,并针对这些问题进行解决。

减少图层混合

在程序中我们或多或少会使用到透明的控件,而我们的优化可以先从这些透明的控件抓起。首先我们需要知道透明意味着图层混合,而图层混合则意味着更多的渲染成本,比如我通过半透明红色的图层和半透明蓝色的图层叠加来获取一个紫色的图层,和我直接使用一个紫色的图层的性能代价是差别很大的。

所以进行优化的第一步是尽可能的减少透明控件的数量,另外更需要重视的是backgroundColor这一属性,在白色背景下clearColorwhiteColor可能没有视觉上的区别,但是在实际上app已经出现了图层混合,对性能造成了影响,所以最好给每一个控件都设置到backgroundColor

合理使用 reuserIdentifier

tableviewCell或者collectionViewCell添加reuseIdentifier应该成为习惯,这意味着每显示一个新的cell的时候不需要再去新建一个cell,而是先从已经存在的cell队列中选择一个来进行显示,这样能够保证性能的消耗是最小的,而已滑动时会十分顺畅。

同时不应该忽略UITableViewHeaderUITableViewFooter,他们也是可以设置reuseIdentifier的:

Objective-c

static NSString *identifier = @"headerViewReuseIdentifier";  
UITableViewHeaderFooterView *headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:identifier];  
if (headerView == nil) {  
   headerView = [[UITableViewHeaderFooterView alloc] initWithReuseIdentifier:@"myheaderView"];  
   // do something ...
    
}

Swift

let identifier = "headerViewReuseIdentifier"
if let headerview = tableView.dequeueReusableHeaderFooterView(withIdentifier: identifier) {
    // do something ...
}

警惕 heightForRow 方法

UITableView大概是我们使用得最多的控件了,所以针对它我们有许多的优化策略。就拿最基本的动态计算cell高度方法来说,看到下面这段代码时,可能会觉得写得还挺聪明的:

Objective-c

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return [tableView cellForRowAtIndexPath:indexPath].frame.size.height;
}

Swift

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return tableView.cellForRow(at: indexPath)?.frame.size.height
}

在需要获取cell高度的时候就把cell生成出来,然后获取高度并赋值,的确是一个美好的想法,但是在真正使用过程中,如果面对很多cell的情况下,我们会发现tableview的滑动变得极其不流畅,甚至出现卡死的情况,为什么会出现这个问题呢?

其实在tableview生成cell时是经历了这样的一个过程:先调用cell数量次数的heightForRowAtIndexPath方法,获取cell的高度,然后当需要显示cell到屏幕上时,就调用cellForRowAtIndexPath方法,生成足够显示的cell到复用队列,并显示出来。正是这样的机制确保的tableview的流畅滑动。但是当我们使用上面的方法来获取高度的时候,变相调用了cell数量次数的cellForRowAtIndexPath方法,假如有1000个cell就意味着调用1000次该方法,其中的花销可想而知。

正确的获取动态高度的方法有很多,比如可以在model里面声明对应cell的高度,或者使用estimateHeight等方法来动态设置高度,这里就不一一举例了。

所以在优化UITableView的时候,可以从heightForRowAtIndexPath这个方法入手,看看是否有一些耗时的方法在该方法中执行,导致整个tableview被拖慢。

masksToBounds & clipsToBounds

  • clipsToBounds(UIView): 是指视图上的子视图,如果超出父视图的部分就截取掉

  • masksToBounds(CALayer): 是指视图的图层上的子视图,如果超出父图层的部分就截取掉

而clipsToBounds方法执行的时候,会调用自己图层的maskToBounds方法。而maskToBounds方法的调用,则会引发离屏渲染。

so?

首先我们先来认识一下“离屏渲染”究竟是什么。

普通渲染时,当OpenGl提交命令后,GPU便开始进行渲染,渲染完毕后就把结果放入Render Buffer中。但对于一些复杂的图案,它需要分布渲染再组装起来,比如UIImageView中,OpenGL提交命令后,GPU由上至下开始渲染其中的子控件,并在渲染完毕后暂时保存起来,只有当最后一个子控件渲染完毕后,再将所有的渲染结果取出并合并,再放入Render Buffer中。

所以离屏渲染比普通渲染耗费更多的资源

举个例子,很多人喜欢设置圆角,因为它更加好看,设置圆角本来是很简单的一件事:

Obejctive-c

[self.view.layer setCornerRadius:10];

Swift

view.layer.cornerRadius = 10

这样设置下我们可以看到是没有任何的性能耗损,也没有出现离屏渲染的情况。

但是在有一些情况下,不正确的设置圆角 = 性能耗损。在很多时候,我们设置时需要带上masksToBounds或者是clipsToBounds方法,因为UILabelUIImageView等等内部还有子视图的控件在我们单单设置cornerRadius时没有任何反应,一定要加上masksToBoundsclipsToBounds才能显示圆角。而一旦调用了这两个方法,在圆角数量很多(> 30)个时,就会出现严重的性能耗损,因为他们都发生了离屏渲染。

警惕图片缩放

我们当然都知道不可以在主线程上做所有工作,应当让它只响应渲染、响应、输入、更新UI等操作。但是在某些时候我们可能会“无意识”的阻塞的了主线程,比如当我们从后台下载一系列的图片,并加载到UITableViewCell中的UIImageView的时候,就可能会出现性能问题。

因为当我们后台获取的图片有可能并不能完成的适应我们的UIImageView,这时候就需要对这个图片进行缩放,而运行中缩放图片是很耗费资源的,这就会导致主线程被阻塞了。

所以我们可以在下载完成后,在后台对其进行缩放到适合大小,再用UIImageView加载缩放后的图片。

Objective-C

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
    // in background  
   //MARK: 1. resize image
   //MARK: 2. [imageView setImage:resizedImage] 
});

swift

DispatchQueue.global().async {
    //in background
    //MARK: 1. resize image
    //MARK: 2. imageView.image = resizedImage                
}

处理内存警告

很多时候我们会忽略UIViewController中的didReceiverMemoryWarning方法,其实这是一个十分有必要认真对待的方法。当系统内存过低的的时候,iOS会通知所有运行中的app,这时app就需要尽可能释放更多的内存,这时UIViewController会默认移除一些不可见的view,Appdelegate会移除缓存等等。如果不重视这些,app就很可能被系统kill掉,造成闪退。

Lazy Load

假如现在有一个按钮,当用户点击这个按钮的时候,需要显示一个聊天对话框,对话框里又可以选择点击其他按钮来显示更多的页面,这时候我们应该怎么处理?

我推荐的做法是先创建并渲染最重要的页面,而把另一些不太重要页面在其他线程中创建,当需要显示这些页面的时候在对其进行渲染。另一种方法时只创建并渲染最重要的页面,而在需要显示到其他页面的时候再创建并渲染这些页面。其实两种方法有利有弊,第一种方法响应速度会优秀很多,但是相对的对内存消耗也会更多,而第二种方法则消耗更少内存,但响应速度会相对较慢。

再谈优化

在看完这些优化方法之后,其实有一个需要权衡的问题:

我究竟需不需要这样优化?

我究竟有没有必要为了把帧率提升1-2点,而放弃cornerRadius方法,而去选择使用CALayer去画圆角,或者有没有必要去把每一个有可能复用的View都创建一个重用队列来确保性能的最优化呢?

我认为没有这样的必要,真正的优化应该是基于实现难度与用户需求的,比如上文提到的聊天窗口加载问题,我完全可以仅仅加载最重要的页面,而把什么发送图片的页面、好友详细信息的页面等到需要显示的时候再去加载,因为这些页面可能使用的频率很低,或者是用户能够承受得了这一个等待的时间,相反,用户可能更注重的是能够在点击会话列表后就马上能进入聊天界面的这一个反馈,而不是在进入聊天界面之后点击其他按钮的反馈有多迅速。

所以在面对优化这一个问题的时候,先思考再去写代码,不要为了优化而优化。

正文到此结束
Loading...