注册

iOS 界面优化

卡顿原因

计算机通过CPUGPU显示器三者协同工作将试图显示到屏幕上

  • 1、CPU将需要显示的内容计算出来,提交到GPU
  • 2、GPU将内容渲染完成后将渲染后的内容存放到FrameBuffer(帧缓冲区)
  • 3、视频控制器根据VSync(垂直同步)信号来读取FrameBuffer中的数据
  • 4、将转换的数模传递给显示器显示
fb5edc04ce6b57c83fc0cb8d27c8c261.png

iOS设备中采用双缓存区+VSync

在收到VSync信号后,系统的图形服务通过CADisplayLink等机制通知App,在主程序中调度CPU计算显示的内容,随后将计算好的内容提交到GPU变换、合成、渲染,GPU将渲染结果提交帧缓冲区,等待下一个VSync信号到来时显示到屏幕上。由于垂直同步机制的原因,如果再一个VSync时间内,CPU或者GPU没有完成内容的处理,就会导致当前处理的帧丢弃,此时屏幕会保持上一帧的显示,造成掉帧


卡顿检测

  • FPS监控:因为iOS设备屏幕的刷新时间是60次/秒,一次刷新就是一次VSync信号,时间间隔是1000ms/60 = 16.67ms,所有如果咋16.67ms内下一帧数据没有准备好,就会产生掉帧
  • RunLoop监控:通过子线程检测主线程的RunLoop的状态,kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting两个状态之间的耗时是否达到一定的阈值

FPS监控

参照YYKit中的YYFPSLabel,其中通过CADisplayLink来实现,通过刷新次数/时间差得到刷新频率


class YPFPSLabel: 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: CJLWeakProxy(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监控

开辟子线程,通过监听主线程的kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting两个Activity之间的差值


#import "YPBlockMonitor.h"

@interface YPBlockMonitor (){
CFRunLoopActivity activity;
}

@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;

@end

@implementation YPBlockMonitor

+ (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)
{
LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)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{
// 创建信号
_semaphore = dispatch_semaphore_create(0);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
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)
{
if (++self->_timeoutCount < 2){
NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
continue;
}
// 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
NSLog(@"检测到超过两次连续卡顿");
}
}
self->_timeoutCount = 0;
}
});
}

@end

界面优化

UIView和CALayer的关系

  • UIView是基于UIKit框架,继承自UIResponder,可以处理事件,管理子视图
  • CALayer是基于CoreAnimation的,继承自NSObject,只负责显示,不能处理事件
  • UIKit组件最终都会分解为layer,存储到图层树
  • UIView中的部分属性,frame、bounds、transform等,来自CALayer的映射
  • CALayer内部没有属性,在调用属性时,内部通过运行时resolveInstanceMethod方法为对象临时添加一个方法,并将对应属性值保存到内部的Dictionary,同时通知delegate、创建动画等

CPU层面的优化

  • 1、对于不需要触摸的控件使用CALayer代替UIView

  • 2、减少UIViewCALayer的属性修改

  • 3、大量对象释放时,移动到后台线程释放

  • 4、预排版:在异步子线程中提前计算好视图的大小

  • 5、Autolayout在简单页面情况下们可以很好的提升开发效率,但是对于复杂视图而言,会产生严重的性能问题,随着视图数量的增长,Autolayout带来的CPU消耗是呈指数上升的。所以尽量使用代码布局

  • 6、文本处理

    • 对于文本没有特殊要求的,可以使用UILabel内部实现方法,放在子线程中执行
      • 计算文本宽高:[NSAttributedString boundingRectWithSize:options:context:]
      • 文本绘制:[NSAttributedString drawWithRect:options:context:]
    • 使用自定义文本控件,通过TextKit或者CoreText进行异步文本绘制。CoreText对象创建后,可以直接获取文本宽高等信息。CoreText直接使用了CoreGraphics占用内存小,效率高
  • 7、图片优化
    在使用UIImage或者CGImageSource方法创建图片时,图片数据不会立即解码,而是在设置到UIImageView/CALayer.contents中,然后由CALayer提交到GPU渲染前才在主线程进行解码,可以参考SDWebImage中对图片的处理,在子线程中先将图片绘制到CGBitmapContext,然后从Bitmap直接创建图片

    • 使用PNG图片,而非JPGE图片
    • 子线程中解码,主线程渲染,即通过Bitmap创建图片,在子线程赋值image
    • 优化图片大小,避免动态缩放
    • 多图合成一张图片显示
  • 8、避免使用透明View,会导致GPU在计算像素时,会将下层图层的像素也计算进来,颜色混合处理

  • 9、按需加载:例如通过RunLoop分发任务,ScrollView滚动时不加载

  • 10、少使用addView 给cell动态添加view

GPU层面优化

GPU主要是接收CPU提交的纹理+顶点,经过一系列transform,最终混合并渲染,输出到屏幕上
1、避免短时间显示大量图片,可以将多张图片合成一张
2、控制图片尺寸不超过4096x4096,因为图片超过这个尺寸,CPU会先进行预处理再提交给GPU
3、减少视图层级和数量
4、避免离屏渲染
5、异步渲染,例如可以将cell中的所有控件、视图合成一张位图进行显示



作者:木扬音
链接:https://www.jianshu.com/p/2f9a06932879

0 个评论

要回复文章请先登录注册