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

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容

  • Socket:http://www.mamicode.com/info-detail-877996.html We...
    多情刀客无情刀阅读 3,965评论 1 18
  • 1 NSOperationQueue和GCD的区别是什么 GCD(Grand Central Dispatch)是...
    紫色冰雨阅读 739评论 0 3
  • 1.为什么说Objective-C是一门动态的语言? 1.object-c类的类型和数据变量的类型都是在运行是确定...
    墨徕阅读 676评论 0 0
  • 1、改变 UITextField 占位文字 颜色和去掉底部白框 [_userName setValue:[UICo...
    i_MT阅读 1,036评论 0 2
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32