美文网首页
iOS开发性能优化(一)

iOS开发性能优化(一)

作者: 朽木自雕也 | 来源:发表于2017-10-20 17:45 被阅读263次

    tableview优化

    • Cell重用
    • 提前计算并缓存Cell的高度
    • 异步绘制cell
    • 滑动时,按需加载
    • 渲染

    网络优化

    • 第三方AFN时做网络隔离,减少对第三方的依赖性
    • 功能设计时,减少同一时间多次网络请求的可能性
    • 请求数据用json
    • 图片加载优化

    tableview优化

    cell重用

    数据源方法优化,在可见的页面会重复绘制页面,每次刷新显示都回去创建新的cell,非常耗费性能。
    解决方案,首先创建一个静态变量reuseID(代理方法返回cell回调用很多次,防止重复创建,static保证会被创建一次,提高性能),然后,从缓存池中取相应identifier的cell并更新数据,如果没有,才开始创建新的cell,并用identified镖师cell。每一个cell都会注册一个identified(重用标识符)放入缓存池,当需要调用的时候就直接从缓存池中找对应的ID,当不需要时就放入缓存池中等待调用。

    缓存池的实现

    当cell要alloc时,UITableView会在堆中开辟一段内存以供cell缓存之用。cell的重用通过identifier镖师不同类型的cell,由此可以推断出,缓存池外层可能是一个可变字典,通过key来取出内部的cell,而缓存池为存储不同高度、不同类型的cell,可以推断出缓存池的字典内部可能时一个可变数组,用来存放不同类型的cell,缓存池中只会保存已经被移除屏幕的不同类型的cell。缓存池获取celltableview提供了两个方法:

    //如果注册了cell,能够查询到返回cell,查询不到就返回nil
    -(nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier; 
    //使用这个方法之前,必须通过xib(storyboard)或是Class(纯代码)注册可重用Cell,而且这个方法一定会返回一个Cell
    - (void)registerNib:(UINib *)nib forCellReuseIdentifier:(NSString *)identifier;
    - (void)registerClass:(Class)cellClass forCellReuseIdentifier:(NSString *)identifier;
    

    用法:

    - (UITableView *)tableView {
        if(_tableView){
            _tableView = [[UItableView alloc]initWithFrame:self.view.frame];
            _tableView.delagate = self;
            _tableView.dataSource = self;
            [self.view addSubView:_tableView];
            [_tableView registerClass:[MXCell class] forCellreuseIdentifier:@"MXCellID"];
        }
        return _tableView;
    }
    - (UITableViewCell *)tableView:(UITableView *)tableView CellForRowAtIndexPath:(NSIndexPath *)indexPath {
        MXCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MXCellID"];
        cell.textLabel.text = @"傻逼就是你";
        return cell;
    }
    

    如果缓冲区 Cell 不存在,会使用原型 Cell 实例化一个新的 Cell,不需要再判断,同时代码结构更清晰。

    提前计算并缓存Cell的高度

    rowHeight
    tableview询问cell高度的方式有两种
    1、

    self.tableView.rowHeight = 88;
    

    2、

    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
         return 88;
    }
    

    强烈建议使用第一种方式,保证不必要的高度计算方法调用,rowHeight属性的默认值是44。第二种是代理回调,需要注意的是,实现第二个方法后,rowHeight设置奖无效。所以,这个方法用于具有多种cell高度的UITableView。
    具有动态高度内容的cell一直是个头疼的问题,比如聊天气泡的cell,frame布局时代通常是用数据内容反算高度:

    CGFloat height = textHeightWithFont() + imageHeight + topMargin + bottomMargin + ...;
    
    @interface BubbleCell : UITableViewCell
    + (CGFloat)heightWithEntity:(id)entity;
    @end
    

    各种魔法margin加上耦合了屏幕宽度。
    AutoLayout时代好了不少,提供了-systemLayoutSizeFittingSize:的API,在contentView中设置约束后,就能计算出准确的值;缺点是计算速度肯定没有手算快,而且这是个实例方法,需要维护专门为计算高度而生的template layout cell,它还要求使用者对约束设置的比较熟练,要保证contentView内部上下左右所有方向都有约束支撑,设置不合理的话计算的高度就成了0。

    UITableView+FDTemplateLayoutCell
    使用UITableView+FDTemplateLayoutCell无疑是解决算高问题的最佳实践之一,既有iOS 8 self-sizing功能简单的API,又可以达到iOS7流畅的滑动效果,还保持了最低支持iOS6。

    使用起来大概是这样:

    #import <UITableView+FDTemplateLayoutCell.h>
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
        return [tableView fd_heightForCellWithIdentifier:@"identifer" cacheByIndexPath:indexPath configuration:^(id cell) {
     
            //cell.entity = self.feedEntities[indexPath.row];
        }];
    }
    

    写完上面的代码后,你就已经使用到了:

    和每个UITableViewCell ReuseID一一对应的template layout cell
    这个cell只为了参加高度计算,不会真的显示到屏幕上;它通过UITableView的-dequeueCellForReuseIdentifier:方法lazy创建并保存,所以要求这个ReuseID必须已经被注册到了UITableView中,也就是说,要么是Storyboard中的原型cell,要么就是使用了UITableView的-registerClass:forCellReuseIdentifier:或-registerNib:forCellReuseIdentifier:其中之一的注册方法。

    根据autolayout约束自动计算高度
    使用了系统在iOS 6就提供的API:-systemLayoutSizeFittingSize:

    根据index path的一套高度缓存机制
    计算出的高度会自动进行缓存,所以滑动时每个cell真正的高度计算只会发生一次,后面的高度询问都会命中缓存,减少了非常可观的多余计算。

    自动的缓存失效机制
    无须担心你数据源的变化引起的缓存失效,当调用如-reloadData,-deleteRowsAtIndexPaths:withRowAnimation:等任何一个触发 UITableView 刷新机制的方法时,已有的高度缓存将以最小的代价执行失效。如删除一个indexPath为[0:5]的cell时,[0:0] ~ [0:4]的高度缓存不受影响,而[0:5]后面所有的缓存值都向前移动一个位置。自动缓存失效机制对UITableView的9个公有API都进行了分别的处理,以保证没有一次多余的高度计算。

    预缓存机制。就不展开说了,都抄得不好意思了😄。有兴趣可以去看看这篇文章《优化UITableViewCell高度计算的那些事》,写得狠屌

    异步绘制cell

    这里我就不逼逼了,逼逼也是抄人家的,大神写的《UITableView优化技巧》

    滑动时,按需加载

    开发的过程中,自定义Cell的种类千奇百怪,但Cell本来就是用来显示数据的,不说100%带有图片,也差不多,这个时候就要考虑,下滑的过程中可能会有点卡顿,尤其网络不好的时候,异步加载图片是个程序员都会想到,但是如果给每个循环对象都加上异步加载,开启的线程太多,一样会卡顿,我记得好像线程条数一般3-5条,最多也就6条吧。这个时候利用UIScrollViewDelegate两个代理方法就能很好地解决这个问题。

    - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
    - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
    

    思想就是识别UITableView禁止或者减速滑动结束的时候,进行异步加载图片,快滑动过程中,只加载目标范围内的Cell,这样按需加载,极大的提高流畅度。而SDWebImage可以实现异步加载,与这条性能配合就完美了,尤其是大量图片展示的时候。而且也不用担心图片缓存会造成内存警告的问题。

    //获取可见部分的Cell
    NSArray *visiblePaths = [self.tableView indexPathsForVisibleRows];
            for (NSIndexPath *indexPath in visiblePaths)
            {
            //获取的dataSource里面的对象,并且判断加载完成的不需要再次异步加载
                 <code>
            }
    

    记得在记得在“tableView:cellForRowAtIndexPath:”方法中加入判断:

    // tableView 停止滑动的时候异步加载图片
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    
             if (self.tableView.dragging == NO && self.tableView.decelerating == NO)
                {
                   //开始异步加载图片
                }
     
    
    渲染
    • 减少cell内部view的个数和层数, 子控件的层级越深,渲染到屏幕上所需要的计算量就越大;
    • 尽量少用view的透明图层,对于不透明的View,设置opaque为YES,这样在绘制该View时,就不需要考虑被View覆盖的其他内容;
    • 避免CALayer特效,给Cell中View加阴影会引起性能问题;

    网络优化

    第三方AFN时做网络隔离

    网络隔离少不了对afn的二次封装,前几天突然去翻看了一下15年写的项目,已经满目疮痍,跑起来网络都不通。继我之后项目又经历3个人之手,每个人的编程习惯都不一样,有一个跟我一样把网络请求的代码写在viewcontrller,有一个哥们用苹果原生NSURLSession、NSURLRequest、NSURLResPonse、NSURLProcotol这几个类自己封装的,一个哥们对afn做了一些简单的封装,但是给出的接口有点不好看。总结一下,就是项目代码乱七八糟的。项目代码是我一手带大了,所以决定把它写规范一点。闲话就不逼逼了。

    afn网络隔离封装需要做到几点:

    1. 与项目代码完全解耦,把请求代码放在封装体内部。否则,如果以后换网络请求框架,就需要到每一个控制器里面去改,要死人的。
    2. AFHTTPSessionManager写成单例,AFNetworking作者也是这样建议的。
    3. 封装的接口统一,参数越少越好。

    下面我就拿一下我满目疮痍的优化后的网络封装来说明一下

    .h文件

    /**
     *可以设置一个基础的url,比如:https://www.baidu.com/
     */
    #define BASE_URL @""
    /** 请求类型*/
    typedef NS_ENUM(NSInteger, MXNet_YTPE)
    {
        GET = 1,
        POST,
        DOWN_LOAD,//下载
        UPLOADING//上传
    };
    /**
     *发起网络请求
     *urlPath,url地址
     *type 请求类型
     *parameters 参数
     *returnBlock 成功/失败 block块
     *ProgressBlock 下载/上传 进度block块
     */
    void request(NSString *urlPath, MXNet_YTPE type,id parameters,void(^returnBlock)(id resuter,NSError *error),void(^ProgressBlock)(NSProgress *downloadProgress));
    /** 取消网络请求
     *urlPath nil取消所有的网络请求,不为nil取消特定的url的网络请求
     */
    void cancel(NSString *urlPath);
    /** 暂停请求
     *urlPath nil暂停所有的网络请求,不为nil暂停特定的url的网络请求
     */
    void suspend(NSString *urlPath);
    /** 继续请求
     *urlPath nil继续所有的网络请求,不为nil继续特定的url的网络请求
     */
    void resume(NSString *urlPath);
    

    在.h里面就给粗来几个简单的接口实现几个功能,发起请求、暂停请求、继续请求、取消请求。在发起请求函数里面需要完成的功能有请求成功的返回,请求的进度,比如实时监测下载或者上传进度,显示进度条。

    .m文件

    /** 网络请求二次封装*/
    @interface MXNetTool : NSObject
    @property (nonatomic, strong) AFHTTPSessionManager *netManager;
    /** 记录每次请求的标记*/
    @property (nonatomic, strong) NSMutableDictionary *taskDictionary;
    /** 单列对象*/
    + (instancetype)netTool;
    /**
     *对外的网请求接口
     *如果type为DOWN_LOAD或者UPLOADING,urlPath需要穿绝对路径
     */
    - (void)requestUrl:(NSString *)urlPath Moth:(MXNet_YTPE)type parameters:(id)parameters returnBlock:(void(^)(id resuter,NSError *error))returnBlock progressBlock:(void(^)(NSProgress *downloadProgress))progressBlock;
    /**
     *urlKey 传nil,取消所有网络请求
     *urlKey 取消特定url请求
     */
    - (void)cancel:(NSString *)urlKey;
    /** 继续*/
    - (void)resume:(NSString *)urlKey;
    /** 暂停*/
    - (void)suspend:(NSString *)urlkey;
    @end
    
    @implementation MXNetTool
    
    + (instancetype)netTool {
        static MXNetTool *networking = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            networking = [[self alloc] init];
            networking.netManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:BASE_URL]];
            [networking configNetManager];
        });
        return networking;
    }
    - (NSMutableDictionary *)taskDictionary {
        if (!_taskDictionary) {
            _taskDictionary = [NSMutableDictionary dictionary];
        }
        return _taskDictionary;
    }
    /** 配置网络参数*/
    - (void)configNetManager {
        self.netManager.responseSerializer = [AFHTTPResponseSerializer serializer];
        self.netManager.responseSerializer.acceptableContentTypes = [NSSet setWithArray:@[@"application/json",\
                                                                                          @"text/html",\
                                                                                          @"text/json",\
                                                                                          @"text/javascript",\
                                                                                          @"image/tiff",\
                                                                                          @"image/jpeg",\
                                                                                          @"image/png",\
                                                                                          @"image/x-icon",\
                                                                                          @"image/gif"]];
        //这里还可以配置请求头以及其他...
        
        /** 超时时间*/
        self.netManager.requestSerializer.timeoutInterval = 15.0;
    }
    /** MXNetTool入口*/
    - (void)requestUrl:(NSString *)urlPath Moth:(MXNet_YTPE)type parameters:(id)parameters returnBlock:(void(^)(id resuter,NSError *error))returnBlock progressBlock:(void(^)(NSProgress *downloadProgress))progressBlock {
        __weak __block typeof(self) weakSelf = self;
        /** 当请求结束以后回调移除标记,并返回数据*/
        void (^block)(id data, NSError *error) = ^(id data,NSError *error){
            [weakSelf.taskDictionary removeObjectForKey:urlPath];
            returnBlock(data,error);
        };
        NSURLSessionTask *task = nil;
        switch (type) {
            case GET:
                task = [self GET_requestUrlPath:urlPath Parameters:parameters returnBlock:block];
                break;
            case POST:
                task = [self POST_requestUrlPath:urlPath Parameters:parameters returnBlock:block];
                break;
            case DOWN_LOAD:
                task = [self downLoadWithUrl:urlPath parameters:parameters returnBlock:block progressBlock:progressBlock];
                break;
            case UPLOADING:
                task = [self uploadingUrl:urlPath parameters:parameters returnBlock:block progressBlock:progressBlock];
                break;
            default:
                task = nil;
                returnBlock(nil,nil);
                break;
        }
        /** 记录下每一次网络,并用url做标记*/
        if (task && urlPath) {
            self.taskDictionary[urlPath] = task;
        }
    }
    /** GET*/
    - (NSURLSessionTask *)GET_requestUrlPath:(NSString *)urlPath Parameters:(NSDictionary *)parameters returnBlock:(void(^)(id resuter,NSError *error))returnBlock {
    
        return [self.netManager GET:urlPath parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            if (returnBlock) {
                returnBlock(responseObject,nil);
            }
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            returnBlock(nil,error);
        }];
    }
    /** POST*/
    - (NSURLSessionTask *)POST_requestUrlPath:(NSString *)urlPath Parameters:(NSDictionary *)parameters returnBlock:(void(^)(id resuter,NSError *error))returnBlock {
        
        return [self.netManager POST:urlPath parameters:parameters progress:^(NSProgress * _Nonnull uploadProgress) {
            
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            if (returnBlock) {
                returnBlock(responseObject,nil);
            }
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            returnBlock(nil,error);
        }];
    }
    /** 下载*/
    - (NSURLSessionTask *)downLoadWithUrl:(NSString *)urlPath parameters:(NSDictionary *)parameters returnBlock:(void(^)(id resuter,NSError *error))returnBlock progressBlock:(void(^)(NSProgress *downloadProgress))progressBlock {
        NSURL *url = [NSURL URLWithString:[BASE_URL stringByAppendingString:urlPath]];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        return [self.netManager downloadTaskWithRequest:request progress:progressBlock destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
            NSString *path =  [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES) lastObject];
            NSString *filePath = [path stringByAppendingString:response.suggestedFilename];
            return [NSURL fileURLWithPath:filePath];
        } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
            if (error) {
                returnBlock(nil,error);
                return ;
            }
            NSString *path = [filePath path];
            NSData *data = [[NSData alloc]initWithContentsOfFile:path];
            returnBlock(data,nil);
        }];
    }
    /** 上传*/
    - (NSURLSessionTask *)uploadingUrl:(NSString *)urlPath parameters:(NSDictionary *)parameters returnBlock:(void(^)(id resuter,NSError *error))returnBlock progressBlock:(void(^)(NSProgress *downloadProgress))progressBlock {
        NSURL *url = [NSURL URLWithString:[BASE_URL stringByAppendingString:urlPath]];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        NSData *data = [NSJSONSerialization dataWithJSONObject:parameters options:NSJSONWritingPrettyPrinted error:nil];
        return [self.netManager uploadTaskWithRequest:request fromData:data progress:progressBlock completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
            if (error) {
                returnBlock(nil,error);
                return ;
            }
            returnBlock(responseObject,nil);
        }];
    }
    /** 取消*/
    - (void)cancel:(NSString *)urlKey {
        if (urlKey == nil) {
            for (NSString *key in self.taskDictionary) {
                NSURLSessionDownloadTask *downloadTask = self.taskDictionary[key];
                [downloadTask cancel];
            }
            [self.taskDictionary removeAllObjects];
        }else{
             NSURLSessionDownloadTask *downloadTask = self.taskDictionary[urlKey];
            [downloadTask cancel];
            [self.taskDictionary removeObjectForKey:urlKey];
        }
    }
    /** 暂停*/
    - (void)suspend:(NSString *)urlkey {
        if (urlkey == nil) {
            for (NSString *key in self.taskDictionary) {
                NSURLSessionDownloadTask *downloadTask = self.taskDictionary[key];
                [downloadTask suspend];
            }
        }else{
            NSURLSessionDownloadTask *downloadTask = self.taskDictionary[urlkey];
            [downloadTask suspend];
        }
    }
    /** 继续*/
    - (void)resume:(NSString *)urlkey {
        if (urlkey == nil) {
            for (NSString *key in self.taskDictionary) {
                NSURLSessionDownloadTask *downloadTask = self.taskDictionary[key];
                [downloadTask resume];
            }
        }else{
            NSURLSessionDownloadTask *downloadTask = self.taskDictionary[urlkey];
            [downloadTask resume];
        }
    }
    @end
    
    #pragma mark <--公布的接口-->
    /** 发起请求*/
    void request(NSString *urlPath, MXNet_YTPE type,id parameters,void(^returnBlock)(id resuter,NSError *error),void(^progressBlock)(NSProgress *downloadProgress)) {
        [NetTool requestUrl:urlPath Moth:type parameters:parameters returnBlock:returnBlock progressBlock:progressBlock];
    }
    /** 取消请求*/
    void cancel(NSString *urlPath) {
        [NetTool cancel:urlPath];
    }
    /** 暂停请求*/
    void suspend(NSString *urlPath) {
        [NetTool suspend:urlPath];
    }
    /** 继续请求*/
    void resume(NSString *urlPath) {
        [NetTool resume:urlPath];
    }
    
    

    这里原本是想把MXNetTool类公布在.h的,通过MXNetTool类单例对象来进行网络操作,但是想到多人开发,难免有人会写错不用单例对象,自己要去创建重新创建,然而机智的我就想到了,把类的声明放在.m文件里,.h文件用声明几个c语言函数接口,在.m里实现函数,通过函数来调用MXNetTool的接口。😄,这也许是多此一举,欢迎来喷。

    请求中的继续/暂停/取消的实现,这个很简单,就一次发起网络请求,不管是用苹果原生API方法,还是afn接口方法都会返回一个NSURLSessionTask类或者NSURLSessionTask子类对象,把网络请求地址跟这个对象通过一个可变字典存起来,下次想操作哪个网络请求就操作哪个网络请求,NSURLSessionTask类提供类三个对象方法,suspend、resume、cancel,分别对应暂停、继续、取消。

    功能设计时,减少同一时间多次网络请求的可能性

    这也是设计网络要用单例的原因,在上面我还没有实现,但很快会补上。具体实现可以去看一下串行队列,任务之间的依赖关系。

    请求数据用json

    json数据是大多数公司用的数据,方便解析。就说这么多,其他的就不做粘贴拷贝了。

    图片加载优化

    这个可以去仔细研究一下SDWebImage的内部实现,这里有一篇写得不错的文章《SDWebImage 源码阅读笔记》

    相关文章

      网友评论

          本文标题:iOS开发性能优化(一)

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