iOS 界面优化

卡顿原理

CPU耗时,产生丢帧

一帧CPU和 GPU加载流程

帧CPU和 GPU加载流程

CPU计算, GPU渲染

屏幕显示采取双缓冲区


屏幕显示

当帧缓冲区2 比较耗时没有完成,切换读取帧缓冲区1完成后,帧缓冲区2 仍然没有完成,再次读取帧缓冲区1,造成帧缓冲区2 不显示,产生丢帧

卡顿检测

1.CADisplayLink

_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];

 [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

1秒调用多少次即fps

1秒 if (delta < 1) return;

- (void)tick:(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;

}

2.NSRunLoop检测

通过监听NSRunLoop的生命周期,查看在规定时间内,NSRunLoop任务执行情况

通过信号量等待时间完成 规定时间1秒

dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));

监听NSRunLoop的生命周期

static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)

{

    LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;

    NSLog(@"%lu", activity);

    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);

}

界面优化

1. 预排版

加载数据时完成,cell 高度的计算

2.预解码


图片加载流程

对图片解码后 数据的处理

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];

                        }

                    });

图片避免无谓的解码操作

DownSampling 就是在Decode的时候指定尺寸,只Decode部分数据,减少内存的使用

比如我一个控件大小是100 * 100,但是原图可能是300 * 300的。使用DownSampling后,只需要解码少量数据就可以达到所需。

这个SDWebImage也已经支持,大家只需在加载图片的时候,利用context参数设置图片的大小和控件的大小相同即可。

@{SDWebImageContextImageThumbnailPixelSize:@(size)}

swift 图片加载框架Kingfisher

图片复用

对于使用频繁的图片,可以使用[UIImage imageWithNamed:@""]方式创建,利用系统级别的缓存来提高效率,减少内存。

对于使用不频繁的图片,建议使用直接读文件的方式加载,用完就会自动释放,减少内存。

cell 将要展示,加载图片

override func collectionView(

 _ collectionView: UICollectionView,

        willDisplay cell:UICollectionViewCell,

        forItemAt indexPath:IndexPath)

    {

 let imageView = (cell asImageCollectionViewCell).cellImageView!

 let url = ImageLoader.sampleImageURLs[indexPath.row % ImageLoader.sampleImageURLs.count]

        KF.url(url, cacheKey:"\(url.absoluteString)-\(indexPath.row)")

            .downsampling(size:CGSize(width: 250, height: 250))

            .cacheOriginalImage()

            .set(to: imageView)

    }

cell 将要消失时,取消加载图片

override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath)

    {

        (cellasImageCollectionViewCell).cellImageView.kf.cancelDownloadTask()

    }

3.离屏渲染

离屏渲染检测

1、模拟器下检测:Simulator --> Debug --> Color Off-screen rendered,模拟器下只需要设置模拟器一次就可以

2、真机下检测:XCode --> Debug --> View Debugging -->Rendering --> Color Offscreen-Rendered Yellow,真机下每次运行后都要在XCode中设置一下生效

离屏渲染产生原因

正常情况下,系统会按照60FPS或者120FPS的频率来执行渲染流程。在每个屏幕渲染周期内,系统会从帧的缓冲区里拿到已经渲染好的数据,渲染到屏幕上。而由于图层或者其他因素,导致在屏幕内无法直接渲染,需要在屏幕外开辟一个空间用来合成帧数据。这就是所谓的离屏渲染

渲染流程

离屏渲染的坏处

1.开辟了一块额外的空间,内存增加了

2.切换环境造成的牺牲很大

很容易发生在渲染周期内,数据无法渲染好,因此造成卡顿问题。

造成离屏渲染的方式

关于离屏渲染,实际开发中基本上都是:

圆角+剪裁的组合,并不是所有圆角+裁剪都会产生离屏渲染

1. 一旦设置圆角+裁剪,如果视图一定是有contents(图片、绘制内容、有图像信息的子视图),加上背景色或者边框,就会产生离屏渲染。

2.设置圆角+裁剪,加上子视图位于裁剪区域,也会离屏渲染。

3. 仅有圆角+裁剪,和contents是不会离屏渲染的。经典例子就是【button setImage】的了,只需要对button.imageView.layer.cornerRadius和button.imageView.clipsToBounds进行就不会离屏渲染

设置layer的mask

设置阴影

光栅化,shouldRasterize,一旦设置为true,就会把layer的渲染结果包括其子layer,以及圆角、阴影、group opacity等等)保存在一块内存中,这样一来在下一帧仍然可以被复用,而不会再次触发离屏渲染。主旨在于降低性能损失,但总是至少会触发一次离屏渲染。

抗锯齿

解决离屏渲染

对于设置阴影造成的离屏渲染,解决方式就是使用贝塞尔曲线绘制好path,这样就能解决问题

对于UIImageView的圆角方案

让设计师切一个遮罩盖在上面是最好的解决方案

苹果认为组合subViews的方式比自己绘制的方式好很多

离屏渲染参考连接

逻辑教育公众号

4.界面按需加载

使用数组储存当前页面的cell + 3 个, cell加载时 根据数组进行判断

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

- (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) {

                [arraddObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];

                [arraddObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];

                [arraddObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];

            }

        }else {

            NSIndexPath *indexPath = [temp firstObject];

            if (indexPath.row>3) {

                [arraddObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];

                [arraddObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];

                [arraddObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];

            }

        }

        [needLoadArr addObjectsFromArray:arr];

    }

}

5.异步渲染

框架美团Graver

将页面内所有组件 构造成一张图片,放到layer进行加载

1.重写layer

+ (Class)layerClass{

    NSLog(@"layerClass");

    return [LGLayer class];

}

2.layer 布局和展示

layer 布局

- (void)layoutSublayers{

    if (self.delegate && [self.delegate respondsToSelector:@selector(layoutSublayersOfLayer:)]) {

        //UIView

        [self.delegate layoutSublayersOfLayer:self];

    }else{

        [super layoutSublayers];

    }

}

layer展示

绘制流程的发起函数

- (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)];

}

3. UIView实现

layer布局

- (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

    NSLog(@"layerWillDraw");

}

绘制的操作

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{

    [super drawLayer:layer inContext:ctx];

    NSLog(@"drawLayer");

    [[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);

}

加载位图 layer.contents = (位图)

- (void)displayLayer:(CALayer *)layer{

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

    dispatch_async(dispatch_get_main_queue(), ^{

        layer.contents = (__bridge id)(image.CGImage);

    });

}

- (void)closeContext{

    UIGraphicsEndImageContext();

}

你可能感兴趣的:(iOS 界面优化)