当前位置: 首页 > 编程日记 > 正文

VVeboTableView 源码解析

这次分享一个关于性能优化的源码。

我们知道UITabelView在iOS开发中扮演者举足轻重的角色,因为它是iOS开发中使用频率非常高的控件之一:几乎每个app都离不开它,因此,UITabelView的性能将直接影响这个app的性能。

如果UITabelView里的cell设计的比较简单,那么即使不做相应的优化,对性能的影响也不会很大。

但是,当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的高度即可):

- (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];//覆盖在头像上面的图片,制造圆角效果:framecornerImage = [[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 : 停止后的contentOffsetNSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];//当前可见第一行row的indexNSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];//设置最小跨度,当滑动的速度很快,超过这个跨度时候执行按需加载NSInteger skipCount = 8;//快速滑动(跨度超过了8个cell)if (labs(cip.row-ip.row)>skipCount) {//某个区域里的单元格的indexPathNSArray *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;}//触摸的时候刷新可见cellif (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];//绘制名字下面的infoy += 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:contextwithPosition: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转化为图片,赋给背景imageviewUIImage *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增加了heightCGContextTranslateCTM(context,0,height);//缩放坐标系统,所有点的x乘以1.0,所有的点的y乘以-1.0CGContextScaleCTM(context,1.0,-1.0);//文字颜色UIColor* textColor = color;//生成CTFontCTFontRef 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;//创建CTParagraphStyleRefCTParagraphStyleRef 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);//设置属性字典;对象,keyNSDictionary* 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拿到ctframesetterCTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);//从framesetter拿到 core text 的 ctframeCTFrameRef 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];}//传入framelabel.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 colorUIColor* textColor = self.textColor;//Set line height, font, color and break modeCGFloat 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 settingsCTParagraphStyleRef 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];//拿到CFAttributedStringRefNSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:text attributes:attributes];CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)[self highlightText:attributedStr];//根据attributedStringRef 获取CTFramesetterRefCTFramesetterRef 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];//恢复contextCGContextSetTextMatrix(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) {//高亮状态:把图片付给highlightImageViewif (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 {//非高亮状态,把图片付给labelImageViewif ([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源码解析

欢迎来参观 ^^

本文已在版权印备案,如需转载请访问版权印。48422928

获取授权

相关文章:

人工智能第二次作业

2.9设有如下语句&#xff0c;请用相应的谓词公式分别把他们表示出来 (1) 有的人喜欢梅花&#xff0c;有的人喜欢菊花&#xff0c;有的人既喜欢梅花又喜欢菊花 。 解&#xff1a; P(x)&#xff1a;x是人 L(x,y)&#xff1a;x喜欢y 其中&#xff0c;y的个体域是{梅花&#xff0c;…

Perl 校验命中的脚本

这个脚本无比的重要&#xff0c;虽然代码简单&#xff0c;但是在判断是否准确上&#xff0c;有着很重要的地位。 通过icmp和解析&#xff0c;它有一定意义所在。 mark!.. #!/usr/bin/perl use Net::Ping; sub icmp_domain{$ktrue;local($host)shift;$pNet::Ping->new("…

从基于网络的安装服务器安装操作系统,PXE 概述 - Sun Fire X4800 服务器安装指南(适用于 Linux 操作系统)...

PXE 概述使用 Linux 预引导执行环境 (preboot execution environment, PXE) 可从网络接口而不是本地存储引导服务器。对于 OS 安装&#xff0c;从基于 PXE 的OS 分发映像引导目标服务器就像从 DVD 引导一样&#xff0c;不同之处在于介质位于网络中。要使用 PXE&#xff0c;您需…

下载最新Android代码的方法

之前我是去Android官方网站下载最新Android代码&#xff0c;但是这种方法需要翻墙&#xff0c;而且有时候翻墙又不太方便&#xff0c;今天我发现一个不错的网站&#xff0c;是清华大学搞的&#xff0c;跟Android官方的代码基本保持同步&#xff0c;而且下载方法跟Android官方的…

socket编程缓冲区大小对send()的影响

1. 概述 Socket编程中&#xff0c;使用send()传送数据时&#xff0c;返回结果受到以下几个因素的影响&#xff1a; • Blocking模式或non-blocking模式 • 发送缓冲区的大小 • 接收窗口大小 本文档介绍通过实验的方式&#xff0c;得出&#xff08;收发&#xff09;缓冲区大…

不用任何第三方,写一个RTMP直播推流器

2016年是移动直播爆发年&#xff0c;不到半年的时间内无数移动直播App掀起了全民直播的热潮。然而个人觉得直播的门槛相对较高,从推流端到服务端器到播放端&#xff0c;无不需要专业的技术来支撑&#xff0c;仅仅推流端就有不少需要学习的知识。目前大部分直播采用的都是RTMP协…

手机连接服务器数据库文件,手机连接服务器数据库文件夹

手机连接服务器数据库文件夹 内容精选换一换GaussDB(DWS)支持使用gs_dump工具导出某个数据库级的内容&#xff0c;包含数据库的数据和所有对象定义。可根据需要自定义导出如下信息&#xff1a;导出数据库全量信息&#xff0c;包含数据和所有对象定义。使用导出的全量信息可以创…

开源一个上架 App Store 的相机 App

原创 2017-02-21 伯乐专栏/陈浩 iOS大全&#xff08;点击上方公众号&#xff0c;可快速关注&#xff09; 来源&#xff1a;伯乐在线 - Hawk0620 如有好文章投稿&#xff0c;请点击 → 这里了解详情 如需转载&#xff0c;发送「转载」二字查看说明 Osho 相机是我独立开发上架的…

WIN7下,联想A30T通过USB连接上网

1.手机连接3G信号 2.手机上在设置里 "设置" |"应用程序" |"USB模式" |勾选"网卡模式" 3.连接上Win7,会出现驱动安装程序,手动安装这里的驱动.(驱动在百度网盘:) 4.在设备管理,网络适配器里查看驱动是否安装成…

在bootstrap ace样式框架上修改的后台管理型模板(Tab页后台管理模板)

后台管理模板开始用frameset布局&#xff0c;但是有时候会遮挡比如上面导航或者左边导航的二级三级弹出菜单&#xff0c;因为宽度被限制了&#xff0c;所以有时候就用easyui或者ext的&#xff0c;但是样式不好看&#xff0c;然后看到了bootstrap ace的后台管理模板&#xff0c;…

文件服务器的内存要多少,文件服务器内存要多大

文件服务器内存要多大 内容精选换一换Windows场景中&#xff0c;当把源端服务器迁移到华为云后&#xff0c;目的端服务器C盘的已用空间比对应源端服务器C盘的已用空间大至少1GB&#xff0c;而不是与源端服务器C盘的已用空间一致&#xff0c;这正常吗&#xff1f;正常现象。您可…

好玩的 RAC

UIControl 监听 control 点击 从此告别 addTarget 和 btnClick 1234[[self.loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *btn) {// btn, 即 self.loginBtn// 这里执行点击之后的操作}];UITextField 监听 textField 的 text 改…

机器学习简单代码示例

机器学习简单代码示例 //在gcc-4.7.2下编译通过。 //命令行&#xff1a;g -Wall -ansi -O2 test.cpp -o test #include <iostream> using namespace std; void input(int &oper,const bool meth) {//meth为true则只判断1&#xff0c;为false则判断1或0while(true){ci…

【2007-5】【素数算式】

Description 在下面的算式中每个“#”都表示一个素数数字。##* #————###请编写程序确定这些数字&#xff0c;输出所有的解。Input Output 依次打印输出每一种解&#xff0c;每行表示一种解&#xff0c;格式为&#xff1a;##*# ###。Sample Input Sample Output HINT Sou…

ubuntu服务器版编辑文件,Ubuntu 服务器版 18.04.4 固定 IP 设置

1、输入命令 su 以root用户权限操作2、找到Ubuntu网络配置文件vim /etc/netplan/50-cloud-init.yaml3、输入 i 让文件变成可编辑状态4、修改内容dhcp4: nodhcp4: noaddresses: [192.168.3.50/24]gateway4: 192.168.3.1nameservers:addresses: [8.8.8.8, 8.8.4.4]如图&#xff…

RACCommand 粗解

前言 学习 RAC 的过程中&#xff0c;RACCommand 是我一直很迷惑的点&#xff0c;感觉一直抓不到它的要点&#xff0c;不明白为何要这样使用。曾经想过用别的方法来替代&#xff0c;只要能找到替代的方法&#xff0c;暂时就没必要死磕&#xff0c;结果发现避免不了&#xff0c;那…

将整数拆分为2的幂次方

任意一个正整数都可以用2的幂次方表示&#xff0c;例如&#xff1a;137&#xff1d;2^72^32^0&#xff0c;同时约定次方用括号来表示&#xff0c;即a^b&#xff1d;a(b)。由此可知&#xff0c;137可表 示&#xff1a;2(7)2(3)2(0)。进一步&#xff1a;72^222^0(2^1用2表示)&…

vbs脚本在服务器上虚拟按键,iisvdir.vbs iis虚拟目录管理脚本使用介绍

IIS管理器也是通过调用iisvdir.vbs来实现虚拟目录的创建和删除的。我们可以通过命令行的方式来执行iisvdir.vbs脚本1)创建虚拟目录&#xff1a;cscript c:\windows\system32\iisvdir.vbs [/s server] [/u username /p password] /create [virtualRoot] Alias PhysicalPath2)删除…

Python字典部分源码分析,字典是无序的

1 def clear(self): # real signature unknown; restored from __doc__ 2 """ D.clear() -> None. Remove all items from D. """ 3 pass 1 #练习1、清空字典&#xff08;置空&#xff09; 2 li {"key1":"v…

【2011-3】【旋转表格】

Description 小敏是个数学迷&#xff0c;特别擅长加法与除法。老师给他一个问题&#xff0c;有一个22表格&#xff0c;表中有正整数A, B, C 和D&#xff0c;表格和表格值的计算如下&#xff1a;小敏的任务是对给出的表格每次顺时针旋转90度&#xff0c;使旋转后的表格值最大&am…

iOS-FXDanmaku弹幕库介绍、相关技术分享

前言 去年, 2016年, 一大波直播平台在移动端涌出, 直播慢慢步入了人们的视角. 网上如今能够看到各式各样的直播, 如秀场直播、游戏直播、体育直播、娱乐直播等等. 在各种类型的直播中, 弹幕在PC、移动端都几乎成为了标配, 今天在这里主要介绍一下个人开源的iOS弹幕, 以及提前为…

【廖雪峰Python学习笔记】字符串与编码

字符串与编码 三种字符编码 ASCII编码 &#xff1a;计算机由美国人发明&#xff0c;最早只有127个字符编码—— 大小写英文字母、数字和符号Unicode&#xff1a;把中文、日文、韩文等所有语言统一到一套编码中&#xff0c;2-4byte&#xff0c;现代OS和大多数语言都支持utf-8&a…

application

说明: 进行应用级操作.比如:设置提醒框,打开其他应用,显示联网状态,控制状态栏 单例对象, UIApplication *app[UIApplication sharedApplication];常用方法 方法1:设置app图标数字提示信息//设置app图标数字提示信息app.applicationIconBadgeNumber10;//ios8以后需要实现以下方…

博客园2013年4月份第2周源码发布详情

Silk Navigation for ASP.NET源码 2013-4-12 [VS2010]功能介绍&#xff1a;Silk Navigation 微软提供&#xff0c;为构建cross-browser 的Web应用程序&#xff0c;是用ASP.NET MVC3和jQuery的。它是非常简单使用ASP.NET Web窗体构建&#xff0c;从而在一个相当小的&#xff0c…

iOS 静态库封装

静态库和动态库 静态库和动态库存在形式 静态库&#xff1a;以.a 和 .framework为文件后缀名。动态库&#xff1a;以.tbd(之前叫.dylib) 和 .framework 为文件后缀名。 静态库和动态库的区别 静态库&#xff1a;链接时会被完整的复制到可执行文件中&#xff0c;被多次使用就有多…

【廖雪峰Python学习笔记】list tuple dict set

列表元组字典集合创建l [1, ‘a’, [1, 3], True]t (1, )d {‘key’ : ‘value’}s set([1, 2, 4, 2, 1])索引l[1]t[0]d.get(‘key’)/插入l.insert(1, ‘3’) / l.append(‘4’)/d[‘k1’] ‘v1’s.add(‘9’)修改l[0] 4/d[‘k1’] v/移除l.pop(0)/d.pop(‘k1’)s.remo…

响应式布局简明示例

响应式布局简明示例&#xff0c;响应式布局最好同时也是自适应布局&#xff0c;然后再配合css3媒体查询&#xff0c;来达到完美的响应式布局。css3的媒体查询ie9一下是不支持的&#xff0c;这太遗憾了&#xff0c;不过幸运的是有大神已经为前端屌丝们写好了用于IE9以下的媒体查…

iOS - APP任意push新页面那些事

大家都知道&#xff0c;UINavigationController对象有一个方法pushViewController&#xff0c;用来做视图跳转&#xff0c;也是在iOS开发中常用的页面转换方法之一。大多数APP的结构一般都是&#xff0c;使用一个UITabBarController&#xff0c;每个tab上都是一个UINavigationC…

主攻ASP.NET.4.5 MVC4.0之重生:Entity Framework生成实体类步骤(十三)

1.新建一个ASP.NET MVC 4.0 项目 2.安装Entity Framework Power Tools 3.Entity Framework- -RepositoryReverse Engineer Code First 4.Install-Package EntityFramework or Update-Package EntityFramework PM -Repository Install-Package EntityFramework 已安装“EntityFr…

【廖雪峰Python学习笔记】高阶函数

Higher-order function高阶函数映射过滤算法排序算法高阶函数 变量可指向函数 >>> abs # 函数 <built-in function abs> >>> abs(-0) # 函数调用 0 >>> func abs # 变量可指向函数 >>> func(-9) # 调用变量 调用函数 9函数名…