iOS开发之性能优化全解析

网上有很多优化iOS性能的文章,读后受到了很多启发,在这里进行一下总结并加入一些自己的感悟以备后续回忆,如果恰巧在某个点能帮到后续阅读的你就最好不过了^O(∩_∩)O哈哈~

1、ARC内存的管理

在我们进行视图的优化前我们首先要保证的就是内存的管理,现在我们开发基本都是使用的ARC进行内存管理的,大部分情况下我们是没必要刻意关心某个对象何时释放,ARC会自动为你管理retain和release的过程。但是有几个点还是需要特别注意下的。

1>循环引用

当两个不同的对象各有一个强引用指向对方或者多个对象间形成强引用的闭环,那么循环引用便产生了。
iOS开发之性能优化全解析

我们重点需要关注的几类循环引用有:block、delegate、NSTime。

Block

在 ARC 中,在被拷贝的 block 中无论是直接引用 self 还是通过引用 self 的成员变量间接引用 self,该 block 都会 retain self。
如果某类将block作为自己的属性变量如:

@property (nonatomic, copy) TestCircleBlock testCircleBlock;

那为避免block中使用该类而造成循环引用,我们可以这样:

__weak typeof(self) weakSelf = self;
 self.testObject.testCircleBlock = ^{
      __strong typeof (weakSelf) strongSelf = weakSelf;
      [strongSelf doSomething];
};
delegate

对于代理的的属性定义,需要以弱引用来打破循环如:

@property (nonatomic, weak) id <TestDelegate> delegate;

假如我们是写的strong,那么 两个类之间调用代理就是这样的啦

BViewController *bViewController = [[BViewController alloc] init];
bViewController.delegate = self; //假设 self 是AViewController
[self.navigationController pushViewController:bViewController animated:YES];

/**
 假如是 strong 的情况
    bViewController.delegate ===> AViewController (也就是 A 的引用计数 + 1)
    AViewController 本身又是引用了 <BViewControllerDelegate> ===> delegate 引用计数 + 1
 导致: AViewController <======> Delegate ,也就循环引用啦
 */
NSTime

NSTimer 其实相对来说,我们其实是很容易忽略它这种情况的,毕竟还是很特殊的。
iOS开发之性能优化全解析

scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:

方法的最后一个参数为YES时,NSTimer会保留目标对象,等到自身失效才释放.执行完任务后,一次性的定时器会自动失效;重复性的定时器,需要主动调用invalidate方法才会失效.
或许你想在dealloc方法中使定时器失效,那你就太天真了.此时定时器还保留着当前控制器,此方法是不可能调用的,因此会出现内存泄漏.
这里采用block块的方法为NSTimer增加一个分类,具体细节看代码(程序员最好的语言是代码)。

//.h文件
#import <Foundation/Foundation.h>

@interface NSTimer (SGLUnRetain)
+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval
                                        repeats:(BOOL)repeats
                                          block:(void(^)(NSTimer *timer))block;
@end

//.m文件
#import "NSTimer+SGLUnRetain.h"

@implementation NSTimer (SGLUnRetain)

+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
    
    return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(sgl_blcokInvoke:) userInfo:[block copy] repeats:repeats];
}

+ (void)sgl_blcokInvoke:(NSTimer *)timer {
    
    void (^block)(NSTimer *timer) = timer.userInfo;
    
    if (block) {
        block(timer);
    }
}
@end

//控制器.m

#import "ViewController.h"
#import "NSTimer+SGLUnRetain.h"

//定义了一个__weak的self_weak_变量
#define weakifySelf  \
__weak __typeof(&*self)weakSelf = self;

//局域定义了一个__strong的self指针指向self_weak
#define strongifySelf \
__strong __typeof(&*weakSelf)self = weakSelf;

@interface ViewController ()

@property(nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block NSInteger i = 0;
    weakifySelf
    self.timer = [NSTimer sgl_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
        strongifySelf
        [self p_doSomething];
        NSLog(@"----------------");
        if (i++ > 10) {
            [timer invalidate];
        }
    }];
}

- (void)p_doSomething {
    
}

- (void)dealloc {
      // 务必在当前线程调用invalidate方法,使得Runloop释放对timer的强引用(具体请参阅官方文档)
     [self.timer invalidate];
}
@end

在使用中,最需要注意的就是下面这段代码:

weakifySelf
self.timer = [NSTimer sgl_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
       strongifySelf
       [self p_doSomething];
       NSLog(@"----------------");
       if (i++ > 10) {
           [timer invalidate];
       }
   }];
2>修饰属性变量类型

strong,weak,retain,assign,copy我们经常使用到的类型的区别是什么呢?如何正确使用减少程序出错也是很关键的。

  • assign: 简单赋值,不更改索引计数(Reference Counting)对基础数据类
  • copy:内容拷贝,其实是建立了一个相同的对象
  • retain:释放旧的对象,将旧对象的值赋予输入对象,再提高输入对象的索引计数为1
  • assign:简单赋值,不更改索引计数(Reference Counting)对基础数据类

weak 和 strong 属性只有在你打开ARC时才会被要求使用,这时你是不能使用retain release autorelease 操作的,因为ARC会自动为你做好这些操作,但是你需要在对象属性上使用weak 和strong,其中strong就相当于retain属性,而weak相当于assign。

不过在声明Block时,使用strong和retain会有截然不同的效果。strong会等于copy,而retain竟然等于assign!当然定义Block最好还是应该用copy。

以上是基本使用介绍,几个需要特别注意的点大致如下:

weak和assign的区别
  • weak只可以修饰对象。如果修饰基本数据类型,编译器会报错
  • weak 不会产生野指针问题。因为weak修饰的对象释放后(引用计数器值为0),指针会自动被置nil,之后再向该对象发消息也不会崩溃。 weak是安全的。
  • assign 可修饰对象,和基本数据类型
  • assign 如果修饰对象,会产生野指针问题;如果修饰基本数据类型则是安全的。修饰的对象释放后,指针不会自动被置空,此时向对象发消息会崩溃。
assign 适用于基本数据类型如int,float,struct等值类型,不适用于引用类型。因为值类型会被放入栈中,遵循先进后出原则,由系统负责管理栈内存。而引用类型会被放入堆中,需要我们自己手动管理内存或通过ARC管理。
weak 适用于delegate和block等引用类型,不会导致野指针问题,也不会循环引用,非常安全。
copy 关键字
  • NSString、NSArray、NSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;
  • block 也经常使用 copy 关键字
非ARC下不copy的Block会在栈中,ARC中的Block都会在堆上的
@property (copy) NSMutableArray *array;

特别注意如上写法会出现问题,可变类型不能使用copy,因为 copy 就是复制一个不可变 NSArray 的对象。

2、界面的优化

首先分析一下界面卡顿出现的主要原因:
iOS开发之性能优化全解析
在 iOS 系统中,图像内容展示到屏幕的过程需要 CPU 和 GPU 共同参与。CPU 负责计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。之后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

因此,我们需要平衡 CPU 和 GPU 的负荷避免一方超负荷运算。为了做到这一点,我们首先得了解 CPU 和 GPU 各自负责哪些内容。
iOS开发之性能优化全解析
那明白这些原理后,优化程序要遵循的两个基本原则:

  • 1、减少CPU和GPU负载
  • 2、均衡使用CPU和GPU
1>CPU 消耗型任务
布局计算:

布局计算是 iOS 中最为常见的消耗 CPU 资源的地方,如果视图层级关系比较复杂,计算出所有图层的布局信息就会消耗一部分时间。因此我们应该尽量提前计算好布局信息,然后在合适的时机调整对应的属性。还要避免不必要的更新,只在真正发生了布局改变时再更新。

对象创建:

对象创建过程伴随着内存分配、属性设置、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如 CALayer 比 UIView 要轻量许多,如果视图元素不需要响应触摸事件,用 CALayer 会更加合适。

一些objects的初始化很慢,比如NSDateFormatter和NSCalendar。然而,你又不可避免地需要使用它们,比如从JSON或者XML中解析数据。
想要避免使用这个对象的瓶颈你就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。
Autolayout:

对于复杂视图来说常常会产生严重的性能问题

文本计算:

如果一个界面中包含大量文本(比如微博、微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。

一个比较常见的场景是在 UITableView 中,heightForRowAtIndexPath这个方法会被频繁调用,即使不是耗时的计算在调用次数多了之后也会带来性能损耗。这里的优化就是尽量避免每次都重新进行文本的行高计算,可以在获取到 Model 数据后就根据文本内容计算好布局信息,然后将这份布局信息作为一个属性保存到对应的 Model 中,这样在 UITableView 的回调中就可以直接使用 Model 中的属性,减少了文本的计算。

文本渲染:

屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版、绘制为 Bitmap 显示的。常见的文本控件 (UILabel、UITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。

图像的绘制:

图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示的过程。前面的模块图里介绍了 CoreGraphic 是作用在 CPU 之上的,因此调用 CG 开头的方法消耗的是 CPU 资源。

2>GPU 消耗型任务
大量几何结构:

所有的 Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture。不论是提交到显存的过程,还是 GPU 调整和渲染 Texture 的过程,都要消耗不少 GPU 资源。当在较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU 占用率很低,GPU 占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。

另外当图片过大,超过 GPU 的最大纹理尺寸时,图片需要先由 CPU 进行预处理,这对 CPU 和 GPU 都会带来额外的资源消耗。

如果要在UIImageView中显示一个来自bundle的图片,你应保证图片的大小和UIImageView的大小相同。在运行中缩放图片是很耗费资源的,特别是UIImageView嵌套在UIScrollView中的情况下。
如果图片是从远端服务加载的你不能控制图片大小,比如在下载前调整到合适大小的话,你可以在下载完成后,最好是用background thread,缩放一次,然后在UIImageView中使用缩放后的图片。
视图的混合:

当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并且减少不必要的透明视图。

离屏渲染:

由以上可以看出离屏渲染需要重新开辟新的缓存空间,必定要更加消耗资源。
通过查资料目前知道了设置了以下属性时,都会触发离屏绘制:
shouldRasterize(光栅化)
masks(遮罩)
shadows(阴影)
edge antialiasing(抗锯齿)
group opacity(不透明)
复杂形状设置圆角等
渐变

这里会以圆角为例讲述下如何避免离屏渲染,具体如下:

单独设置layer.cornerRadius = 10;是不会触发离屏渲染的,如果再结合layer.masksToBounds = YES,便会触发离屏渲染。避免离屏渲染我目前能想到的有两种思路

  • 对控件关联的主图层进行裁剪
  • 为控件添加一个遮罩(非layer.mask)

首先看第一种思路的代码

- (void)bp_setRaidus:(CGFloat)radius view:(UIView *)view {
    UIImage *image = nil;
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, [UIScreen mainScreen].scale);
    CGContextRef currnetContext = UIGraphicsGetCurrentContext();
    [[UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:radius] addClip];
    [view.layer renderInContext:currnetContext];
    image = UIGraphicsGetImageFromCurrentImageContext();
    view.layer.contents = (__bridge id _Nullable)(image.CGImage);
    UIGraphicsEndImageContext();
}

代码的核心在于UIBezierPath 的 addClip方法,该方法的作用是在当前上下文环境中让闭合路径区域可视化,外部区域不可视。然后再把layer在上下文环境中渲染成一张图片,最后设置到layer.contents中。

第二种思路就是为控件添加一个圆角矩形的遮罩,这个遮罩可以是UIView、也可以是CALayer,前者是控件,后者图层。这种思路是这样的,使用抑或运算,先画一个大的矩形在上下文中,再在里面绘制一个圆角矩形,对上下文路径进行抑或就可以了,代码如下:

-(void)bp_setRaidus:(CGFloat)radius view:(UIView *)view backgroundColor:(UIColor *)color {
    CALayer *layer = [CALayer layer];
    layer.frame = view.bounds;
    [view.layer addSublayer:layer];
    
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, [UIScreen mainScreen].scale);
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:layer.bounds cornerRadius:radius];
    UIBezierPath *path1 = [UIBezierPath bezierPathWithRect: layer.bounds];
    [color setFill];
    [path fill];
    [path1 fillWithBlendMode:kCGBlendModeXOR alpha:1];
    layer.contents = (__bridge id)(UIGraphicsGetImageFromCurrentImageContext().CGImage);
    UIGraphicsEndImageContext();
}
3>Table View的重点优化

上述是一些通用的优化,在iOS很多控件里最有可能被用来处理展示大量数据的就是Table View,下面重点列举下一些优化注意事项:

  • 正确使用reuseIdentifier来重用cells
  • 尽量使所有的view opaque,包括cell自身
  • 避免渐变,图片缩放,后台选人
  • 缓存行高
  • 如果cell内现实的内容来自web,使用异步加载,缓存请求结果

使用shadowPath来画阴影

  • 减少subviews的数量
  • 尽量不使用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果
  • 使用正确的数据结构来存储数据
  • 使用rowHeight, sectionFooterHeight和 sectionHeaderHeight来设定固定的高,不要请求delegate
4>选择是否缓存图片

常见的从bundle中加载图片的方式有两种,一个是用imageNamed,二是用imageWithContentsOfFile,第一种比较常见一点。

既然有两种类似的方法来实现相同的目的,那么他们之间的差别是什么呢?
imageNamed的优点是当加载时会缓存图片。imageNamed的文档中这么说:这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象如果它存在的话。如果缓存中没有找到相应的图片,这个方法从指定的文档中加载然后缓存并返回这个对象。

相反的,imageWithContentsOfFile仅加载图片。

下面的代码说明了这两种方法的用法:

UIImage *img = [UIImage imageNamed:@"myImage"];// caching
// or
UIImage *img = [UIImage imageWithContentsOfFile:@"myImage"];// no caching

那么我们应该如何选择呢?
如果你要加载一个大图片而且是一次性使用,那么就没必要缓存这个图片,用imageWithContentsOfFile足矣,这样不会浪费内存来缓存它。
然而,在图片反复重用的情况下imageNamed是一个好得多的选择。

以上是从内存和页面上优化的一些总结,后续有新的感悟会再来更新O(∩_∩)O。

相关推荐