您的位置:首页 > 产品设计 > UI/UE

如何实现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)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: