如何实现UITableView优化

一.为什么要进行UITableView的性能优化?

背景: 

        当代移动端App层次不齐, UITableView控件可谓是屡见不鲜,所以说UITbleView成为了每个程序要必备的技能.当然了,会用和用的六又是两码子事,会用只是停留在界面的展示,而作为一名合格的开发人员就必须具备优化性能的意识.


------->>>>下面阐述自己工作中处理UITableView和查看资料所得出的一些优化UITableView的建议:

前提: 

        Table view需要有很好的滚动性能,不然用户会在滚动过程中发现动画的瑕疵(比如说界面卡顿的现象)。为了保证table view平滑滚动,确保你采取了以下的措施


1.      正确使用`reuseIdentifier`来重用cells

        在此涉及到的知识点-->>调度缓存池的中获取可重的cell

                                        -->>解决cell的复用:(分情况是多种cell还是单种cell),多种不同样式的cell是依靠reuseIdentifier来重用cell的.

2.      尽量使所有的viewopaque,包括cell自身

3.      如果cell内现实的内容来自web,使用异步加载,缓存请求结果

4.      使用`shadowPath`来画阴影

5.      减少subviews的数量

6.      尽量不适用`cellForRowAtIndexPath:`,如果你需要用到它,只用一次然后缓存结果

        

           ---->>因为dequeueReusableCellWithIdentifier:forIndexPath:(会调用heightForRowAtIndexPath)和 dequeueReusableCellWithIdentifier (后面这个不会再次调用heightForRowAtIndexPath)

            6.1 tableView在cell显示之前会调用heightForRowAtIndexPath,有多少个cell就会调用多少次, 算contentSize

            6.2 使用了预估行高,并不会再显示之前去计算获取所有的行高,根据预估行高和实际行高来获取cell的行高,先根据预估行高计算好要先获取几个cell,如果计算的这几个cell高度确实够(高度能超出屏幕的高度就不计算了.如果不够还会计算),目的也是让contentSize大于屏幕,就能滚动,后面要显示,才来计算行高, 会发现滚动条会跳

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

//       方法一:
//       let cell = tableView.dequeueReusableCellWithIdentifier(identifier, forIndexPath: indexPath) as! ALStatusCell
//       方法二:
        let cell = tableView.dequeueReusableCellWithIdentifier(identifier) as! ALStatusCell
        //相比oc的不同点是不做下面的判断赋值
//        if cell == nil {
//            cell = ALStatusCell.init(style: UITableViewCellStyle.Default, reuseIdentifier: identifier)
//        }
//        cell.textLabel?.text = self.userDatas![indexPath.row].text
        cell.status = self.userDatas?[indexPath.row]
        return cell
    }


7.      使用正确的数据结构来存储数据(类似新浪微博加载微博数据,我们要将其存储到本地数据库(SQLite的方式来处理微博数据))

    7.1  先定义SQLite单例

// SELECT * FROM T_Product WHERE name like '%子%'; 在代码中 '%%子%%'
class ALSQLiteManager: NSObject {
    static let shareManager: ALSQLiteManager = ALSQLiteManager()
    
    let dequeue : FMDatabaseQueue
    private override init() {
        //数据库路径
        let dbPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last! + "/status.db"
        
        //打开数据库
        dequeue = FMDatabaseQueue(path: dbPath)
        super.init()
        
        //创建数据表
        creatTable()
    }
    private func creatTable(){
    
        let tablesPath = NSBundle.mainBundle().pathForResource("statuses.sql", ofType: nil)!
        
        
        let sql = try! NSString(contentsOfFile: tablesPath, encoding:NSUTF8StringEncoding)
        print("\(sql)")
        //执行sql语句
        dequeue.inDatabase { (db) in
            //执行多条语句
            if db.executeStatements(sql as String){
            print("执行成功")
            }else{
            print("执行失败")
            }
        }
    }
}

7.2 创建数据访问层单例---实现数据缓存数据的逻辑

 /**

 缓存数据的加载流程:

 1.先查找本地数据库有没有保存数据

 2.如果本地数据库有,就直接从本地数据库中加载

 3.如果没有,发送网络请求,从网络加载

 4.将服务器返回的数据保存到本地

 5.在将服务器返回的数据返回给用户处理

 */

class ALStatusDAL: NSObject {

    //创建数据访问层单例
    static let shareDAL:ALStatusDAL = ALStatusDAL()
    

    //加载微博数据,可能从服务器,也有可能从本地加载
    func loadStatusData(since_id:Int64,max_id: Int64,loadStatusCallback: (status:[[String:AnyObject]]?,error:NSError?)->()){
    
//         缓存数据的加载流程:
//         1.先查找本地数据库有没有保存数据
        
         //TODO:- 加载本地数据
        loadCacheStatus(since_id, max_id: max_id) { (statuses) in
         if statuses != nil && statuses!.count > 0{
         //表示从数据库中加载数据成功
//         2.如果本地数据库有,就直接从本地数据库中加载
            //将加载成功的数据进行返回
            loadStatusCallback(status: statuses,error: nil)
            return
            
}
        // 1.urlString
        let urlString = "https://api.weibo.com/2/statuses/home_timeline.json"
        
        // 2.参数
        var parameters:[String:AnyObject] = ["access_token": ALUserAccountViewModel.userAccountViewModel.userAccount!.access_token!]
        // 将since_id, max_id拼接到请求参数里面
        if since_id > 0 {   // 别人转了since_id
            parameters["since_id"] = NSNumber(longLong: since_id)
        } else if max_id > 0 {
            parameters["max_id"] = NSNumber(longLong: max_id)
        }
        
      //         3.如果没有,发送网络请求,从网络加载
        
        ALNetWorkTool.shareInstance.GET(urlString, parameters: parameters, progress: nil, success: { (_, responseObject) in
            if let dict = responseObject as? [String: AnyObject], let statusesDict = dict["statuses"] as? [[String: AnyObject]]{
                //         4.将服务器返回的数据保存到本地
                self.saveCacheStatus(statusesDict)
                //         5.在将服务器返回的数据返回给用户处理
                loadStatusCallback(status: statusesDict,error: nil)
            }
            }) { (_, error) in
                print("服务器返回数据错误:\(error)")
                loadStatusCallback(status: nil, error: error)
        }
       } 
    }
    //保存微博的字典数据到数据库
   private func saveCacheStatus(statuses: [[String:AnyObject]]){
    
       //获取UID
        let uid = ALUserAccountViewModel.userAccountViewModel.userAccount?.uid
        
        //断言:一定要获取到uid.获取不到就奔溃
        assert(uid != nil ,"没有uid")
        
        let sql = "INSERT INTO T_Status (status_id ,status,user_id) VALUES (?,?,?);"
        
        //使用事务插入批量数据
        ALSQLiteManager.shareManager.dequeue.inTransaction { (db, rollback) in
            //遍历获取每条微博的字典
            do{
                for dict in statuses{
                
                    let id = dict["id"] as! Int

                    let data = try NSJSONSerialization.dataWithJSONObject(dict, options: NSJSONWritingOptions(rawValue: 0))
                    let statusText = String(data: data, encoding: NSUTF8StringEncoding)!
                    //数据库中不能直接保存字典
                    //要将字典中的内容先json序列化转为二进制,在转为字符串

                    try db.executeUpdate(sql, values: [id,statusText,uid!])

                }
                print("保存了: \(statuses.count) 条微博")
            }catch let error as NSError{
            
                print("保存数据出错了:\(error)")
            }
            
        }
    }
    
    //从本地数据库加载数据
      private func loadCacheStatus(since_id:Int64,max_id:Int64,loadCacheStatusCallback:(statuses:[[String:AnyObject]]?)->()){
    //获取uid
        
        let uid = ALUserAccountViewModel.userAccountViewModel.userAccount?.uid
        
        assert(uid != nil ,"uid为空")
        
        //数据库查询语句
        var sql = "SELECT status_id ,status ,user_id FROM T_Status \n" + "WHERE user_id = \(uid!) \n"
        
        if since_id > 0{
            sql += "AND status_id > \(since_id)\n"
        }else if max_id > 0{
            sql += "AND status_id < \(max_id)\n"
        }
        //限制显示的数量
        sql += "ORDER BY status_id DESC LIMIT 20;"
        
        print("\(sql)")
            //查询数据
        ALSQLiteManager.shareManager.dequeue.inDatabase { (db) in
            do{
                //序列集合
            let resultSet = try db.executeQuery(sql, values: [])
                //定义字典保存数据库加载的数据
                var statusDicts = [[String:AnyObject]]()
                while resultSet.next(){
                    //取出当前的序列集内容,类型是string
                let statusText = resultSet.stringForColumn("status")
                    
                    //将其转换为字典数组
                    //转化过程: String -> NSData -> 字典数组
                let data = statusText.dataUsingEncoding(NSUTF8StringEncoding)
                    //反序列化
                    let statusDict = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as! [String:AnyObject]
                    
                    //将转化好的字典数据添加到准备好的数组中去
                    statusDicts.append(statusDict)
                }
                //将转化好的字典数组进行返回
                loadCacheStatusCallback(statuses:statusDicts)
                
            }catch let error as NSError{
                print("\(error)")
                loadCacheStatusCallback(statuses:nil)
            }
        }
    }

}

8.      使用`rowHeight`,`sectionFooterHeight` `sectionHeaderHeight`来设定固定的高,不要请求delegate

9.      cell的行高是根据内容变化的时候,只计算一次,将计算好的cell行高缓存起来

    /**
    1. 在考虑到tableView的优化的时候,rowHeight的计算最好不要设置预设行高,我们应该在代理方法中计算行高
    2. 当给tableView给定预估行高的时候,具体的行高会在cell进行展示的时候才去确定具体的值
     */
    override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
       //在模型中定义cellHeight的属性,
        //获取模型
        let status = self.userDatas![indexPath.row]
        //判断模型中有没有之前缓存过的行高
        if  (status.rowHeight != nil) {
            //存缓存中获取cell的rowheight
            return status.rowHeight!
        }
        
        //如果缓存中没有的还自己在去cell中获取
        let cell = tableView.dequeueReusableCellWithIdentifier(identifier) as! ALStatusCell
        
        cell.status = status
        
        //我们目的是在cell中获得cell的行高,根据view的大小获取行高
        let  rowHeight = cell.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
        //将行高的数据保存到模型的中
        status.rowHeight = rowHeight
        return rowHeight
    }

10.下载图片使用SDWebImage(异步操作)

11.cellForRowAtIndexPath不要做耗时操作

12.读取文件,写入文件,最好是放到子线程,或先读取好,在让tableView去显示

13.尽量不要去添加和移除view, 现将会用到的控件懒加载,要就显示,不要就隐藏

14.tableView滚动的时候,不要去做动画

15.cell里面的控件,约束最好不要使用remake,动态添加约束是比较耗性能的

16.cell里面的控件,背景最好是不透明的 (图层混合), view的背景颜色 clearColor 尽量少

17.图片圆角不要使用 layer.cornerRadius   最好使用异步绘制

异步绘制图片OC版本:

      ---->>>在此我举一个比较简单的例子来进行图片的绘制

打开系统的图库,进行选择图片,将选择好的图片赋值给所点击的控件:

步骤如下:

1.>   创建要显示图片的控件(要能接受点击事件),在创建控件的时候要指定控件的显示模式是UIViewContentModeScaleAspectFill
      属性(这个属性可以保证图片是等比例拉升进行显示的,图片本身不会变形)

-(UIButton *)pictureButtton{
    if (_pictureButtton == nil) {
        _pictureButtton = [[UIButton alloc] init];
        [_pictureButtton setImage:[UIImage imageNamed:@"AddPic"] forState:(UIControlStateNormal)];
        [_pictureButtton setImage:[UIImage imageNamed:@"compose_pic_add_highlighted"] forState:(UIControlStateHighlighted)];
        _pictureButtton.contentMode = UIViewContentModeScaleAspectFill;
        [_pictureButtton addTarget:self action:@selector(pictureButttonDidCick:) forControlEvents:(UIControlEventTouchUpInside)];
    }
    return _pictureButtton;
}

2.>   监听控件的点击事件,在监听事件的方法中创建图库选择器的对象,设置代理,进行跳转到图库的控制器

#pragma mark - 按钮点击绑定方法
-(void)pictureButttonDidCick:(UIButton*)button{
    //1 从相册选择
    UIImagePickerController* imagePickerVc = [[UIImagePickerController alloc] init];
    imagePickerVc.delegate = self;
    //2 弹出控制器
    [[NSNotificationCenter defaultCenter] postNotificationName:QYPictureChooseNotification object:imagePickerVc];
}


3. >  在图库控制器的代理方法(选择图片完成后调用的方法中),将选择的图片异步绘制到之前创建的控件上

-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info{
    [picker dismissViewControllerAnimated:YES completion:nil];
    UIImage* selectedImage = info[UIImagePickerControllerOriginalImage];
    //对绘制的图片进行异步绘制
    //等比例缩放
    CGFloat width = 300;
    CGFloat height = 300/selectedImage.size.width * selectedImage.size.height;
    [selectedImage asyncDrawPicture:CGSizeMake(width, height) callBack:^(UIImage *resultImage) {
        [self.pictureButtton setImage:resultImage forState:(UIControlStateNormal)];
        [self.pictureButtton setImage:resultImage forState:(UIControlStateHighlighted)];
    }];
}


4.>异步绘制图片的方法是UIImage的分类

@implementation UIImage (AsyncDraw)
-(void)asyncDrawPicture:(CGSize)size callBack:(void(^)(UIImage*))callBack{
    //1 开启上下文绘制图片
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        UIGraphicsBeginImageContext(size);
        
        [self drawInRect:CGRectMake(0, 0, size.width, size.height)];
        
        UIImage* resultImage = UIGraphicsGetImageFromCurrentImageContext();
        
        dispatch_async(dispatch_get_main_queue(), ^{
            callBack(resultImage);
        });
    });
}
@end


异步绘制图片Swift版本:

//异步绘制图片

extension UIImage{
    
    func al_AsyncDrawImage(var size:CGSize?,bgColor : UIColor? = UIColor.whiteColor(),isCorner:Bool = false ,drawFinish:(image:UIImage)->()){

        dispatch_async(dispatch_get_global_queue(0, 0)) {
            let starttime = CACurrentMediaTime()
            //先判断图片有没有传进来尺寸
            if size == nil{
            size  = self.size
            }
            
            //绘制的上下文的rect
            let rect = CGRect(origin: CGPointZero, size: size!)
            
            //开启图形上下文
            
            UIGraphicsBeginImageContextWithOptions(size!, bgColor != nil, UIScreen.mainScreen().scale)
            //设置背景颜色
            bgColor?.setFill()
            
            //填充
            UIRectFill(rect)
            
            //判断是否需要绘制圆角
            if isCorner{
            //绘制圆角
                let path = UIBezierPath(ovalInRect: rect)
                
                path.addClip()
            }
            
            self.drawInRect(rect)
            
            //获取图片
            let image = UIGraphicsGetImageFromCurrentImageContext()
            //结束图片上下文
            UIGraphicsEndImageContext()
            
            //打印出测试时间
            let endtime = CACurrentMediaTime()
//            print("\(endtime)-\(starttime)")
            //返回绘制好的图片
            dispatch_sync(dispatch_get_main_queue(), { 
                drawFinish(image: image)
            })
        }
        
    }
}


18.图层最好不要使用阴影, 阴影会导致离屏渲染

19.栅格化

20.AsyncDisplayKit -> 不使用UIKit (UIView) ->  (Node)

21.借助工具来测试性能(Profile - > instrument)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值