IOS

关注公众号 jb51net

关闭
首页 > 软件编程 > IOS > iOS开发界面优化

iOS开发底层探索界面优化示例详解

作者:晓之卫

这篇文章主要为大家介绍了iOS开发底层探索界面优化示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

1、卡顿原理

1.1、界面显示原理

1.2、界面撕裂

显示端每16.67ms从 FrameBuffer(帧缓存区)读取一帧数据,如果遇到耗时操作交付不了,那么当前画面就还是旧一帧的画面,但显示过程中,下一帧数据准备完毕,导致部分显示的又是新数据,这样就会造成屏幕撕裂

1.3、界面卡顿

为了解决界面撕裂,苹果使用双缓冲机制 + 垂直同步信号,使用 2个FrameBuffer 存储 GPU 处理结果,显示端交替从这2个FrameBuffer中读取数据,一个被读取时另一个去缓存;但解决界面撕裂的问题也带来了新的问题:掉帧

如果遇到画面带马赛克等情况,导致GPU渲染能力跟不上,会有2种掉帧情况;

如图,FrameBuffer2 未渲染完第2帧,下一个16.67ms去 FrameBuffer1 中拿第3帧:

小结

固定的时间间隔会收到垂直同步信号(Vsync),如果 CPU 和 GPU 还没有将下一帧数据放到对应的帧 FrameBuffer缓冲区,就会出现 掉帧

2、卡顿检测

2.1、CADisplayLink

系统在每次发送 VSync 时,就会触发CADisplayLink,通过统计每秒发送 VSync 的数量来查看 App 的 FPS 是否稳定

#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *link;
@property (nonatomic, assign) NSTimeInterval lastTime;  // 每隔1秒记录一次时间
@property (nonatomic, assign) NSUInteger count;         // 记录VSync1秒内发送的数量
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkAction:)];
    [_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)linkAction: (CADisplayLink *)link {
    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }
    _count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
    if (delta < 1) return;
    _lastTime = link.timestamp;
    float fps = _count / delta;
    _count = 0;
    NSLog(@"🎈 FPS : %f ", fps);
}
@end

2.2、RunLoop检测

RunLoop 的退出和进入实质都是Observer的通知,我们可以监听Runloop的状态,并在相关回调里发送信号,如果在设定的时间内能够收到信号说明是流畅的;如果在设定的时间内没有收到信号,说明发生了卡顿。

#import "LZBlockMonitor.h"
@interface LZBlockMonitor (){
    CFRunLoopActivity activity;
}
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;
@end
@implementation LZBlockMonitor
+ (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];
}
- (void)registerObserver{
    CFRunLoopObserverContext context = {0, (__bridge void*)self, NULL, NULL};
    //NSIntegerMax : 优先级最小
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            NSIntegerMax,
                                                            &CallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    LZBlockMonitor *monitor = (__bridge LZBlockMonitor *)info;
    monitor->activity = activity;
    // 发送信号
    dispatch_semaphore_t semaphore = monitor->_semaphore;
    dispatch_semaphore_signal(semaphore);
}
- (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

2.3、微信matrix

2.4、滴滴DoraemonKit

3、优化方法

平时简单的方案有:

3.1、预排版

3.2、预编码 / 解码

UIImage 是一个Model,二进制流数据 存储在DataBuffer中,经过decode解码,加载到imageBuffer中,最终进入FrameBuffer才能被渲染

3.3、按需加载

如果目标行与当前行相差超过指定行数,只加载目标滚动范围的前后指定3行

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    [needLoadArr removeAllObjects];
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
    NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
    NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
    NSInteger skipCount = 8;
    if (labs(cip.row-ip.row)>skipCount) {
        NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
        NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
        if (velocity.y<0) {
            NSIndexPath *indexPath = [temp lastObject];
            if (indexPath.row+3<datas.count) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
            }
        } else {
            NSIndexPath *indexPath = [temp firstObject];
            if (indexPath.row>3) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
            }
        }
        [needLoadArr addObjectsFromArray:arr];
    }
}

在滑动结束时进行 Cell 的渲染

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{
    scrollToToping = YES;
    return YES;
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
    scrollToToping = NO;
    [self loadContent];
}
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView{
    scrollToToping = NO;
    [self loadContent];
}
//用户触摸时第一时间加载内容
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    if (!scrollToToping) {
        [needLoadArr removeAllObjects];
        [self loadContent];
    }
    return [super hitTest:point withEvent:event];
}
- (void)loadContent{
    if (scrollToToping) {
        return;
    }
    if (self.indexPathsForVisibleRows.count<=0) {
        return;
    }
    if (self.visibleCells && self.visibleCells.count>0) {
        for (id temp in [self.visibleCells copy]) {
            VVeboTableViewCell *cell = (VVeboTableViewCell *)temp;
            [cell draw];
        }
    }
}

3.4、异步渲染

3.4.1、CALayer

3.4.2、异步渲染实现

简单例子

继承 CALayer

#import "LZLayer.h"
@implementation LZLayer
//前面断点调用写下的代码
- (void)layoutSublayers{
    if (self.delegate && [self.delegate respondsToSelector:@selector(layoutSublayersOfLayer:)]) {
        //UIView
        [self.delegate layoutSublayersOfLayer:self];
    }else{
        [super layoutSublayers];
    }
}
//绘制流程的发起函数
- (void)display{
    // Graver 实现思路
    CGContextRef context = (__bridge CGContextRef)([self.delegate performSelector:@selector(createContext)]);
    [self.delegate layerWillDraw:self];
    [self drawInContext:context];
    [self.delegate displayLayer:self];
    [self.delegate performSelector:@selector(closeContext)];
}
@end

继承 UIView

// - (CGContextRef)createContext 和 - (void)closeContext要在.h中声明
#import "LZView.h"
#import "LZLayer.h"
@implementation LZView
- (void)drawRect:(CGRect)rect {
    // Drawing code, 绘制的操作, BackingStore(额外的存储区域产于的) -- GPU
}
//子视图的布局
- (void)layoutSubviews{
    [super layoutSubviews];
}
+ (Class)layerClass{
    return [LZLayer class];
}
//
- (void)layoutSublayersOfLayer:(CALayer *)layer{
    [super layoutSublayersOfLayer:layer];
    [self layoutSubviews];
}
- (CGContextRef)createContext{
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.layer.opaque, self.layer.contentsScale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    return context;
}
- (void)layerWillDraw:(CALayer *)layer{
    //绘制的准备工作,do nontihing
}
//绘制的操作
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
    [super drawLayer:layer inContext:ctx];
    // 画个不规则图形
    CGContextMoveToPoint(ctx, self.bounds.size.width / 2- 20, 20);
    CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 + 20, 20);
    CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 + 40, 80);
    CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 - 40, 100);
    CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 - 20, 20);
    CGContextSetFillColorWithColor(ctx, UIColor.magentaColor.CGColor);
    CGContextSetStrokeColorWithColor(ctx, UIColor.magentaColor.CGColor); // 描边
    CGContextDrawPath(ctx, kCGPathFillStroke);
    // 画个红色方块
    [[UIColor redColor] set];
       //Core Graphics
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(self.bounds.size.width / 2- 20, self.bounds.size.height / 2- 20, 40, 40)];
    CGContextAddPath(ctx, path.CGPath);
    CGContextFillPath(ctx);
    // 文字
    [@"LZ" drawInRect:CGRectMake(self.bounds.size.width / 2 - 40, 100, 80, 24) withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:20],NSForegroundColorAttributeName: UIColor.blueColor}];
    // 图片
    [[UIImage imageWithContentsOfFile:@"/Volumes/Disk_D/test code/Test/Test/yasuo.png"] drawInRect:CGRectMake(10, self.bounds.size.height/2, self.bounds.size.width  - 20, self.bounds.size.height/2 -10)];
}
//layer.contents = (位图)
- (void)displayLayer:(CALayer *)layer{
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    dispatch_async(dispatch_get_main_queue(), ^{
        layer.contents = (__bridge id)(image.CGImage);
    });
}
- (void)closeContext{
    UIGraphicsEndImageContext();
}

控件们被绘制成了一张图

此外,虽然将控件画到一张位图上,但是还有问题,就是控件的交互事件,内容较多建议钻研一下graver的源码

以上就是iOS开发底层探索界面优化示例详解的详细内容,更多关于iOS开发界面优化的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文