iOS之VVeboTableView性能探究之路

背景

話說(shuō)如果項(xiàng)目越來(lái)越復(fù)雜,頁(yè)面隨之也會(huì)變得復(fù)雜,最終導(dǎo)致的就是性能優(yōu)化問(wèn)題。性能優(yōu)化方面也是比較多的,我們通過(guò)研究VVeboTableView這個(gè)項(xiàng)目的優(yōu)化點(diǎn)進(jìn)而學(xué)習(xí)優(yōu)化思路。

其實(shí)項(xiàng)目?jī)?yōu)化最終僅僅就是一個(gè)目的,頁(yè)面流暢度達(dá)到60FPS,這個(gè)項(xiàng)目通過(guò)優(yōu)化甚至在低端機(jī)都表現(xiàn)良好,接下來(lái)就讓我們深入探究一下它是如何做到的。

不過(guò)關(guān)于優(yōu)化一般都是不要過(guò)早的大方面優(yōu)化,最好是項(xiàng)目穩(wěn)定之后,具體針對(duì)某一具體的復(fù)雜沒(méi)有達(dá)到60FPS的頁(yè)面進(jìn)行優(yōu)化。

探究之路

滑動(dòng)過(guò)程中不加載視圖

首先是VVeboTableView實(shí)現(xiàn)了UIScrollViewDelegate的幾個(gè)方法,這些方法中最關(guān)鍵的就是- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset這個(gè)方法,在我們抬手的那一刻,通過(guò)此時(shí)手指的位置計(jì)算出當(dāng)前頁(yè)面需要加載的所有UITableViewCell。屏幕外上下各加3個(gè)。

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    [needLoadArr removeAllObjects];
}

//按需加載 - 如果目標(biāo)行與當(dāng)前行相差超過(guò)指定行數(shù),只在目標(biāo)滾動(dòng)范圍的前后指定3行加載。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{

    NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
    NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
    NSInteger skipCount = 8;
    if (labs(cip.row-ip.row)>skipCount) {
        NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
        NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
        if (velocity.y<0) {
            NSIndexPath *indexPath = [temp lastObject];
            if (indexPath.row+3<datas.count) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
            }
        } else {
            NSIndexPath *indexPath = [temp firstObject];
            if (indexPath.row>3) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
            }
        }
        [needLoadArr addObjectsFromArray:arr];
    }
}

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{
    scrollToToping = YES;
    return YES;
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
    NSLog(@"scrollViewDidEndScrollingAnimation\n");
    scrollToToping = NO;
    [self loadContent];
}

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView{
    scrollToToping = NO;
    [self loadContent];
}

緊接著我們來(lái)看UITableView加載cell的方法,在此方法中是調(diào)用了
[self drawCell:cell withIndexPath:indexPath]此方法,此方法實(shí)現(xiàn)如下:

- (void)drawCell:(VVeboTableViewCell *)cell withIndexPath:(NSIndexPath *)indexPath{
    NSDictionary *data = [datas objectAtIndex:indexPath.row];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    [cell clear];
    cell.data = data;
    if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
        [cell clear];
        return;
    }
    if (scrollToToping) {
        return;
    }
    [cell draw];
}

此方法中通過(guò)判斷是否需要繪制cell,如果不需要會(huì)返回空白。所以在滑動(dòng)的過(guò)程中我們會(huì)看到一些空白頁(yè)面

減少視圖的層級(jí)和數(shù)量

//將主要內(nèi)容繪制到圖片上
- (void)draw{
    if (drawed) {
        return;
    }
    NSInteger flag = drawColorFlag;
    drawed = YES;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        CGRect rect = [_data[@"frame"] CGRectValue];
        UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
        CGContextRef context = UIGraphicsGetCurrentContext();
        [[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];
        CGContextFillRect(context, rect);
        if ([_data valueForKey:@"subData"]) {
            [[UIColor colorWithRed:243/255.0 green:243/255.0 blue:243/255.0 alpha:1] set];
            CGRect subFrame = [_data[@"subData"][@"frame"] CGRectValue];
            CGContextFillRect(context, subFrame);
            [[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set];
            CGContextFillRect(context, CGRectMake(0, subFrame.origin.y, rect.size.width, .5));
        }

        {
            float leftX = SIZE_GAP_LEFT+SIZE_AVATAR+SIZE_GAP_BIG;
            float x = leftX;
            float y = (SIZE_AVATAR-(SIZE_FONT_NAME+SIZE_FONT_SUBTITLE+6))/2-2+SIZE_GAP_TOP+SIZE_GAP_SMALL-5;
            [_data[@"name"] drawInContext:context withPosition:CGPointMake(x, y) andFont:FontWithSize(SIZE_FONT_NAME)
                             andTextColor:[UIColor colorWithRed:106/255.0 green:140/255.0 blue:181/255.0 alpha:1]
                                andHeight:rect.size.height];
            y += SIZE_FONT_NAME+5;
            float fromX = leftX;
            float size = [UIScreen screenWidth]-leftX;
            NSString *from = [NSString stringWithFormat:@"%@  %@", _data[@"time"], _data[@"from"]];
            [from drawInContext:context withPosition:CGPointMake(fromX, y) andFont:FontWithSize(SIZE_FONT_SUBTITLE)
                   andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]
                      andHeight:rect.size.height andWidth:size];
        }

        {
            CGRect countRect = CGRectMake(0, rect.size.height-30, [UIScreen screenWidth], 30);
            [[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];
            CGContextFillRect(context, countRect);
            float alpha = 1;

            float x = [UIScreen screenWidth]-SIZE_GAP_LEFT-10;
            NSString *comments = _data[@"comments"];
            if (comments) {
                CGSize size = [comments sizeWithConstrainedToSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) fromFont:FontWithSize(SIZE_FONT_SUBTITLE) lineSpace:5];

                x -= size.width;
                [comments drawInContext:context withPosition:CGPointMake(x, 8+countRect.origin.y)
                                andFont:FontWithSize(12)
                           andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]
                              andHeight:rect.size.height];
                [[UIImage imageNamed:@"t_comments.png"] drawInRect:CGRectMake(x-5, 10.5+countRect.origin.y, 10, 9) blendMode:kCGBlendModeNormal alpha:alpha];
                commentsRect = CGRectMake(x-5, self.height-50, [UIScreen screenWidth]-x+5, 50);
                x -= 20;
            }

            NSString *reposts = _data[@"reposts"];
            if (reposts) {
                CGSize size = [reposts sizeWithConstrainedToSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) fromFont:FontWithSize(SIZE_FONT_SUBTITLE) lineSpace:5];

                x -= MAX(size.width, 5)+SIZE_GAP_BIG;
                [reposts drawInContext:context withPosition:CGPointMake(x, 8+countRect.origin.y)
                                andFont:FontWithSize(12)
                           andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]
                              andHeight:rect.size.height];

                [[UIImage imageNamed:@"t_repost.png"] drawInRect:CGRectMake(x-5, 11+countRect.origin.y, 10, 9) blendMode:kCGBlendModeNormal alpha:alpha];
                repostsRect = CGRectMake(x-5, self.height-50, commentsRect.origin.x-x, 50);
                x -= 20;
            }

            [@"???" drawInContext:context
                     withPosition:CGPointMake(SIZE_GAP_LEFT, 8+countRect.origin.y)
                          andFont:FontWithSize(11)
                     andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:.5]
                        andHeight:rect.size.height];

            if ([_data valueForKey:@"subData"]) {
                [[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set];
                CGContextFillRect(context, CGRectMake(0, rect.size.height-30.5, rect.size.width, .5));
            }
        }

        UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        dispatch_async(dispatch_get_main_queue(), ^{
            if (flag==drawColorFlag) {
                postBGView.frame = rect;
                postBGView.image = nil;
                postBGView.image = temp;
            }
        });
    });
    [self drawText];
    [self loadThumb];
}

通過(guò)以上代碼我們可以看出,將主要的內(nèi)容比如名字、評(píng)論圖標(biāo)和數(shù)量、轉(zhuǎn)發(fā)圖標(biāo)和數(shù)量等一些內(nèi)容全部繪制到了一張圖片上,然后設(shè)置到了UIImageView

通過(guò)異步繪制視圖,主線程進(jìn)行設(shè)置

上邊的代碼我們看到在繪制的過(guò)程中是在全局并發(fā)隊(duì)列中進(jìn)行的,并且最終回到了主線程進(jìn)行設(shè)置,而且這也是必須要這樣做的

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  ...
  dispatch_async(dispatch_get_main_queue(), ^{
    if (flag==drawColorFlag) {
        postBGView.frame = rect;
        postBGView.image = nil;
        postBGView.image = temp;
    }
});
}
[self drawText];
[self loadThumb];

接下來(lái)讓我看一下[self drawText]方法的實(shí)現(xiàn)。

- (void)drawText{
    if (label==nil||detailLabel==nil) {
        [self addLabel];
    }
    label.frame = [_data[@"textRect"] CGRectValue];
    [label setText:_data[@"text"]];
    if ([_data valueForKey:@"subData"]) {
        detailLabel.frame = [[_data valueForKey:@"subData"][@"textRect"] CGRectValue];
        [detailLabel setText:[_data valueForKey:@"subData"][@"text"]];
        detailLabel.hidden = NO;
    }
}

這個(gè)方法自定了一個(gè)Label,在setText方法中同樣是異步繪制內(nèi)容,并且底層實(shí)現(xiàn)是使用CoreText性能方面也是比較優(yōu)越

下邊的代碼我稍微做了精簡(jiǎn)

- (void)setText:(NSString *)text{
    ....
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSString *temp = text;
        _text = text;
        CGSize size = self.frame.size;
        size.height += 10;
        UIGraphicsBeginImageContextWithOptions(size, ![self.backgroundColor isEqual:[UIColor clearColor]], 0);
        CGContextRef context = UIGraphicsGetCurrentContext();
        if (context==NULL) {
            return;
        }
        if (![self.backgroundColor isEqual:[UIColor clearColor]]) {
            [self.backgroundColor set];
            CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
        }
        CGContextSetTextMatrix(context,CGAffineTransformIdentity);
        CGContextTranslateCTM(context,0,size.height);
        CGContextScaleCTM(context,1.0,-1.0);

        //Determine default text color
        UIColor* textColor = self.textColor;

        //Set line height, font, color and break mode
        CGFloat minimumLineHeight = self.font.pointSize,maximumLineHeight = minimumLineHeight, linespace = self.lineSpace;
        CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)self.font.fontName, self.font.pointSize,NULL);
        CTLineBreakMode lineBreakMode = kCTLineBreakByWordWrapping;
        CTTextAlignment alignment = CTTextAlignmentFromUITextAlignment(self.textAlignment);
        //Apply paragraph settings
        CTParagraphStyleRef style = CTParagraphStyleCreate((CTParagraphStyleSetting[6]){
            {kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment},
            {kCTParagraphStyleSpecifierMinimumLineHeight,sizeof(minimumLineHeight),&minimumLineHeight},
            {kCTParagraphStyleSpecifierMaximumLineHeight,sizeof(maximumLineHeight),&maximumLineHeight},
            {kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(linespace), &linespace},
            {kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(linespace), &linespace},
            {kCTParagraphStyleSpecifierLineBreakMode,sizeof(CTLineBreakMode),&lineBreakMode}
        },6);

        NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)font,(NSString*)kCTFontAttributeName,
                                    textColor.CGColor,kCTForegroundColorAttributeName,
                                    style,kCTParagraphStyleAttributeName,
                                    nil];

        //Create attributed string, with applied syntax highlighting
        NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:text attributes:attributes];
        CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)[self highlightText:attributedStr];

        //Draw the frame
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);

        CGRect rect = CGRectMake(0, 5,(size.width),(size.height-5));

        if ([temp isEqualToString:text]) {
            [self drawFramesetter:framesetter attributedString:attributedStr textRange:CFRangeMake(0, text.length) inRect:rect context:context];
            CGContextSetTextMatrix(context,CGAffineTransformIdentity);
            CGContextTranslateCTM(context,0,size.height);
            CGContextScaleCTM(context,1.0,-1.0);
            UIImage *screenShotimage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            dispatch_async(dispatch_get_main_queue(), ^{
                CFRelease(font);
                CFRelease(framesetter);
                CFRelease(style);
                [[attributedStr mutableString] setString:@""];

                if (drawFlag==flag) {
                    if (isHighlight) {
                      ....
                    } else {
                        if ([temp isEqualToString:text]) {
                            ...
                            highlightImageView.image = nil;
                            labelImageView.image = nil;
                            labelImageView.image = screenShotimage;
                        }
                    }
//                    [self debugDraw];//繪制可觸摸區(qū)域
                }
            });
        }
    });
}

這個(gè)實(shí)現(xiàn)與- (void)drawInContext:(CGContextRef)context withPosition:(CGPoint)p andFont:(UIFont *)font andTextColor:(UIColor *)color andHeight:(float)height這個(gè)方法的實(shí)現(xiàn)很相似

接下來(lái)就是[self loadThumb];這個(gè)方法這個(gè)方法其實(shí)關(guān)鍵就是應(yīng)用了SDWebimage加載圖片,因?yàn)榇丝蚣茉诩虞d圖片已經(jīng)幫我優(yōu)化了。(異步解壓圖片,并且將圖片繪制到畫(huà)布上,從而不會(huì)使解壓數(shù)據(jù)在內(nèi)存中駐留)

總結(jié)

作者通過(guò)以上三點(diǎn)確實(shí)做到了性能極大的提高。不過(guò)我在研究的過(guò)程中遇到了兩個(gè)bug:

  • 代碼中有內(nèi)存泄漏,主要是由于有一處CTParagraphStyleRef style這個(gè)對(duì)象沒(méi)有釋放
  • 當(dāng)我單手使勁向上滑動(dòng),界面停下的時(shí)候頁(yè)面不會(huì)加載數(shù)據(jù)。其實(shí)就是由于監(jiān)聽(tīng)了- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset在這個(gè)方法導(dǎo)致的

在以上三個(gè)優(yōu)化點(diǎn)的基礎(chǔ)上其實(shí)我們還可以做一個(gè)比較明顯的優(yōu)化就是將繪制結(jié)果用NSCache緩存起來(lái),沒(méi)必要每次都進(jìn)行繪制。加上這個(gè)性能會(huì)有更大的提升

加上最后一點(diǎn)總共四點(diǎn),除了第一點(diǎn)有的項(xiàng)目不會(huì)接受外,其他三點(diǎn)和SDWebimage的優(yōu)化方式只要我們?cè)陧?xiàng)目適當(dāng)?shù)氖褂?,基本上能夠解決很大一部分的性能問(wèn)題的

我的博客

FlyOceanFish

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容