一.为什么要进行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)];
}];
}
@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)