VVeboTableView 源碼解析

此次分享一個關於性能優化的源碼。git

咱們知道UITabelView在iOS開發中扮演者舉足輕重的角色,由於它是iOS開發中使用頻率很是高的控件之一:幾乎每一個app都離不開它,所以,UITabelView的性能將直接影響這個app的性能。程序員

若是UITabelView裏的cell設計的比較簡單,那麼即便不作相應的優化,對性能的影響也不會很大。github

可是,當cell裏面涉及到圖文混排,cell高度不都相等的設計時,若是不進行一些操做的話,會影響性能,甚至會出現卡頓,形成很是很差的用戶體驗。正則表達式

最近在看一些iOS性能優化的文章,我找到了VVeboTableView這個框架。嚴格來講這個不屬於框架,而是做者用本身的方式優化UITableView的一個實踐。編程

VVeboTableView展現了各類類型的cell(轉發貼,原貼,有圖,無圖)。雖然樣式比較複雜,可是滑動起來性能卻很好:我在個人iphone 4s上進行了Core Animation測試,在滑動的時候幀率沒有低於56,並且也沒有以爲有半點卡頓,那麼他是怎麼作到的呢?數組

看了源碼以後,我把做者的思路整理了出來:緩存

優化思路圖

從圖中咱們能夠看出,做者從減小CPU/GPU計算量,按需加載cell,異步處理cell三大塊來實現對UITableView的優化。下面我就從左到右,從上到下,結合代碼來展現一下做者是如何實現每一點的。性能優化

1. 減小CPU/GPU計算量

1.1 cell的重用機制

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    //cell重用
    VVeboTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    
    if (cell==nil) {
        cell = [[VVeboTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    
    //繪製
    [self drawCell:cell withIndexPath:indexPath];
    return cell;
}
複製代碼

這部分就不贅述了,相信你們都已經掌握。網絡

1.2 將cell高度和 cell裏的控件的frame緩存在model裏

這一步咱們須要在字典轉模型裏統一計算(不須要看代碼細節,只須要知道這裏在模型裏保存了須要保存的控件的frame和整個cell的高度便可):app

- (void)loadData{
    
    ...
    
    for (NSDictionary *dict in temp) {
        
        NSDictionary *user = dict[@"user"];
        
        ...
        
        NSDictionary *retweet = [dict valueForKey:@"retweeted_status"];
        
        if (retweet) {
            
            NSMutableDictionary *subData = [NSMutableDictionary dictionary];
            ...
            
            {
                float width = [UIScreen screenWidth]-SIZE_GAP_LEFT*2;
                CGSize size = [subData[@"text"] sizeWithConstrainedToWidth:width fromFont:FontWithSize(SIZE_FONT_SUBCONTENT) lineSpace:5];
                NSInteger sizeHeight = (size.height+.5);
                subData[@"textRect"] = [NSValue valueWithCGRect:CGRectMake(SIZE_GAP_LEFT, SIZE_GAP_BIG, width, sizeHeight)];
                sizeHeight += SIZE_GAP_BIG;
                if (subData[@"pic_urls"] && [subData[@"pic_urls"] count]>0) {
                    sizeHeight += (SIZE_GAP_IMG+SIZE_IMAGE+SIZE_GAP_IMG);
                }
                sizeHeight += SIZE_GAP_BIG;
                subData[@"frame"] = [NSValue valueWithCGRect:CGRectMake(0, 0, [UIScreen screenWidth], sizeHeight)];
            }
            
            data[@"subData"] = subData;
        
        
        
            float width = [UIScreen screenWidth]-SIZE_GAP_LEFT*2;
            CGSize size = [data[@"text"] sizeWithConstrainedToWidth:width fromFont:FontWithSize(SIZE_FONT_CONTENT) lineSpace:5];
            NSInteger sizeHeight = (size.height+.5);
            ...
            sizeHeight += SIZE_GAP_TOP+SIZE_AVATAR+SIZE_GAP_BIG;
            if (data[@"pic_urls"] && [data[@"pic_urls"] count]>0) {
                sizeHeight += (SIZE_GAP_IMG+SIZE_IMAGE+SIZE_GAP_IMG);
            
            
            NSMutableDictionary *subData = [data valueForKey:@"subData"];
            
            if (subData) {
                sizeHeight += SIZE_GAP_BIG;
                CGRect frame = [subData[@"frame"] CGRectValue];
                ...
                sizeHeight += frame.size.height;
                data[@"subData"] = subData;
            }
            
            sizeHeight += 30;
            data[@"frame"] = [NSValue valueWithCGRect:CGRectMake(0, 0, [UIScreen screenWidth], sizeHeight)];
        }
        [datas addObject:data];
    }
}

//獲取高度緩存
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    NSDictionary *dict = datas[indexPath.row];
    float height = [dict[@"frame"] CGRectValue].size.height;
    return height;
}
複製代碼

這裏咱們能夠看到,做者根據帖子類型的不一樣:原貼(subData)的存在與否),來逐漸疊加cell的高度。

緩存的高度在heightForRowAtIndexPath:方法裏使用。而緩存的控件的frame的使用,咱們在下面講解繪製cell的代碼裏詳細介紹。

1.3 減小cell內部控件的層級

咱們先來看一下一個帶有原貼的轉發貼的佈局:

佈局

可能有小夥伴會將上中下這三個部分各自封裝成一個view,再經過每一個view來管理各自的子view。可是這個框架的做者卻將它們都排列到一層上。

減小了子view的層級,有助於減小cpu對各類約束的計算。這在子view的數量,層級都不少的狀況下對cpu的壓力會減輕不少。

1.4 經過覆蓋圓角圖片來實現頭像的圓角效果

//頭像,frame固定
    avatarView = [UIButton buttonWithType:UIButtonTypeCustom];//[[VVeboAvatarView alloc] initWithFrame:avatarRect];
    avatarView.frame = CGRectMake(SIZE_GAP_LEFT, SIZE_GAP_TOP, SIZE_AVATAR, SIZE_AVATAR);
    avatarView.backgroundColor = [UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1];
    avatarView.hidden = NO;
    avatarView.tag = NSIntegerMax;
    avatarView.clipsToBounds = YES;
    [self.contentView addSubview:avatarView];
    
    //覆蓋在頭像上面的圖片,製造圓角效果:frame
    cornerImage = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, SIZE_AVATAR+5, SIZE_AVATAR+5)];
    cornerImage.center = avatarView.center;
    cornerImage.image = [UIImage imageNamed:@"corner_circle@2x.png"];
    cornerImage.tag = NSIntegerMax;
    [self.contentView addSubview:cornerImage];
複製代碼

在這裏,做者沒有使用任何複雜的技術來實現圖片的圓角(使用layer或者裁剪圖片),只是將一張圓角顏色和cell背景色一致的圖片覆蓋在了原來的頭像上,實現了圓角的效果(可是這個方法不太適用於有多個配色方案的app)。

2. 按需加載cell

上文提到過,UITableView持有一個needLoadArr數組,它保存着須要刷新的cell的NSIndexPath

咱們先來看一下needLoadArr是如何使用的:

2.1 在cellForRow:方法裏只加載可見cell

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    ...
    [self drawCell:cell withIndexPath:indexPath];
    ...
}

- (void)drawCell:(VVeboTableViewCell *)cell withIndexPath:(NSIndexPath *)indexPath{
    
    NSDictionary *data = [datas objectAtIndex:indexPath.row];
    
    ...
    cell.data = data;
    
    //當前的cell的indexPath不在needLoadArr裏面,不用繪製
    if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
        [cell clear];
        return;
    }
    
    //將要滾動到頂部,不繪製
    if (scrollToToping) {
        return;
    }
    
    //真正繪製cell的代碼
    [cell draw];
}
複製代碼

2.2 監聽tableview的快速滾動,保存目標滾動範圍的先後三行的索引

知道了如何使用needLoadArr,咱們看一下needLoadArr裏面的元素是如何被添加和刪除的。

添加元素NSIndexPath

//按需加載 - 若是目標行與當前行相差超過指定行數,只在目標滾動範圍的先後指定3行加載。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
    
    //targetContentOffset : 中止後的contentOffset
    NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
    
    //當前可見第一行row的index
    NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
    
    //設置最小跨度,當滑動的速度很快,超過這個跨度時候執行按需加載
    NSInteger skipCount = 8;
    
    //快速滑動(跨度超過了8個cell)
    if (labs(cip.row-ip.row)>skipCount) {
        
        //某個區域裏的單元格的indexPath
        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];
            
            //超過倒數第3個
            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];
            //超過正數第3個
            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]];
            }
        }
        //添加arr裏的內容到needLoadArr的末尾
        [needLoadArr addObjectsFromArray:arr];
    }
}
複製代碼

知道了如何向needLoadArr裏添加元素,如今看一下什麼時候(重置)清理這個array:

移除元素NSIndexPath

//用戶觸摸時第一時間加載內容
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    
    if (!scrollToToping) {
        [needLoadArr removeAllObjects];
        [self loadContent];
    }
    return [super hitTest:point withEvent:event];
}


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

//將要滾動到頂部
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{
    scrollToToping = YES;
    return YES;
}

//中止滾動
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
    scrollToToping = NO;
    [self loadContent];
}

//滾動到了頂部
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView{
    scrollToToping = NO;
    [self loadContent];
}
複製代碼

咱們能夠看到,當手指觸碰到tableview時 和 開始拖動tableview的時候就要清理這個數組。

並且在手指觸碰到tableview時和 tableview中止滾動後就會執行loadContent方法,用來加載可見區域的cell。

loadContent方法的具體實現:

- (void)loadContent{
    
    //正在滾動到頂部
    if (scrollToToping) {
        return;
    }
    
    //可見cell數
    if (self.indexPathsForVisibleRows.count<=0) {
        return;
    }
    
    //觸摸的時候刷新可見cell
    if (self.visibleCells&&self.visibleCells.count>0) {
        for (id temp in [self.visibleCells copy]) {
            VVeboTableViewCell *cell = (VVeboTableViewCell *)temp;
            [cell draw];
        }
    }
}
複製代碼

在這裏注意一下,tableview的visibleCells屬性是可見的cell的數組。

3. 異步處理cell

在講解如何異步處理cell以前,咱們大體看一下這個cell都有哪些控件:

控件名稱

瞭解到控件的名稱,位置以後,咱們看一下做者是如何佈局這些控件的:

控件佈局
在上面能夠大體看出來,除了須要異步網絡加載的頭像(avatarView)和帖子圖片(multiPhotoScrollView),做者都將這些控件畫在了一張圖上面(postBgView)。

並且咱們能夠看到,在postBgView上面須要異步顯示的內容分爲四種:

  1. UIImageView:本地圖片(comments, more,reposts)。
  2. UIView:背景,分割線(topLine)。
  3. NSString:name,from字符串。
  4. Label:原貼的detailLabel 和 當前貼的 label。

下面結合代碼來說解這四種繪製:

首先看一下cell內部的核心繪製方法:

如今咱們來看一下cell繪製的核心方法,draw方法:

//將cell的主要內容繪製到圖片上
- (void)draw{
    
    //drawed = YES說明正在繪製,則當即返回。由於繪製是異步的,因此在開始繪製以後須要當即設爲yes,防止重複繪製
    if (drawed) {
        return;
    }
    
    //標記當前的繪製
    NSInteger flag = drawColorFlag;
    
    drawed = YES;
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        //獲取整個cell的frame,已經換存在模型裏了
        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];
        
        //經過rect填充背景顏色
        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];
            
            //繪製名字下面的info
            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];
                
                //評論圖片(bundle裏的圖片)
                [[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];
               
                //轉發圖片(bundle裏的圖片)
                [[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));
            }
        }
        
        //將整個contex轉化爲圖片,賦給背景imageview
        UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        dispatch_async(dispatch_get_main_queue(), ^{
            if (flag==drawColorFlag) {
                postBGView.frame = rect;
                postBGView.image = nil;
                postBGView.image = temp;
            }
        });
    });
    
    //繪製兩個label的text
    [self drawText];
    
    //加載帖子裏的網路圖片,使用SDWebImage
    [self loadThumb];
}
複製代碼

下面抽出每一種繪製內容的代碼,分別講解:

3.1 異步加載網絡圖片

關於網絡圖片的異步加載和緩存,做者使用了第三方框架:SDWebImage

- (void)setData:(NSDictionary *)data{
    _data = data;
    [avatarView setBackgroundImage:nil forState:UIControlStateNormal];
    if ([data valueForKey:@"avatarUrl"]) {
        NSURL *url = [NSURL URLWithString:[data valueForKey:@"avatarUrl"]];
        [avatarView sd_setBackgroundImageWithURL:url forState:UIControlStateNormal placeholderImage:nil options:SDWebImageLowPriority];
    }
}
複製代碼

對於SDWebImage,我相信你們都不會陌生,我前一陣寫了一篇源碼解析,有興趣的話能夠看一下:SDWebImage源碼解析

3.2 異步繪製本地圖片

本地圖片的繪製,只須要提供圖片在bundle內部的名字和frame就能夠繪製:

[[UIImage imageNamed:@"t_comments.png"] drawInRect:CGRectMake(x-5, 10.5+countRect.origin.y, 10, 9) blendMode:kCGBlendModeNormal alpha:alpha];
複製代碼

###3.3 異步繪製UIView

對於UIView的繪製,咱們只須要知道要繪製的UIView的frame和顏色便可:

//背景顏色
[[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];
        
//經過rect填充背景顏色
CGContextFillRect(context, rect);
複製代碼

講到如今,就剩下了關於文字的繪製,包括脫離了UILabel的純文本的繪製和UILabel裏文本的繪製,咱們先說一下關於簡單的純NSString的繪製:

3.4 異步繪製NSString

做者經過傳入字符串的字體,顏色和行高,以及位置就實現了純文本的繪製:

//繪製名字
[_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];
複製代碼

這個方法是做者在NSString的一個分類裏自定義的,咱們看一下它的實現:

- (void)drawInContext:(CGContextRef)context withPosition:(CGPoint)p andFont:(UIFont *)font andTextColor:(UIColor *)color andHeight:(float)height andWidth:(float)width{
    
    CGSize size = CGSizeMake(width, font.pointSize+10);
    
    CGContextSetTextMatrix(context,CGAffineTransformIdentity);
    
    //移動座標系統,全部點的y增長了height
    CGContextTranslateCTM(context,0,height);
    
    //縮放座標系統,全部點的x乘以1.0,全部的點的y乘以-1.0
    CGContextScaleCTM(context,1.0,-1.0);

    
    //文字顏色
    UIColor* textColor = color;
    
    //生成CTFont
    CTFontRef font1 = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize,NULL);
    
    //用於建立CTParagraphStyleRef的一些基本數據
    CGFloat minimumLineHeight = font.pointSize,maximumLineHeight = minimumLineHeight+10, linespace = 5;
    CTLineBreakMode lineBreakMode = kCTLineBreakByTruncatingTail;
    
    //左對齊
    CTTextAlignment alignment = kCTLeftTextAlignment;
    
    //建立CTParagraphStyleRef
    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);
    
    //設置屬性字典;對象,key
    NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys:
                                (__bridge id)font1,(NSString*)kCTFontAttributeName,
                                textColor.CGColor,kCTForegroundColorAttributeName,
                                style,kCTParagraphStyleAttributeName,
                                nil];

    //生成path,添加到cgcontex上
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path,NULL,CGRectMake(p.x, height-p.y-size.height,(size.width),(size.height)));
    
    //生成CF屬性字符串
    NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:self attributes:attributes];
    CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)attributedStr;
    
    //從attributedString拿到ctframesetter
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
    
    //從framesetter拿到 core text 的 ctframe
    CTFrameRef ctframe = CTFramesetterCreateFrame(framesetter, CFRangeMake(0,CFAttributedStringGetLength(attributedString)),path,NULL);
    
    //將ctframe繪製到context裏面
    CTFrameDraw(ctframe,context);
    
    //由於不是對象類型,須要釋放
    CGPathRelease(path);
    CFRelease(font1);
    CFRelease(framesetter);
    CFRelease(ctframe);
    [[attributedStr mutableString] setString:@""];
    
    //恢復context座標系統
    CGContextSetTextMatrix(context,CGAffineTransformIdentity);
    CGContextTranslateCTM(context,0, height);
    CGContextScaleCTM(context,1.0,-1.0);
}
複製代碼

在這裏,做者根據文字的起點,顏色,字體大小和行高,使用Core Text,將文字繪製在了傳入的context上面。

3.5 異步繪製UILabel

而對於UILabel裏面的繪製,做者也採起了相似的方法:

首先看一下在cell實現文件裏,關於繪製label文字方法的調用:

//將文本內容繪製到圖片上,也是異步繪製
- (void)drawText{
    
    //若是發現label或detailLabel不存在,則從新add一次
    if (label==nil||detailLabel==nil) {
        [self addLabel];
    }
    
    //傳入frame
    label.frame = [_data[@"textRect"] CGRectValue];
    //異步繪製text
    [label setText:_data[@"text"]];
    
    //若是存在原帖
    if ([_data valueForKey:@"subData"]) {
        
        detailLabel.frame = [[_data valueForKey:@"subData"][@"textRect"] CGRectValue];
        //異步繪製text
        [detailLabel setText:[_data valueForKey:@"subData"][@"text"]];
        detailLabel.hidden = NO;
    }
}
複製代碼

能夠看出,對於帖子而言,是否存在原貼(當前貼是不是轉發貼)是不固定的,因此須要在判斷以後,用hidden屬性來控制相應控件的隱藏和顯示,而不是用addSubView的方法。

這裏的label是做者本身封裝的VVeboLabel。它具備高亮顯示點擊,利用正則表達式區分不一樣類型的特殊文字(話題名,用戶名,網址,emoji)的功能。

簡單介紹一下這個封裝好的label:

  • 繼承於UIView,能夠響應用戶點擊,在初始化以後,_textAlignment,_textColor,_font,_lienSpace屬性都會被初始化。
  • 使用Core Text繪製文字。
  • 持有兩種UIImageView,用來顯示默認狀態和高亮狀態的圖片(將字符串繪製成圖片)。
  • 保存了四種特殊文字的顏色,用正則表達式識別之後,給其着色。

這裏講一下這個label的setText:方法:

//使用coretext將文本繪製到圖片。
- (void)setText:(NSString *)text{
   
    //labelImageView 普通狀態時的imageview
    //highlightImageView 高亮狀態時的iamgeview
    
    ...
    
    //繪製標記,初始化時賦一個隨機值;clear以後更新一個隨機值
    NSInteger flag = drawFlag;
    
    //是否正在高亮(在點擊label的時候設置爲yes,鬆開的時候設置爲NO)
    BOOL isHighlight = highlighting;
    
    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];
        
        //拿到CFAttributedStringRef
        NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:text attributes:attributes];
        CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)[self highlightText:attributedStr];
        
        //根據attributedStringRef 獲取CTFramesetterRef
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
        
        CGRect rect = CGRectMake(0, 5,(size.width),(size.height-5));
        
        if ([temp isEqualToString:text]) {
            
            //根據 framesetter 和 attributedString 繪製text
            [self drawFramesetter:framesetter attributedString:attributedStr textRange:CFRangeMake(0, text.length) inRect:rect context: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);
                [[attributedStr mutableString] setString:@""];
                
                if (drawFlag==flag) {
                    
                    if (isHighlight) {
                        
                        //高亮狀態:把圖片付給highlightImageView
                        if (highlighting) {
                            highlightImageView.image = nil;
                            if (highlightImageView.width!=screenShotimage.size.width) {
                                highlightImageView.width = screenShotimage.size.width;
                            }
                            if (highlightImageView.height!=screenShotimage.size.height) {
                                highlightImageView.height = screenShotimage.size.height;
                            }
                            highlightImageView.image = screenShotimage;
                        }
                    
                    } else {
                        
                        //非高亮狀態,把圖片付給labelImageView
                        if ([temp isEqualToString:text]) {
                            if (labelImageView.width!=screenShotimage.size.width) {
                                labelImageView.width = screenShotimage.size.width;
                            }
                            if (labelImageView.height!=screenShotimage.size.height) {
                                labelImageView.height = screenShotimage.size.height;
                            }
                            highlightImageView.image = nil;
                            labelImageView.image = nil;
                            labelImageView.image = screenShotimage;
                        }
                    }
// [self debugDraw];//繪製可觸摸區域
                }
            });
        }
    });
}

複製代碼

這個被做者封裝好的Label裏面還有不少其餘的方法,好比用正則表達式高亮顯示特殊字符串等等。

關於tableView的優化,做者作了不少處理,使得這種顯示內容比較豐富的cell在4s真機上好不卡頓,很是值得學習。


本篇文章已經同步到我我的博客:VVeboTableView源碼解析

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

筆者在近期開通了我的公衆號,主要分享編程,讀書筆記,思考類的文章。

  • 編程類文章:包括筆者之前發佈的精選技術文章,以及後續發佈的技術文章(以原創爲主),而且逐漸脫離 iOS 的內容,將側重點會轉移到提升編程能力的方向上。
  • 讀書筆記類文章:分享編程類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。

並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~

掃下方的公衆號二維碼並點擊關注,期待與您的共同成長~

公衆號:程序員維他命
相關文章
相關標籤/搜索