一次 TableView 性能优化经历

遇到了一个Tableview卡帧的问题,花了点时间才解决,记录一下吧。好了,废话不多说,先上张效果图:

1.gif

ps:其实是仿照nice的照片详情浏览效果

现在的照片详情页面是一个单独的页面(vc),用户想看其他的照片详情,需返回上一级页面,再点击进来,然后下个版本产品想改成上面那种效果。当时我想到两种方案:

一:用一个倾斜90°的tableview来做,简单,不用自己维护重用队列,每个cell放一个 vc 的view 就可以了,so easy。但是后面出现了问题,没记太清,当时也忘了截图,就换用第二种方案。

二:用scrollView来写,自己来维护重用队列,具体做法大家可以参考 UIScrollView 实践经验 (3.重用) 。最后“完美”地实现了需求,开始做别的需求去了。

因为当时在模拟器上开发,也没想到真机上会卡帧。过了1天,这个功能提交给测试,然后就发现了问题:在scrollView滚动的时候,明显的感觉到了卡帧,然后就开始优化。

ps:有关TableView的效果一定要跑真机!!有关TableView的效果一定要跑真机!!有关TableView的效果一定要跑真机!! (重要的事说三遍)

卡帧猜想

因为也没有仔细看那个vc以及cell中的代码,就大概猜想了一下卡帧的原因:

1.尼玛,该不会是 UIScrollView的重用 没写好?

断点验证了下,vc只会创建3个,重用没问题呀。

2.因为涉及重用,所有vc里面tableview的内容肯定不是一下子全请求出来的,每滚动一次才会去请求下个页面的数据,以及初始化页面。然后再看nice,忽然发现它滚动的时候,状态栏居然没有网络请求的小菊花!!难不成是一次请求的?应该不会吧,这么多数据呀。为了验证这种猜想,用 Charles 拦截下,结果nice也是每滚动次发次请求的:

2.gif

iOS开发工具-网络封包分析工具Charles

3.这个时候我又想到去搜nice的iOS工程师的github 和 博客,可惜github不能搜组织,就在微博搜了下

blob.png

blob.png

(互相关注 是后来事)

blob.png

最后找到了他的博客,但是可惜没有找到我想要的。。。

进入正题

不管什么原因,先跑下Instruments三件套吧(Time Profiler,Core Animation,GPU Driver)

3.gif

性能调优

好嘛,真是卡,一个一个看吧

1.首先排除了GPU的问题

blob.png

2.CPU

blob.png

这算多吗?我不太确定,对比 上面性能调优一文中的这段

blob.png

得了,还是看那个vc里面是怎么写得吧??

要声明一点的是,我们项目中没有用到model,用的全是字典…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 网络请求
- (void)refreshData
{
     HBStoryDetailFetcher *fetcher = [[HBStoryDetailFetcher alloc] init];
     fetcher.parameters = @{@ "id" : _story_id};
     [self runFetcher:fetcher forView:self.view success:^{
         _story = [fetcher.story mutableCopy];
         [self refreshCommentList];
     } failure:^(NSError *error){
         [self refreshCommentList];
     }];
}
- (void)refreshCommentList {
     HBCommentListFetcher *fetcher = [[HBCommentListFetcher alloc] init];
     fetcher.parameters = @{@ "story_id" : _story_id};
     [self runFetcher:fetcher forView:self.view success:^{
         _page = 0;
         _comments = [[NSMutableArray alloc] init];
         [_comments addObjectsFromArray:fetcher.comments];
         [_tableView reloadData];
         if  (fetcher.comments.count == 20) {
             [self addLoadMore];
         }
     } failure:^(NSError *error){
         [_tableView reloadData];
     }];
}
// UITableViewDataSource
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
     if  (indexPath.row == 0) {
         HBStoryDetailCell *cell = [tableView dequeueReusableCellWithIdentifier:@ "feedHomeCell"  forIndexPath:indexPath];
         cell.parent = self;
         cell.isStoryDetailView = YES;
         cell.indexPath = indexPath;
         cell.story = _story;
         cell.delegate = self;
         [cell updateUI];
         return  cell;
     }
     else
     {
         HBStoryCommentCell *cell = [tableView dequeueReusableCellWithIdentifier:@ "commentCell"  forIndexPath:indexPath];
         cell.parent = self;
         cell.indexPath = indexPath;
         cell.story = [_comments[indexPath.row-1] mutableCopy];
         [cell updateUI];
         return  cell;
     }
}
// UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
     if  (indexPath.row == 0) {
         return  [HBStoryDetailCell calculateHeightForStory:_story] - [HBStoryDetailCell commentLabelHeight:_story];
     }
     else
     {
         return  [HBStoryCommentCell calculateHeightForStory:_comments[indexPath.row-1]];
     }
     
}

首先,cell的高度没有缓存,这肯定有影响。另外,在打断点调试的过程中,发现refreshData的success block回调居然执行2次,这岂不是意味着tableview要reloadData两次,短时间刷新2次,肯定会卡啊,继续往里面看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (void)runWithSuccess:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *))failure
{
     HBLog(@ "正在加载:%@" ,_requestURL);
     
     //直接从网上加载数据
     if  (HBCachePolicyNetworkOnly == _cachePolicy) {
         [self loadFromNetWorkWithSuccess:success failure:failure];
         return ;
     }
     
     //先从缓存中取出数据,把结果返回,再判断缓存数据是否过期,如果过期,则再从网上取,取到后把结果后返回
     if  (HBCachePolicyCacheElseNetwork == _cachePolicy ) {
         [self loadFromCacheWithSuccess:success failure:failure];
         if  (self.cacheHasExpired) {
             [self loadFromNetWorkWithSuccess:success failure:failure];
         }
     }
}
- (BOOL)cacheHasExpired{
     
     NSDate *LoadFromNetworkFinishTime = [self getObjectFromCacheWithFileName:_dateFileName];
     
     if  (nil == LoadFromNetworkFinishTime) {
         return  YES;
     }
     
     if  ([[NSDate date] timeIntervalSinceDate:LoadFromNetworkFinishTime]>_maxCacheAge * 60){
         return  YES;
     }
     
     return  NO;
}
typedef NS_ENUM(int, HBCachePolicy)
{
     HBCachePolicyCacheElseNetwork,   //先从缓存中取出数据,把结果返回,再判断缓存数据是否过期,如果过期,则再从网上取,取到后把结果后返回
     HBCachePolicyNetworkOnly
};

HBStoryDetailFetcher.m

1
2
3
4
5
6
7
8
9
10
11
#import "HBCommentListFetcher.h"
@implementation HBCommentListFetcher
- (id)init {
     self = [ super  init];
     self.method = @ "POST" ;
     self.requestURL = @ "api/home/comment/query" ;
     self.cachePolicy = HBCachePolicyCacheElseNetwork;
     return  self;
}
...
@end

HBCommentListFetcher.m

1
2
3
4
5
6
7
8
9
10
#import "HBStoryDetailFetcher.h"
@implementation HBStoryDetailFetcher
- (id)init {
     self = [ super  init];
     self.requestURL = @ "api/home/story/get" ;
     self.cachePolicy = HBCachePolicyCacheElseNetwork;
     return  self;
}
...
@end

然后发现@property (assign,nonatomic) float maxCacheAge; //缓存过期时间 单位分钟 默认是0的,这尼玛缓存分分钟过期啊,不过想想之前是一个单独的vc,这样做也挺好的,不过现在滚动中请求就有点不好了。先把数据请求设置成只从网络中获取吧,看看效果先:

测试优化.gif

请相信我快速滑动的速度是一样一样滴!

blob.png

区域1是快速滑动的,感觉还可以,帧数也比较稳定。区域2是慢滑,在滚动到中间的时候还是感到有点小卡。不过现在已经好很多了,再接着优化,重点排查这几个方法:

参考:iOS性能优化

在 - (void)loadLikeUsers 方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#define kHBImageW (kScreenWidth-45)/8
- (void)loadLikeUsers
{
     ...
     for  (UIView * subview  in  [_usersLikeView subviews]) {
         [subview removeFromSuperview];
     }
     for (int i=0;i<_usersLikeArray.count ;i++)
     {
         _dictionary = [_usersLikeArray objectAtIndex:i];
         UIImageView *image = [[UIImageView alloc]init];
         image.frame = CGRectMake(i*kHBImageW, 0, 24, 24);
//            NSString *imageURL =[NSString stringWithFormat:@"%@",[_dictionary getString:@"avatar"]];
//            [image sd_setImageWithURL:[NSURL URLWithString:imageURL] placeholderImage:[UIImage imageNamed:@"DefaultAvatarSmall"]];
         
         [image sd_setImageWithURL:[NSURL URLWithString:[HBOSSHelper getThumbnail:[_dictionary getString:@ "avatar" ] compressLevel:1]] placeholderImage:[UIImage imageNamed:@ "DefaultAvatarSmall" ]];
         
         image.clipsToBounds = YES;
         image.userInteractionEnabled = YES;
         image.tag = i;
         UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showAvatarView:)];
         [image addGestureRecognizer:tap];
         [_usersLikeView addSubview:image];
         [image.layer setCornerRadius:12];
         if  (i == 7) {
             break ;
         }
     }
     ...
}

blob.png

很明显,反复创建View是不可取的…一般都会在初始化的时候全部创建,然后控制显示和隐藏。另外还有圆角。。。 (ps:这个View是照片下面的点赞头像)

参考:小心别让圆角成了你列表的帧数杀手

而且还发现下载下来的图片尺寸跟View的大小还不一致。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
+ (NSString *)getThumbnail:(NSString *)imageURL compressLevel:(NSInteger)level {
     NSArray *a = [imageURL componentsSeparatedByString:@ "/" ];
     if  (a.count < 4) {
         return  imageURL;
     }
     NSArray *b = [a[2] componentsSeparatedByString:@ "." ];
     if  (b.count < 4) {
         return  imageURL;
     }
     if  (![b[0] isEqualToString:@ "fitzerolesson" ]) {
         return  imageURL;
     }
     NSString *filename = a[3];
     NSString *attr;
     if  (level == 0) {
         attr = @ "@1e_50w_50h_1c_0i_1o_90Q_1x.jpg" ;
     }
     else  if  (level == 1) {
         attr = @ "@1e_100w_100h_1c_0i_1o_90Q_1x.jpg" ;
     else  if  (level == 2) {
         attr = @ "@1e_360w_360h_1c_0i_1o_90Q_1x.jpg" ;
     }
     else  if  (level == 3) {
         attr = @ "@1e_640w_640h_1c_0i_1o_90Q_1x.jpg" ;
     } else  {
         attr = @ "@1e_720w_720h_1c_0i_1o_90Q_1x.jpg" ;
     }
     return  [NSString stringWithFormat:@ "http://image.hotbody.cn/%@%@" , filename, attr];
}

得了,修改下下载图片的尺寸。

在 - (void)loadComments 方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)loadComments
{
     if  (_isStoryDetailView) {
         _commentView.hidden = YES;
     }
     else
     {
         _commentView.hidden = NO;
     }
     
     CGFloat replyViewHeight = [HBStoryDetailCell calculateHeightForStory1:_story];
     _commentView.keepHeight.equal = [HBStoryDetailCell calculateHeightForStory:_story] - replyViewHeight+10;
     for  (UIView * subview  in  [_commentView subviews]) {
         [subview removeFromSuperview];
     }
     ...
}

我隐约记得,vc中cellForRow方法中 HBStoryDetailCell 的 isStoryDetailView 属性是设为YES的。这。。。为毛没个return呢?都隐藏了,下面还走毛线啊??

网络请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)loadFromNetWorkWithSuccess:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *))failure
{
     HBLog(@ "正在从网络加载:%@" ,_requestURL);
     AFHTTPRequestOperation *o = [[HBHTTPRequestManager sharedManager] HTTPCacheRequestOperationWithHTTPMethod:_method URLString:_requestURL parameters:_parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
         _finishLoadFromNetwork = YES;
         NSError *error = [self processData:responseObject];
         if  (error == nil){
             [self saveObject:[NSDate date] ToCacheWithFileName:_dateFileName];
             [self saveObject:responseObject ToCacheWithFileName:self.fileName];
             success(responseObject);
         } else {
             failure(operation, error);
         }
         [self dispose];
         
     } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
         _finishLoadFromNetwork = YES;
         failure(operation, error);
         [self dispose];
         
     }];
     
     [[HBHTTPRequestManager sharedManager].operationQueue addOperation:o];
}
- (void)saveObject:(id)theObject ToCacheWithFileName:(NSString *)theFileName{
     
     NSString *cacheFile = [[Util sharedInstance].dataPath stringByAppendingPathComponent:theFileName];
     NSMutableData *data = [[NSMutableData alloc] init];
     NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
     [archiver encodeObject:theObject forKey:kSerializationKey];
     [archiver finishEncoding];
     [data writeToFile:cacheFile atomically:YES];
}

换成子线程写入文件

另外最后改了一下scrollView滚动时网络请求的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
     if  (scrollView != self.scrollView) {
         return ;
     }
     
     self.startOffsetX = scrollView.contentOffset.x;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
     
     if  (scrollView != self.scrollView) {
         return ;
     }
     
     NSInteger page = 0;
     
     CGFloat contentOffsetX = scrollView.contentOffset.x;
     contentOffsetX = MAX(contentOffsetX, 0);
     
     page = scrollView.contentOffset.x / scrollView.frame.size.width;
     
     if  (contentOffsetX < self.startOffsetX) {
         page += 1;
         
         BOOL isPreviousPage = [[NSNumber numberWithFloat:contentOffsetX] intValue] % [[NSNumber numberWithFloat:scrollView.frame.size.width] intValue] <= 2 ;
         
         if  (isPreviousPage) {
             page -= 1;
         }
     }
// NSInteger page = roundf(scrollView.contentOffset.x / scrollView.frame.size.width);
     
     page = MAX(page, 0);
     page = MIN(page, self.storys.count - 1);
     
     [self loadPage:page];
}

大体改完之后来看下效果:

最后.gif

请相信我快速滑动的速度是一样一样滴!

个人感觉快滑和慢滑时的流畅度还可以呢,只不过到最后heightForRowAtIndexPath 这个方法还是没优化,主要是感觉太麻烦

最后

个人感觉,其实UITableView的优化要注意的点就那么多,大家平时多注意下应该就不会有什么问题了。

参考与推荐

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值