你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 一次 TableView 性能優化經歷

一次 TableView 性能優化經歷

編輯:IOS開發基礎

0.jpg

作者:@__weak_Point 授權本站轉載。

題外話

前段時間才換了工作,從面試准備到入職新公司,大概有半個多月時間吧,感慨頗深。找工作,太多運氣成分在裡面。有一些話想分享給大家:1.多認識一些這個行業的朋友,說不定你的下份工作就是其中的一個朋友介紹的。2.最好不要在7、8月份換工作,因為真的很熱。

緣由

來到新公司後,一開始是熟悉項目、代碼,以及改bug(填坑),然後上周開始做新的功能(准備挖坑)。生活不就是不停的挖坑和填坑嗎?!遇到了一個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,用的全是字典…

// 網絡請求
- (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次,肯定會卡啊,繼續往裡面看

- (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

#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

#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 方法中

#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的大小還不一致。。。

+ (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 方法中

- (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呢?都隱藏了,下面還走毛線啊??

網絡請求

- (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滾動時網絡請求的位置

#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的優化要注意的點就那麼多,大家平時多注意下應該就不會有什麼問題了。

參考與推薦

  • About Instruments

  • iOS應用性能調優的25個建議和技巧

  • iOS性能優化

  • UITableView優化技巧

  • 小心別讓圓角成了你列表的幀數殺手

  • 提升UITableView性能-復雜頁面的優化

  • UITableView 滾動流暢性優化

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved