如何实现UITableView优化
2016-05-07 00:54
453 查看
一.为什么要进行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)
相关文章推荐
- Codeforces 670E Correct Bracket Sequence Editor (list模拟)
- GUID是什么?
- 【SSH网上商城项目实战03】使用EasyUI搭建后台页面框架
- 支持placeholder和自适配高度的TextView控件
- MySQL开启慢查询日志log-slow-queries的方法
- mysql 5.5 开启慢日志slow log的方法(log_slow_queries)
- Tricky Priority Queue
- pt-query-digest查询日志分析工具
- storyboard 让TableViewCell的分割线顶到最左边
- STL学习——Priority_queue篇
- LINQ语句中的.AsEnumerable() 和 .AsQueryable()的区别
- STL学习——Stack/Queue篇
- OCiOS动效设计:UITableView 实现滚动视差效果
- 执行build -ceZ 编译驱动前的小动作1-setenv.bat
- STL学习——Deque篇
- UISenior 之 CoreData初级
- 模拟实现英汉字典(使用key/value形式的哈希表)
- AYITACM2016省赛第三周I - Optimal Array Multiplication Sequence(dp)
- JAVA的堆实现: PriorityQueue
- UVA 348 & ZOJ 1276 Optimal Array Multiplication Sequence(dp , 矩阵链相乘问题)