美文网首页iOS归纳
iOS 界面优化

iOS 界面优化

作者: 木扬音 | 来源:发表于2021-06-30 23:47 被阅读0次

    卡顿原因

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

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

    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监控

    参考 微信的matrix,滴滴的DoraemonKit

    开辟子线程,通过监听主线程的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、少使用addViewcell动态添加view

    GPU层面优化

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

    相关文章

      网友评论

        本文标题:iOS 界面优化

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