iOS-UITableView性能优化

作者: 树下敲代码的超人 | 来源:发表于2017-10-18 18:09 被阅读850次

    1.最常用的就是cell的重用, 注册重用标识符
    它的原理是,根据cell高度和tableView大小,确定界面上能显示几个cell,例如界面上只能显示6个cell,那么这6个cell都是单独创建的而不是根据重用标识符去缓存中找到的。当你开始滑动tableView时,第一个cell开始渐渐消失,第七个cell开始显示的时候,会创建第七个cell,而不是用第一个cell去显示在第七个cell位置,因为有可能第一个cell显示了一半,而第7个cell也显示了一半,这个时候第一个cell还没有被放入缓存中,缓存中没有可利用的cell。所以实际上创建了7个cell。当滑动tableView去显示第八个cell的时候,这时缓存中已经有第一个cell,那么系统会直接从缓存中拿出来而不是创建,这样就算有100个cell的数据需要显示,实际也只消耗7个cell的内存。

    • 重用
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        NSString *identifier = @"CellIdentifier";//标识符
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];//冲缓冲池中取出同样标识的cell
        if(cell == nil)
        {
            //缓冲池中没有时,创建新的
            cell = [[UITableViewCell alloc]init........];
        }
    return cell;
    }
    
    • 如果cell内部显示的内容来自web,使用异步加载,缓存结果请求。

    • 尽量少在cellForRowAtIndexPath中设置数据,假如有100个数据,那么cellForRowAtIndexPath会执行100次,但实际屏幕显示却只有几个。这样会大量消耗时间,可以在willDisplayCell里进行数据的设置,因为willDisplayCell只会在cell将要显示时调用,屏幕显示几个cell才会调用。可以大大减少数据设置时间

    -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    //不要去设置cell的数据
    }
    -(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
    {
    //当cell要显示时才去设置需要显示的cell对应的数据
    }
    
    
    

    2.避免cell的重新布局

    • 创建cell的时候就完成布局,在后期设置cell属性的时候尽量少去添加移除cell内部控件的布局,尽量用hidden控制,在那种界面变动较大的界面或者控件较多的界面,尽量用多个 注册重用标识符或者不用cell来代表,这样减少内部重新布局带来的计算,虽然多个重用标识符会带来内存变多,但相比让用户感觉界面流畅,这点牺牲是有必要的。

    • 各个信息都是根据之前算好的布局进行绘制的。需要异步绘制。重写draeRect方法就不需要异步绘制了,因为drawRect本来就是异步绘制的。图文混排的绘制,coreText绘制。

    3.提前计算并缓存cell的属性及内容

    • 当我们创建cell的数据源方法时,编译器并不是先创建cell 再定cell的高度
    • 而是先根据内容一次确定每一个cell的高度,高度确定后,再创建要显示的cell,滚动时,每当cell进入凭虚都会计算高度,提前估算高度告诉编译器,编译器知道高度后,紧接着就会创建cell,这时再调用高度的具体计算方法,这样可以防止浪费时间去计算显示以外的cell
    • cell内部尽量少计算。比如文字的宽度,图片的宽高等,尽量在model设置前就计算好cell的高度。而不要在cell内部去进行计算,阻塞线程.(加入cell高度计算比较复杂,可以设置一个类似与cell内部计算的view,在创建model数据的时候,用这个view预先计算出cell的高度,而不是在cell内部或者tableView:heightForRowAtIndexPath方法里去计算,设置cell数据的时候高度直接从model里拿出。view的计算可以用异步线程去计算,但是不能让用户等待cell刷新时间过长)

    4.使用局部更新

    • 如果只是更新某组的话,使用reloadSection进行局部更新
    • 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后制定n行加载。滚动很快时,只加载目标范围内得cell,这样按需加载,极大地提高了流畅性

    5.减少cell中控件的数量

    • 尽量使cell得布局大致相同,不同风格的cell可以使用不用的重用标识符,初始化时添加控件(见仁见智哈,看个人对界面的分析领会)
    • 不适用的可以先隐藏

    6.缓存行高

    estimatedHeightForRow不能和HeightForRow里面的layoutIfNeed同时存在,这两者同时存在才会出现“窜动”的bug。所以我的建议是:只要是固定行高就写预估行高来减少行高调用次数提升性能。如果是动态行高就不要写预估方法了,用一个行高的缓存字典来减少代码的调用次数即可

    7.使用不透明视图

    • 不透明的视图可以极大地提高渲染的速度。因此如非必要,可以将table cell及其子视图的opaque属性设为YES(默认值UIButton内部的label的opaque默认值都是NO])。
    • Cell中不要使用clearColor,无背景色,透明度也不要设置为0。
    • 关于opaque

    Opaque该属性为BOOL值,UIView的默认值是YES,但UIButton等子类的默认值都是NO。opaque表示当前UIView是否不透明,不过搞笑的是事实上它却决定不了当前UIView是不是不透明,比如你将opaque设为NO,该UIView照样是可见的(是否可见是由alpha或hidden属性决定的),照理说为NO就表示透明,那就应该是不可见的呀?

    显示器中的每个像素点都可以显示一个由RGBA颜色空间组成的色值,比如有红色和绿色两个图层色块,对于没有交叉的部分,即纯红色和绿色部分来说,对应位置的像素点只需要简单的显示红或绿,对应的RGBA为(1,0,0,1)和(0,1,0,1)就行了,负责图形显示的GPU需要很小的计算量就可以确定像素点对应的显示内容。

    问题是红色和绿色还有相交的一块,其相交的颜色为黄色。这里的黄色是怎么来的呢?原来,GPU会通过图层一和图层二的颜色进行图层混合,计算出混合部分的颜色,最理想情况的计算公式如下:

    R = S + D * ( 1 – Sa )

    其中,R表示混合结果的颜色,S是源颜色(位于上层的红色图层一),D是目标颜色(位于下层的绿色图层二),Sa是源颜色的alpha值,即透明度。公式中所有的S和D颜色都假定已经预先乘以了他们的透明度。

    知道图层混合的基本原理以后,再回到正题说说opaque属性的作用。当UIView的opaque属性被设为YES以后,按照上面的公式,也就是Sa的值为1,这个时候公式就变成了:

    R = S

    即不管D为什么,结果都一样。因此GPU将不会做任何的计算合成,不需要考虑它下方的任何东西(因为都被它遮挡住了),而是简单从这个层拷贝。这节省了GPU相当大的工作量。由此看来,opaque属性的真实用处是给绘图系统提供一个性能优化开关!

    按照前面的逻辑,当opaque属性被设为YES时,GPU就不会再利用图层颜色合成公式去合成真正的色值。因此,如果opaque被设置成YES,而对应UIView的alpha属性不为1.0的时候,就会有不可预料的情况发生,这一点苹果在官方文档中有明确的说明:

    An opaque view is expected to fill its bounds with entirely opaque content—that is, the content should have an alpha value of 1.0. If the view is opaque and either does not fill its bounds or contains wholly or partially transparent content,the results are unpredictable. You should always set the value of this property to NO if the view is fully or partially transparent.

    • 举例
      比如,如果当前我们拥有一个和屏幕大小一致的单一图层,那么屏幕上的每一个像素相当于图层中的一个像素,这个时候,我们在这个图层上放置一个完全不透明的图层,那么GPU将会把上面的图层合成到下面的图层当中,由于上面的是一个完全不透明的图层,所以上面的图层会部份遮盖掉下面的图层,而在遮盖掉的矩形区域内,GPU会直接使用上面图层的像素来显示。如果我们最底的图层上放置的是一个有透明度的图层,那么在这个矩形区域里,GPU需要混合上下两个图层来计算出在屏幕上显示出来的像素的RGB值。若在同一个区域内,存在着多个有透明度的图层,那么GPU需要更多的计算才能得出最终像素的RGB值。而我们要做的就是避免像素混合,尽可能地为视图设置背景色,且设置opaque为YES,这会大大减少GPU的计算。
      这种颜色的混合需要消耗一定的GPU,在实际开发中远不止2层。如果只显示最上层,建议最上次透明度为1和opaque为YES.这样GPU就不会计算其他层的layer,减少计算。
    //以下这种处理方式会出现UILabel出现未知的边框,解决办法有2种
    1.让uilbale的宽高都为正数
    2.设置UILabel的边框颜色为自己的背景颜色。
    这2种办法虽然可以解决,但是在按钮中时,点击按钮会出现边框
    
    
    //IOS8以后UILabel的底图层变成了_UILabelLayer
    //如果label的内容是中文,label实际渲染区域要大于label的size
    //所以只要UILabel中含有中文,比如会造成像素混合增加GPU的计算。
    //cell中的UILabel和button里的label没有设置background,都是默认的。
    //要不造成像素混合,需要让UILabel有背景,并设置masksToBounds来排除像素混合
    
    self.label.background = self.contentView.background;
    self.label.layer.masksToBounds = YES;
    
    • 注意 :maskTobounds与cornerRadius结合才会离屏渲染,单独使用不会造成离屏渲染

    8.cell动画和绘制

    重用时,它内部绘制的内容并不会被自动清除,因此你可能需要调用setNeedsDisplayInRect:或setNeedsDisplay方法。

    CPU与GPU的说明
    CPU就是做绘制的操作把内容放到缓存里,GPU负责从缓存里读取数据然后渲染到屏幕上。CPU将准备好的bitmap放到RAM里,GPU去搬这快内存到VRAM中处理。 而这个过程GPU所能承受的极限大概在16.7ms完成一帧的处理,所以最开始提到的60fps其实就是GPU能处理的最高频率。 GPU是图形硬件,主要的工作是混合纹理并算出像素的RGB值,这是一个非常复杂的计算过程,计算的过程越复杂,所需要消耗的时间就越长,GPU的使用率就越高,这并不是一个好的现像,而我们需要做的是减少GPU的计算量。

    如果不需要动画效果,最好不要使用insertRowsAtIndexPaths:withRowAnimation:方法,而是直接调 用reloadData方法
    利用预渲染加速iOS设备的图像显示

    • 当图片下载完成后,如果cell是可见的,还需要更新图像
    NSArray *indexPaths = [self.tableView indexPathsForVisibleRows];
    for (NSIndexPath *visibleIndexPath in indexPaths) {
    if (indexPath == visibleIndexPath) { 
    MyTableViewCell *cell = (MyTableViewCell *)[self.tableView cellForRowAtIndexPath:indexPath];
    cell.image = image; 
    [cell setNeedsDisplayInRect:imageRect]; break; 
    }
    }// 也可不遍历,直接与头尾相比较,看是否在中间即可。
    

    insertRowsAtIndexPaths:withRowAnimation:方法,插入新行需要在主线程执行,而一次插入很多行的话(例如50行),会长时间阻塞主线程。而换成reloadData方法的话,瞬间就处理完了。

    9.减少视图的数目

    textLabel、detailTextLabel和imageView等view,而你还可以自定义一些视图放在它的contentView里。然而view是很大的对象,创建它会消耗较多资源,并且也影响渲染的性能。如果你的table cell包含图片,且数目较多,使用默认的UITableViewCell会非常影响性能。奇怪的是,使用自定义的view,而非预定义的view,明显会快些。

    10.不要阻塞主线程

    出现这种现象的原因就是主线程执行了耗时很长的函数或方法,在其执行完毕前,无法绘制屏幕和响应用户请求。其中最常见的就是网络请求了,它通常都需要花费数秒的时间,而你不应该让用户等待那么久。
    解决办法就是使用多线程,让子线程去执行这些函数或方法。这里面还有一个学问,当下载线程数超过2时,会显著影响主线程的性能。因此在使用ASIHTTPRequest时,可以用一个NSOperationQueue来维护下载请求,并将其maxConcurrentOperationCount设为2。而NSURLRequest则可以配合GCD来实现,或者使用NSURLConnection的setDelegateQueue:方法。
    当然,在不需要响应用户请求时,也可以增加下载线程数,以加快下载速度:

    - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
        if (!decelerate) {
            queue.maxConcurrentOperationCount = 5;
        }
    }
    
    - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
        queue.maxConcurrentOperationCount = 5;
    }
    
    - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
        queue.maxConcurrentOperationCount = 2;
    }
    

    11.cell内部图片处理
    UIImage类方法总结

    假如内存里有一张400x400的图片,要放到100x100的imageview里,如果不做任何处理,直接丢进去,问题就大了,这意味着,GPU需要对大图进行缩放到小的区域显示,需要做像素点的sampling,这种smapling的代价很高,又需要兼顾pixel alignment。计算量会飙升。
    OpenGL ES是直接调用底层的GPU进行渲染;Core Graphics是一个基于CPU的绘制引擎;

        //重新绘制图片
        //按照imageWidth, imageHeight指定宽高开始绘制图片
        UIGraphicsBeginImageContext(CGSizeMake(imageWidth, imageHeight));
        //把image原图绘制成指定宽高
        [image drawInRect:CGRectMake(0,0,imageWidth,  imageHeight)];
        //从绘制中获取指定宽高的图片
        UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
        //结束绘制
        UIGraphicsEndImageContext();
    

    RunLoop开始,RunLoop是一个60fps的回调,也就是说每16.7ms绘制一次屏幕,也就是我们需要在这个时间内完成view的缓冲区创建,view内容的绘制这些是CPU的工作;然后把缓冲区交给GPU渲染,这里包括了多个View的拼接(Compositing),纹理的渲染(Texture)等等,最后Display到屏幕上。但是如果你在16.7ms内做的事情太多,导致CPU,GPU无法在指定时间内完成指定的工作,那么就会出现卡顿现象,也就是丢帧。

    • 圆角图片处理
    • 1.直接在原图上层覆盖一个内部透明圆的图片。(目前来说最优的方式)
    • 2.重新绘制图片(虽然重新绘制后会减少渲染的计算,但还是会影响渲染。这种方式只是把GPU的压力转义到了CPU上。负载平衡)。下面是绘制图片的方法
    //根据size 和 radius 把image重新绘制。
    -(UIImage *)getCornerRadius:(UIImage *)image size:(CGSize)size radius:(int)r
    {
        int w = size.width;
        int h = size.height;
        int radius = r;
    
        UIImage *img = image;
        CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
        CGContextRef context = CGBitmapContextCreate(NULL, w, h, 8, 4 * w, colorSpace, kCGImageAlphaPremultipliedFirst);
        CGRect rect = CGRectMake(0, 0, w, h);
    
        CGContextBeginPath(context);
        addRoundedRectToPath(context, rect, radius, radius);
        CGContextClosePath(context);
        CGContextClip(context);
        CGContextDrawImage(context, CGRectMake(0, 0, w, h), img.CGImage);
        CGImageRef imageMasked = CGBitmapContextCreateImage(context);
        img = [UIImage imageWithCGImage:imageMasked];
    
        CGContextRelease(context);
        CGColorSpaceRelease(colorSpace);
        CGImageRelease(imageMasked);
        return img;
    }
    
    
    static void addRoundedRectToPath(CGContextRef context, CGRect rect, float ovalWidth,
                                     float ovalHeight)
    {
        float fw, fh;
        
        if (ovalWidth == 0 || ovalHeight == 0)
        {
            CGContextAddRect(context, rect);
            return;
        }
        
        CGContextSaveGState(context);
        CGContextTranslateCTM(context, CGRectGetMinX(rect), CGRectGetMinY(rect));
        CGContextScaleCTM(context, ovalWidth, ovalHeight);
        fw = CGRectGetWidth(rect) / ovalWidth;
        fh = CGRectGetHeight(rect) / ovalHeight;
        
        CGContextMoveToPoint(context, fw, fh/2);  // Start at lower right corner
        CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1);  // Top right corner
        CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1); // Top left corner
        CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1); // Lower left corner
        CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1); // Back to lower right
        
        CGContextClosePath(context);
        CGContextRestoreGState(context);
    }
    

    当然这里圆角的处理最好还是使用不透明的mask来遮罩。既能不用因为绘制造成CPU计算,而多余区域的渲染造成GPU的计算。

    Tip:复制下YY大神说的话和Demo

    目前有些第三方微博客户端(比如 VVebo、墨客等),使用了一种方式来避免高速滑动时 Cell 的绘制过程,相关实现见这个项目:VVeboTableViewDemo。它的原理是,当滑动时,松开手指后,立刻计算出滑动停止时 Cell 的位置,并预先绘制那个位置附近的几个 Cell,而忽略当前滑动中的 Cell。这个方法比较有技巧性,并且对于滑动性能来说提升也很大,唯一的缺点就是快速滑动中会出现大量空白内容。如果你不想实现比较麻烦的异步绘制但又想保证滑动的流畅性,这个技巧是个不错的选择。

    滚动时调整视图的绘制行为

    滚动会导致数个视图在短时间内更新,如果视图的绘制代码没有被适当调整,滚动时的性能会非常低,造成卡顿。相对于去考虑如何让cell视图内部布局简单控件数量少,应该更加倾向于滚动开始时改变cell视图显示方式。例如当滑动时暂时性的减少需要显示的内容,或者滚动时改变cell视图显示的方式,比如图片、视频仅显示占位图等。当滚动停止时,在将cell视图显示状态返回到前一状态。

    参考资料:UITableView性能优化与卡顿问题

    相关文章

      网友评论

        本文标题:iOS-UITableView性能优化

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