IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> iOS之性能优化·优化App界面渲染与保持界面流畅性的技巧 -> 正文阅读

[移动开发]iOS之性能优化·优化App界面渲染与保持界面流畅性的技巧

一、界面渲染流程

① 渲染流程分析
  • 计算机中的显示过程通常是通过 CPU、GPU、显示器协同工作来将图片显示到屏幕上,如下图所示:

在这里插入图片描述

  • 苹果为了解决图片撕裂的问题使用了 VSync + 双缓冲区的形式,就是显示器显示完成一帧的渲染的时候会向发送一个垂直信号 VSync,收到这个这个垂直信号之后显示器开始读取另外一个帧缓冲区中的数据而 App 接到垂直信号之后开始新一帧的渲染。
    • CPU 计算好显示内容,提交至 GPU;
    • GPU 经过渲染完成后将渲染的结果放入 FrameBuffer(帧缓存区);
    • 随后视频控制器会按照 VSync 信号逐行读取 FrameBuffer 的数据;
    • 经过可能的数模转换传递给显示器进行显示。
  • 最开始时,FrameBuffer 只有一个,这种情况下 FrameBuffer 的读取和刷新的效率问题会受到很大的影响,双缓冲机制就可以很好的解决这个问题:GPU 会预先渲染好一帧放入 FrameBuffer,让视频控制器读取,当下一帧渲染好后,GPU 会直接将视频控制器的指针指向第二个 FrameBuffer。
  • 双缓存机制虽然解决了效率问题,但是随之而言的是新的问题,当视频控制器还未读取完成时,例如屏幕内容刚显示一半,GPU 将新的一帧内容提交到 FrameBuffer,并将两个 FrameBuffer 而进行交换后,视频控制器就会将新的一帧数据的下半段显示到屏幕上,造成屏幕撕裂现象。
  • 为了解决这个问题,采用了垂直同步信号机制。当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和 FrameBuffer 更新,而目前 iOS 设备中采用的正是双缓存区 + VSync。
② 屏幕卡顿原因
  • 在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变,中间这个等待的过程就造成了掉帧,也就是会卡顿。
  • 如下图所示,是一个显示过程,第 1 帧在 VSync 到来前,处理完成,正常显示,第 2 帧在 VSync 到来后,仍在处理中,此时屏幕不刷新,依旧显示第 1 帧,此时就出现了掉帧情况,渲染时就会出现明显的卡顿现象:

在这里插入图片描述

二、卡顿检测

① FPS 监控
  • 苹果的 iPhone 推荐的刷新率是60Hz,也就是每秒中刷新屏幕 60 次,也就是每秒中有 60 帧渲染完成,差不多每帧渲染的时间是 1000/60 = 16.67 毫秒整个界面会比较流畅,一般刷新率低于 45Hz ,在 16.67ms 内没有准备好下一帧数据,就会出现明显的卡顿现象。
  • FPS 的监控可以通过 YYFPSLabel 来实现,该原理主要是依靠 CADisplayLink 来实现的,通过 CADisplayLink 来监听每次屏幕刷新并获取屏幕刷新的时间,借助link的时间差,来计算一次刷新刷新所需的时间,然后通过“刷新次数 / 时间差”得到刷新频次,然后使用次数(也就是1)除以每次刷新的时间间隔得到 FPS,并判断是否其范围,通过显示不同的文字颜色来表示卡顿严重程度,具体源码如下:
	#import "YYFPSLabel.h"
	#import "YYKit.h"
	
	#define kSize CGSizeMake(55, 20)
	
	@implementation YYFPSLabel {
	  CADisplayLink *_link;
	  NSUInteger _count;
	  NSTimeInterval _lastTime;
	  UIFont *_font;
	  UIFont *_subFont;
	
	  NSTimeInterval _llll;
	}
	
	- (instancetype)initWithFrame:(CGRect)frame {
	  if (frame.size.width == 0 && frame.size.height == 0) {
	      frame.size = kSize;
	  }
	  self = [super initWithFrame:frame];
	
	  self.layer.cornerRadius = 5;
	  self.clipsToBounds = YES;
	  self.textAlignment = NSTextAlignmentCenter;
	  self.userInteractionEnabled = NO;
	  self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];
	
	  _font = [UIFont fontWithName:@"Menlo" size:14];
	  if (_font) {
	      _subFont = [UIFont fontWithName:@"Menlo" size:4];
	  } else {
	      _font = [UIFont fontWithName:@"Courier" size:14];
	      _subFont = [UIFont fontWithName:@"Courier" size:4];
	  }
	
	  //YYWeakProxy 这里使用了虚拟类来解决强引用问题
	  _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
	  [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
	  return self;
	}
	
	- (void)dealloc {
	  [_link invalidate];
	}
	
	- (CGSize)sizeThatFits:(CGSize)size {
	  return kSize;
	}
	
	- (void)tick:(CADisplayLink *)link {
	  if (_lastTime == 0) {
	      _lastTime = link.timestamp;
	      NSLog(@"sdf");
	      return;
	  }
	
	  // 次数
	  _count++;
	  // 时间
	  NSTimeInterval delta = link.timestamp - _lastTime;
	  if (delta < 1) return;
	  _lastTime = link.timestamp;
	  float fps = _count / delta;
	  _count = 0;
	
	  CGFloat progress = fps / 60.0;
	  UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
	
	  NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
	  [text setColor:color range:NSMakeRange(0, text.length - 3)];
	  [text setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
	  text.font = _font;
	  [text setFont:_subFont range:NSMakeRange(text.length - 4, 1)];
	
	  self.attributedText = text;
	}
	
	@end
  • FPS 只用在开发阶段的辅助性的数值,因为它会频繁唤醒 runloop,如果 runloop 在闲置的状态被 CADisplayLink 唤醒则会消耗性能。
  • FPS 的监控,具体实现逻辑如下:
	class YDWFPSLabel: UILabel {
	
	    fileprivate var link: CADisplayLink = {
	        let link = CADisplayLink.init()
	        return link
	    }()
	    
	    fileprivate var count: Int = 0
	    fileprivate var lastTime: TimeInterval = 0.0
	    fileprivate var fpsColor: UIColor = {
	        return UIColor.green
	    }()
	    fileprivate var fps: Double = 0.0
	    
	    override init(frame: CGRect) {
	        var f = frame
	        if f.size == CGSize.zero {
	            f.size = CGSize(width: 80.0, height: 22.0)
	        }
	        
	        super.init(frame: f)
	        
	        self.textColor = UIColor.white
	        self.textAlignment = .center
	        self.font = UIFont.init(name: "Menlo", size: 12)
	        self.backgroundColor = UIColor.lightGray
	        //通过虚拟类
	        link = CADisplayLink.init(target: YDWWeakProxy(target:self), selector: #selector(tick(_:)))
	        link.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
	    }
	    
	    required init?(coder: NSCoder) {
	        fatalError("init(coder:) has not been implemented")
	    }
	    
	    deinit {
	        link.invalidate()
	    }
	    
	    @objc func tick(_ link: CADisplayLink){
	        guard lastTime != 0 else {
	            lastTime = link.timestamp
	            return
	        }
	        
	        count += 1
	        // 时间差
	        let detla = link.timestamp - lastTime
	        guard detla >= 1.0 else {
	            return
	        }
	        
	        lastTime = link.timestamp
	        // 刷新次数 / 时间差 = 刷新频次
	        fps = Double(count) / detla
	        let fpsText = "\(String.init(format: "%.2f", fps)) FPS"
	        count = 0
	        
	        let attrMStr = NSMutableAttributedString(attributedString: NSAttributedString(string: fpsText))
	        if fps > 55.0 {
	            // 流畅
	            fpsColor = UIColor.green
	        }else if (fps >= 50.0 && fps <= 55.0){
	            // 一般
	            fpsColor = UIColor.yellow
	        }else{
	            // 卡顿
	            fpsColor = UIColor.red
	        }
	        
	        attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: fpsColor], range: NSMakeRange(0, attrMStr.length - 3))
	        attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], range: NSMakeRange(attrMStr.length - 3, 3))
	        
	        DispatchQueue.main.async {
	            self.attributedText = attrMStr
	        }
	    }
	}
② 通过 RunLoop 检测卡顿
  • 通过监听主线程 Runloop 一次循环的时间来判断是否卡顿,这里需要配合使用 GCD 的信号量来实现,设置初始化信号量为 0,然后开一个子线程等待信号量的触发,也是就是在子线程的方法里面调用 dispatch_semaphore_wait 方法设置等待时间是 1 秒,然后主线程的 Runloop 的 Observer 回调方法中发送信号也就是调用 dispatch_semaphore_signal 方法,此时时间可以置为 0 了,如果是等待时间超时则看此时的 Runloop 的状态是否是 kCFRunLoopBeforeSources 或者是 kCFRunLoopAfterWaiting,如果在这两个状态下两秒则说明有卡顿,详细代码如下:
	#import "YDWBlockMonitor.h"
	
	@interface YDWYDWlockMonitor (){
	  CFRunLoopActivity activity;
	}
	
	@property (nonatomic, strong) dispatch_semaphore_t semaphore;
	@property (nonatomic, assign) NSUInteger timeoutCount;
	
	@end
	
	@implementation YDWBlockMonitor
	
	+ (instancetype)sharedInstance {
	  static id instance = nil;
	  static dispatch_once_t onceToken;
	
	  dispatch_once(&onceToken, ^{
	      instance = [[self alloc] init];
	  });
	  return instance;
	}
	
	- (void)start{
	  [self registerObserver];
	  [self startMonitor];
	}
	
	static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
	{
	  YDWBlockMonitor *monitor = (__bridge YDWBlockMonitor *)info;
	  monitor->activity = activity;
	  // 发送信号
	  dispatch_semaphore_t semaphore = monitor->_semaphore;
	  dispatch_semaphore_signal(semaphore);
	}
	
	- (void)registerObserver{
	  CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
	  // NSIntegerMax : 优先级最小
	  CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
	                                                          kCFRunLoopAllActivities,
	                                                          YES,
	                                                          NSIntegerMax,
	                                                          &CallBack,
	                                                          &context);
	  CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
	}
	
	- (void)startMonitor{
	  // 创建信号c
	  _semaphore = dispatch_semaphore_create(0);
	  // 在子线程监控时长
	  dispatch_async(dispatch_get_global_queue(0, 0), ^{
	      while (YES)
	      {
	          // 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
	          // 没有接收到信号底层会先对信号量进行减减操作,此时信号量就变成负数
	          // 所以开始进入等到,等达到了等待时间还没有收到信号则进行加加操作复原信号量
	          // 执行进入等待的方法dispatch_semaphore_wait会返回非0的数
	          // 收到信号的时候此时信号量是1  底层是减减操作,此时刚好等于0 所以直接返回0
	          long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
	          if (st != 0)
	          {
	              if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
	              {
	                  //如果一直处于处理source0或者接受mach_port的状态则说明runloop的这次循环还没有完成
	                  if (++self->_timeoutCount < 2){
	                      NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
	                      continue;
	                  }
	                  // 如果超过两秒则说明卡顿了
	                  // 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
	                  NSLog(@"检测到超过两次连续卡顿");
	              }
	          }
	          self->_timeoutCount = 0;
	      }
	  });
	}
	@end
③ 微信 matrix
  • 此方案也是借助 runloop 实现的大体流程和方案三相同,不过微信加入了堆栈分析,能够定位到耗时的方法调用堆栈,所以需要准确的分析卡顿原因可以借助微信 matrix 来分析卡顿。当然也可以在方案2中使用 PLCrashReporter 这个开源的第三方库来获取堆栈信息。
  • 微信 matrix 的下载链接:微信 matrix
④ 滴滴 DoraemonKit
  • 实现方案大概就是在子线程中一直 ping 主线程,在主线程卡顿的情况下,会出现断在的无响应的表现,进而检测卡顿。
  • 滴滴 DoraemonKit 的下载链接:滴滴 DoraemonKit

三、 CPU 资源消耗优化

① 预排版
  • 预排版主要是对 CPU 进行减负。
  • 假设现在又个 TableView 其中需要根据每个 cell 的内容来定 cell 的高度。知道 TableView 有重用机制,如果复用池中有数据,即将滑入屏内的 cell 就会使用复用池内的 cell,做到节省资源,但是还是要根据新数据的内容来计算 cell 的高度,重新布局新 cell 中内容的布局,这样反复滑动 TableView 相同的 cell 就会反复计算其 frame,这样也给 CPU 带来了负担。如果在得到数据创建模型的时候就把 cell frame 算出,TableView 返回模型中的 frame 这样的话同样的一条 cell 就算来回反复滑动 TableView,计算 frame 这个操作也就仅仅只会执行一次,所以也就做到了减负的功能,如下图:一个 cell 的组成需要 modal 找到数据,也需要 layout 找到这个 cell 如何布局:

在这里插入图片描述

② 预解码 & 预渲染
  • 图片的渲染流程,在 CPU 阶段拿到图片的顶点数据和纹理之后会进行解码生产位图,然后传递到 GPU 进行渲染,主要流程图如下:

在这里插入图片描述

  • 如果图片很多很大的情况下解码工作就会占用主线程 RunLoop 导致其他工作无法执行比如滑动,这样就会造成卡顿现象,所以这里就可以将解码的工作放到异步线程中不占用主线程,可能有人会想只要将图片加载放到异步线程中在异步线程中生成一个 UIImage 或者是 CGImage,然后再主线程中设置给 UIImageView,此时可以写段代码使用 instruments 的 Time Profiler,查看一下堆栈信息:

在这里插入图片描述

  • 发现图片的编解码还是在主线程,针对这种问题常见的做法是在子线程中先将图片绘制到 CGBitmapContext,然后从 Bitmap 直接创建图片,例如 SDWebImage 三方框架中对图片编解码的处理,这就是 Image 的预解码,代码如下:
	dispatch_async(queue, ^{
	 CGImageRef cgImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self]]].CGImage;
	 CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
	
	 BOOL hasAlpha = NO;
	 if (alphaInfo == kCGImageAlphaPremultipliedLast ||
	     alphaInfo == kCGImageAlphaPremultipliedFirst ||
	     alphaInfo == kCGImageAlphaLast ||
	     alphaInfo == kCGImageAlphaFirst) {
	     hasAlpha = YES;
	 }
	
	 CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
	 bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
	
	 size_t width = CGImageGetWidth(cgImage);
	 size_t height = CGImageGetHeight(cgImage);
	
	 CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
	 CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
	 cgImage = CGBitmapContextCreateImage(context);
	
	 UIImage * image = [[UIImage imageWithCGImage:cgImage] cornerRadius:width * 0.5];
	 CGContextRelease(context);
	 CGImageRelease(cgImage);
	 completion(image);
	});
③ 按需加载
  • 顾名思义需要显示的加载出来,不需要显示的加载,例如 TableView 中的图片滑动的时候不加载,在滑动停止的时候加载(可以使用 Runloop,图片绘制设置 defaultModal 就行)。
④ 异步渲染
  • UIView 和 CALayer 的关系:
    • UIView 是基于 UIKit 框架的,能够接受点击事件,处理用户的触摸事件,并管理子视图;
    • CALayer 是基于 CoreAnimation,而 CoreAnimation 是基于 QuartzCode 的,所以 CALayer 只负责显示,不能处理用户的触摸事件;
    • UIView 是直接继承 UIResponder 的,CALayer 是继承 NSObject 的;
    • UIView 的主要职责是负责接收并响应事件;而 CALayer 的主要职责是负责显示 UI,UIView 依赖于 CALayer 得以显示。
  • UIView 主要负责时间处理,CALayer 主要是视图显示,异步渲染的原理其实也就是在子线程将所有的视图绘制成一张位图,然后回到主线程赋值给 layer 的 contents。例如 Graver 框架的异步渲染流程如下:

在这里插入图片描述

  • 核心源码如下:
	if (drawingFinished && targetDrawingCount == layer.drawingCount)
	{
	  CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
	  {
	      // 让 UIImage 进行内存管理
	      // 最终生成的位图  
	      UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
	      void (^finishBlock)(void) = ^{
	          // 由于block可能在下一runloop执行,再进行一次检查
	          if (targetDrawingCount != layer.drawingCount)
	          {
	              failedBlock();
	              return;
	          }
	          //主线程中赋值完成显示
	          layer.contents = (id)image.CGImage;
	          // ...
	      }
	      if (drawInBackground) dispatch_async(dispatch_get_main_queue(), finishBlock);
	      else finishBlock();
	  }
	
	  // 一些清理工作: release CGImageRef, Image context ending
	}
  • 当视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。
⑤ 对象创建
  • 对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如 CALayer 比 UIView 要轻量许多,那么不需要响应触摸事件的控件,用 CALayer 显示会更加合适。如果对象不涉及 UI 操作,则尽量放到后台线程去创建,但可惜的是包含有 CALayer 的控件,都只能在主线程创建和操作。通过 Storyboard 创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多,在性能敏感的界面里,Storyboard 并不是一个好的技术选择。
  • 尽量推迟对象创建的时间,并把对象的创建分散到多个任务中去,懒加载是个不错的选择。尽管这实现起来比较麻烦,并且带来的优势并不多,但如果有能力做,还是要尽量尝试一下。如果对象可以复用,并且复用的代价比释放、创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用。
⑥ 对象销毁
  • 对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。例如:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。
	NSArray *tmp = self.array;
	self.array = nil;
	dispatch_async(queue, ^{
	    [tmp class];
	});
⑦ 文本计算
  • 如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。
  • 如果对文本显示没有特殊要求,可以参考下 UILabel 内部的实现方式:用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。
⑧ 文本渲染
  • 屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版、绘制为 Bitmap 显示的。
  • 常见的文本控件 (UILabel、UITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。
⑨ 图片的解码
  • 用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。
  • 如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。目前常见的网络图片库都自带这个功能。
⑩ 图像的绘制
  • 图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程,这个最常见的地方就是[UIView drawRect:]里面了。
  • 由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制就可以很容易的放到后台线程进行。一个简单的异步绘制的过程大致如下(实际情况会比这个复杂得多,但原理基本一致):
	- (void)display {
	    dispatch_async(backgroundQueue, ^{
	        CGContextRef ctx = CGBitmapContextCreate(...);
	        // draw in context...
	        CGImageRef img = CGBitmapContextCreateImage(ctx);
	        CFRelease(ctx);
	        dispatch_async(mainQueue, ^{
	            layer.contents = img;
	        });
	    });
	}

四、GPU 资源消耗优化

① 纹理渲染
  • 所有的 Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture。不论是提交到显存的过程,还是GPU调整和渲染Texture的过程,都要消耗不少GPU资源。当在较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU占用率很低,GPU占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。
  • 当图片过大,超过 GPU 的最大纹理尺寸时,图片需要先由 CPU 进行预处理,这对 CPU 和 GPU 都会带来额外的资源消耗。目前来说,iPhone 4S 以上机型,纹理尺寸上限都是 4096×4096,所以尽量不要让图片和视图的大小超过这个值。
② 视图混合(Composing)
  • 当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。
  • 为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示。
③ 图形生成
  • CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。
  • 当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。
  • 为了避免这种情况,可以尝试开启CALayer.shouldRasterize(指示在合成之前图层是否呈现为位图) 属性,但这会把原本离屏渲染的操作转嫁到CPU上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。
  • 最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。
④ 预渲染
  • 对于 TableView 来说,Cell 内容的离屏渲染会带来较大的 GPU 消耗,为了图省事儿用到了不少 layer 的圆角属性,在低性能的设备(比如 iPad 3)上快速滑动一下这个列表,能感受到虽然列表并没有较大的卡顿,但是整体的平均帧数降了下来。用 Instument 查看时能够看到 GPU 已经满负荷运转,而 CPU 却比较清闲。
  • 为了避免离屏渲染,应当尽量避免使用 layer 的 border、corner、shadow、mask 等技术,而尽量在后台线程预先绘制好对应内容。

在这里插入图片描述

  • 二进制流 decode 属于 UI 的 prepare,依赖于图形编解码插件。SDWebImage 关于解码的处理:
	// decode the image in coder queue
	dispatch_async(self.coderQueue, ^{
	    @autoreleasepool {
	        UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
	        CGSize imageSize = image.size;
	        if (imageSize.width == 0 || imageSize.height == 0) {
	            [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
	        } else {
	            [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
	        }
	        [self done];
	    }
	});
⑤ 预排版
  • 当获取到 API JSON 数据后,把每条 Cell 需要的数据都在后台线程计算并封装为一个布局对象 CellLayout。CellLayout 包含所有文本的CoreText排版结果、Cell 内部每个控件的高度、Cell 的整体高度。每个 CellLayout 的内存占用并不多,所以当生成后,可以全部缓存到内存,以供稍后使用。这样,TableView 在请求各个高度函数时,不会消耗任何多余计算量;当把 CellLayout 设置到 Cell 内部时,Cell 内部也不用再计算布局了。
  • 对于通常的 TableView 来说,提前在后台计算好布局结果是非常重要的一个性能优化点。为了达到最高性能,你可能需要牺牲一些开发速度,尽量不要用 Autolayout 等技术,少用 UILabel 等文本控件。但如果你对性能的要求并不那么高,可以尝试用 TableView 的预估高度的功能,并把每个 Cell 高度缓存下来。这里有个来自百度知道团队的开源项目可以很方便的实现这一点:FDTemplateLayoutCell。
⑥ 异步绘制
  • 当 TableView 快速滑动时,会有大量异步绘制任务提交到后台线程去执行。但是有时滑动速度过快时,绘制任务还没有完成就可能已经被取消了。如果这时仍然继续绘制,就会造成大量的 CPU 资源浪费,甚至阻塞线程并造成后续的绘制任务迟迟无法完成。
  • 如果这时仍然继续绘制,就会造成大量的 CPU 资源浪费,甚至阻塞线程并造成后续的绘制任务迟迟无法完成。我的做法是尽量快速、提前判断当前绘制任务是否已经被取消;在绘制每一行文本前,都会调用 isCancelled() 来进行判断,保证被取消的任务能及时退出,不至于影响后续操作。
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-07-14 11:02:31  更:2021-07-14 11:02:38 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/28 6:05:58-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码