美文网首页开发应用
六、 性能优化部分点的分析

六、 性能优化部分点的分析

作者: 流小贝 | 来源:发表于2022-05-10 15:47 被阅读0次

iOS 性能优化部分点的分析

一、时间复杂度

1、在集合里数据量小的情况下时间复杂度对于性能的影响看起来微乎其微。但如果某个开发的功能是一个公共功能,无法预料调用者传入数据的量时,这个复杂度的优化显得非常重要了。

上图列出了各种情况的时间复杂度,比如高效的序算法一般都是 O(n log n)。接下来看看下图:

图中可以看出 O(n) 是个分水岭,大于它对于性能就具有很大的潜在影响,如果是个公共的接口一定要加上说明,自己调用也要做到心中有数。当然最好是通过算法优化或者使用合适的系统接口方法,权衡内存消耗争取通过空间来换取时间。

2、下面通过集合里是否有某个值来举个例子:

//O(1)
return array[idx] == value;

//O(n)
for (int i = 0; i < count; i++) {
    if (array[i] == value) {
        return YES;
    }
}
return NO;

//O(n2)找重复值
for (int i = 0;  < count; i++) {
    for (int j = 0; j < count; j++) {
        if (i != j && array[i] == array[j]) {
            return YES;
        }
    }
}
return NO;

3、那么 OC 里几种常用集合对象提供的接口方法时间复杂度是怎么样的。

1、NSArray / NSMutableArray

首先我们发现他们是有排序,并允许重复元素存在的,那么这么设计就表明了集合存储没法使用里面的元素做 hash table 的 key 进行相关的快速操作。所以不同功能接口方法性能是会有很大的差异。

  • containsObject:,indexOfObject*,removeObject: 会遍历里面元素查看是否与之匹对,所以复杂度等于或大于 O(n)
  • objectAtIndex:,firstObject:,lastObject:,addObject:,removeLastObject: 这些只针对栈顶栈底操作的时间复杂度都是 O(1)
  • indexOfObject:inSortedRange:options:usingComparator: 使用的是二分查找,时间复杂度是 O(log n)

2、NSSet / NSMutableSet / NSCountedSet / NSOrderedSet / NSMutableOrderedSet

这些集合类型是无序没有重复元素。这样就可以通过 hash table 进行快速的操作。比如 addObject:, removeObject:, containsObject: 都是按照 O(1) 来的。需要注意的是将数组转成 Set 时会将重复元素合成一个,同时失去排序。

3、NSDictionary / NSMutableDictionary

和 Set 差不多,多了键值对应。添加删除和查找都是 O(1) 的。需要注意的是 Keys 必须是符合 NSCopying。

4、containsObject 方法在数组和 Set 里不同的实现

在数组中的实现

- (BOOL) containsObject: (id)anObject
{
  return ([self indexOfObject: anObject] != NSNotFound);
}
- (NSUInteger) indexOfObject: (id)anObject
{
  unsigned  c = [self count];

  if (c > 0 && anObject != nil)
  {
      unsigned  i;
      IMP   get = [self methodForSelector: oaiSel];
      BOOL  (*eq)(id, SEL, id)
    = (BOOL (*)(id, SEL, id))[anObject methodForSelector: eqSel];

     for (i = 0; i < c; i++)
    if ((*eq)(anObject, eqSel, (*get)(self, oaiSel, i)) == YES)
      return i;
    }
  return NSNotFound;
}

可以看到会遍历所有元素在查找到后才进行返回。

接下来可以看看 containsObject 在 Set 里的实现:

- (BOOL) containsObject: (id)anObject
{
  return (([self member: anObject]) ? YES : NO);
}
//在 GSSet,m 里有对 member 的实现
- (id) member: (id)anObject
{
  if (anObject != nil)
    {
      GSIMapNode node = GSIMapNodeForKey(&map, (GSIMapKey)anObject);
      if (node != 0)
    {
      return node->key.obj;
    }
    }
  return nil;
}

找元素时是通过键值方式从 map 映射表里取出,因为 Set 里元素是唯一的,所以可以 hash 元素对象作为 key 达到快速获取值的目的。hash概念可以参考这篇文章了解一下: 哈希表、时间复杂度、链表

通过测试可以发现:

  • containsObject   NSArray(大)      NSSet(小)
  • indexOfObject    NSArray(大)      NSSet(小)
  • removeObject    NSArray(大)     NSSet(小)
  • filteredArrayUsingPredicate    NSArray(大)     NSSet(小)
  • replaceObjectAtIndex    NSArray(大)     NSSet(小)

二、I/O 性能优化

1、I/O 是性能消耗大户,任何的 I/O 操作都会使低功耗状态被打破,所以减少 I/O 次数是这个性能优化的关键点,为了达成这个目下面列出一些方法。

  • 将零碎的内容作为一个整体进行写入;
  • 使用合适的 I/O 操作 API
  • 使用合适的线程
  • 使用NSCache 做缓存能够减少 I/O

2、NSCache 具有字典的所有功能,同时还有如下的特性:

  • 自动清理系统占用内存;
  • NSCache 是线程安全
  • -(void)cache:(NSCache *)cache willEvictObject:(id)obj; 缓存对象将被清理时的回调
  • evictsObjectsWithDiscardedContent 可以控制是否清理

三、用 GCD 来做优化

1、我们可以通过 GCD 提供的方法来将一些需要耗时操作放到非主线程上做,使得 App 能够运行的更加流畅响应更快。但是使用 GCD 时需要注意避免可能引起线程爆炸和死锁的情况,还有非主线程处理任务也不是万能的,如果一个处理需要消耗大量内存或者大量CPU操作 GCD 也没法帮你,只能通过将处理进行拆解分步骤分时间进行处理才比较妥当。

 1、异步处理事件
上图是最典型的异步处理事件的方法。

2、需要耗时长的任务
将 GCD 的 block 通过 dispatch_block_create_with_qos_class 方法指定队列的 QoS 为 QOS_CLASS_UTILITY。
这种 QoS 系统会针对大的计算,I/O,网络以及复杂数据处理做电量优化。

3、避免线程爆炸

举个例子,下面的写法就比较危险,可能会造成线程爆炸和死锁

for (int i = 0; i < 999; i++) {
dispatch_async(q, ^{...});
}
dispatch_barrier_sync(q, ^{});

那么怎么能够避免呢?首先可以使用 dispatch_apply

dispatch_apply(999, q, ^(size_t i){...});

或者使用 dispatch_semaphore

#define CONCURRENT_TASKS 4
sema = dispatch_semaphore_create(CONCURRENT_TASKS);
for (int i = 0; i < 999; i++){
    dispatch_async(q, ^{
        dispatch_semaphore_signal(sema);
    });
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}

四、界面性能优化

1、产生卡顿的原因

  • 系统中 CPU、GPU、显示器是以上面这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

  • 在 VSync(垂直同步) 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

2、CPU 资源消耗原因和解决方案

1、对象创建
  • 对象的创建会分配内存,调整属性,甚至还有读取文件等操作.应当尽量使用轻量的对象代替重量的对象.

  • 像 CALayer 就比 UIView 轻量级,所以如果视图不涉及触摸操作就可以使用 CALayer 代替 UIView,如果不涉及 UI 操作,可以将UIView的创建过程放到后台线程执行.

  • 通过 storyboard 创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多,在性能敏感的界面里, storyboard 并不是一个好的技术选择.

  • 尽量延迟对象的创建时间,并吧对象的创建分散到多个任务中去.

  • 如果对象可以复用,并且复用的代价比释放,创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用.

2、对象调整
  • 对象调整是常见的消耗 CPU 资源的地方, UIView的 bounds /frame/ center 等属性,在调整时,因为 CALayer 是没有属性的,在调用相关属性时,是生成一个临时方法,调用结束后再把值存到 CALayer的一个字典中,所以在调整 UIView的相关属性时, UIView和 CALayer 之间会产生一系列的通知和代理.

  • 当视图层次调整时, UIView CALayer 之间会出现很多方法的调用与通知,所以在优化性能时,应该尽量避免调整视图层次 添加和移除视图.
    对象销毁

  • 对象的销毁相对消耗的资源较少,通常当容器类持有大量对象时,其造成的消耗也是非常明显的,所以应当将消耗过程放到后台线程执行.

  • 最好的解决方法是,使用 BLOCK 捕捉对象,再在 block 中在后台线程给对象发送一个消息,即可在后台销毁对象

3、布局计算
  • 布局计算也是常见的CPU 消耗.在页面内布局非常复杂时,最好能提前在后台计算好布局
    AutoLayout

  • AutoLayout 是苹果推荐的布局方法,能够显著的提高布局效率,但是在复杂页面其资源消耗是成指数型增加的,所以在布局复杂的页面尽量不要使用 autolayout进行布局

4、文本计算
  • 如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下 UILabel 内部的实现方式:用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。

  • 如果你用 CoreText 绘制文本,那就可以先生成 CoreText 排版对象,然后自己计算了,并且 CoreText 对象还能保留以供稍后绘制使用。

5、文本渲染
  • 屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版、绘制为 Bitmap 显示的。常见的文本控件 (UILabel、UITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。

6、图片解码
  • 当用 UIImage 或 CGImageScoure 的那几个方法创建图片时,图片并不会立刻解码.图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前, CGImage 中的数据才会得到解码.这一步是发生在主线程的.如果想要绕开这个机制,常见的做法是在后台线程先把图片会知道 CGBitmapContext 中,然后冲 Bitmap 直接创建图片.目前常见的网络图片库都自带这个功能

  • 如imageView.layer.cornerRadius及imageView.layer.masksToBounds = YES;这类操作会导致离屏渲染, GPU会导致新开缓冲区造成消耗. 为了避免离屏渲染,你应当尽量避免使用 layer 的 border、corner、shadow、mask 等技术,而尽量在后台线程预先绘制好对应内容。这种SDWebImage的缓存策略是有很大的参考意义的.

7、图片绘制
  • 图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。

3、CPU 资源消耗原因和解决方案

GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。

1、纹理的渲染
  • 所有的 bitmap 包括图片,文本最终都要由内存提交到显存,绑定为 GPU Texture .无论是提交到显存的过程,还是GPU调整 和渲染 Texture的过程,都要消耗不少的 GPU 资源.

  • 当短时间内显示大量图片时, CPU 的占用率很低. GPU 的占用非常高,界面仍然会掉帧.避免这种情况的方法只能是尽量减少段时间内大量图片的显示,尽可能将多张图片合成一张进行显示.

2、视图的混合
  • 当多个视图(或者说 CALayer)重叠在一起显示时, GPU 会首先把他们混合到一起.如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源,为了减轻这种情况的 GPU 消耗,应该尽量减少视图数量和层次,并在不透明的视图里表明 opaque 属性以避免无用的 Alpha通道合成.

3、图形的生成
  • CALayer 的 border/圆角/阴影/遮罩,通常会触发离屏渲染,而离屏渲染通常发生在 GPU 中.当一个列表视图中出现大量的圆角 CAlayer ,并且快速滑动时,可以观察到 GPU 的资源已经占满,而 CPU 的资源消耗很少.这时界面仍然能正常滑动,但平均帧数会降到很低.

  • 对于需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本的视图上面来模拟相同的视觉效果.最称帝的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角/阴影/遮罩等属性.

性能优化参考文章

<font color=#FF0000 >
1、<font color=#FF0000 >参考文章1</font>

2、<font color=#FF0000 >参考文章2</font>
</font>


相关文章

网友评论

    本文标题:六、 性能优化部分点的分析

    本文链接:https://www.haomeiwen.com/subject/jpilurtx.html