1. 背景
需求是一组用户名,给定宽度
- 能展示完全:
用户A, 用户B - 不能展示完全,最后 +x人 :
用户A, 用户B, 用户C +x人
我使用 TextKit 来计算是否发生了截断,拼接好目标字符串。利用 YYTextLayout 来缓存布局信息,展示时直接使用 yyLabel.textLayout = textLayout; 即可使用缓存好的布局信息,避免一些不必要的性能损耗。
2. 踩坑经过
本来一切都很正常,直到一个名叫“@ﻩㅤㅤㅤㅤㅤㅤㅤ阿风😻ㅤㅤㅤㅤ”的用户名,触发了展示异常和OOM崩溃。
下面我们来看看引发这些问题的原因。
2.1 OOM问题
由于第一步已经计算好了截断,我认为后续利用 YYTextLayout 计算文本尺寸是肯定不会超过限制宽度的,所以在使用 YYTextContainer 时就使用了 CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) 来作为容器大小,并在最后使用了 textBoundingSize 来缓存文本的大小。
NSTextStorage *attrStr = // …
YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];
container.maximumNumberOfRows = 1;
container.truncationType = YYTextTruncationTypeEnd;
YYTextLayout *textLayout = [YYTextLayout layoutWithContainer:container text:attrStr];
CGSize textSize = textLayout.textBoundingSize;
// …
这里我踩了第一个坑。
textBoundingSize 是整个绘制区域的大小,textBoundingRect描述了在哪个区域进行绘制。
textBoundingSize 和 textBoundingRect.size 并不等价。
所以,这里使用 textBoundingSize 获取到了一个非常大的尺寸。
又由于这里缓存了这个尺寸 (width = 1048576, height = 14) ,并且最终给 YYLabel 进行 frame 设置之后出现了崩溃问题。
这里 YYLabel 在列表中使用,所以出现了多个
size非常大的视图。熟悉 YYLabel 的同学知道, YYLabel 会通过 Core Graphics 申请一块内存区域,并将文本绘制出来,最后赋值给
layer.contents达到最终显示效果的。这里由于使用了 多个过大的绘制尺寸 ,最终导致了 OOM 崩溃。
2.2 展示异常
textBoundingSize 不行,如果使用 textBoundingRect.size 是否能解决问题呢?
这里要说到第二个坑。
由于 YYTextLayout 会缓存布局信息,yyLabel.textLayout = textLayout; 则会直接用布局信息进行渲染。
虽然我们 YYLabel 的尺寸大小正常了,但是绘制仍发生在 (origin = (x = 1048410.29365625, y = -0.42999999999999972), size = (width = 165.70634375, height = 14.324999999999999)) 这个位置的。由于 x = 1048410.29365625 远超过正常尺寸 (width = 1048576, height = 14) ,所以我们看不到绘制内容。
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.whiteColor;
NSString *content = @"@ﻩㅤㅤㅤㅤㅤㅤㅤ阿风😻ㅤㅤㅤㅤ";
UIFont *font = [UIFont systemFontOfSize:12.0];
NSDictionary<NSAttributedStringKey, **id**> *attrs = @{
NSFontAttributeName : font,
};
NSTextStorage *attrStr = [[NSTextStorage alloc] initWithString:content attributes:attrs];
YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];
container.maximumNumberOfRows = 1;
container.truncationType = YYTextTruncationTypeEnd;
YYTextLayout *textLayout = [YYTextLayout layoutWithContainer:container text:attrStr];
CGSize textSize = textLayout.textBoundingRect.size;
YYLabel *yyLabel = [[YYLabel alloc] initWithFrame:CGRectMake(0, 0, textSize.width, textSize.height)];
yyLabel.backgroundColor = UIColor.grayColor;
yyLabel.textLayout = textLayout;
[self.view addSubview:yyLabel];
yyLabel.center = self.view.center;
}
最终,我使用 -[NSAttributedString boundingRectWithSize:options:context:] 提前计算了展示的宽度,最终让内容可以展示在屏幕上了。
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.whiteColor;
NSString *content = @"@ﻩㅤㅤㅤㅤㅤㅤㅤ阿风😻ㅤㅤㅤㅤ";
UIFont *font = [UIFont systemFontOfSize:12.0];
NSDictionary<NSAttributedStringKey, id> *attrs = @{
NSFontAttributeName : font,
};
NSTextStorage *attrStr = [[NSTextStorage alloc] initWithString:content attributes:attrs];
// ===== 变更 =====
CGSize size = [attrStr boundingRectWithSize:CGSizeMake(200, 0) options:NSStringDrawingUsesFontLeading | NSStringDrawingUsesLineFragmentOrigin context:nil].size;
YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(size.width, CGFLOAT_MAX)];
// ===== 变更 =====
container.maximumNumberOfRows = 1;
container.truncationType = YYTextTruncationTypeEnd;
YYTextLayout *textLayout = [YYTextLayout layoutWithContainer:container text:attrStr];
CGSize textSize = textLayout.textBoundingRect.size;
YYLabel *yyLabel = [[YYLabel alloc] initWithFrame:CGRectMake(0, 0, textSize.width, textSize.height)];
yyLabel.backgroundColor = UIColor.grayColor;
yyLabel.textLayout = textLayout;
[self.view addSubview:yyLabel];
yyLabel.center = self.view.center;
}
2.3 踩坑 NSTextStorage
下面来介绍一下,我们的名称看着是正向的:
“@ﻩㅤㅤㅤㅤㅤㅤㅤ阿风😻ㅤㅤㅤㅤ”
但最终渲染出来时方向却不一样呢?
答案是 “ﻩ” 这个是一个阿拉伯符号,而 阿拉伯文是从右往左写的 所以渲染变成了上图这样。
还记得第一个坑, -[YYTextLayout textBoundingRect] 返回的 (origin = (x = 1048410.29365625, y = -0.42999999999999972), size = (width = 165.70634375, height = 14.324999999999999)) 吗?
这是因为 -[YYTextLayout textBoundingRect] 实现的和系统不一样。即便是从右往左布局,系统 -[NSAttributedString boundingRectWithSize:options:context:] 返回的值为 (origin = (x = 0, y = 0), size = (width = 165.70634375, height = 14.365546875))。而 YYTextLayout 就非常实诚地从容器的最右侧开始布局,所以返回了一个非常大的值。
此外,这些看着像空格的部分也不简单,通过 Unicode 转码后,我们发现这些“空格”其实是 “\u3164”。
这也为我们的渲染造成了一定的影响。
当我们使用 NSMutableAttributedString 时,我们来观察一下富文本信息时怎样的。
NSString *content = @"@ﻩㅤㅤㅤㅤㅤㅤㅤ阿风😻ㅤㅤㅤㅤ";
UIFont *font = [UIFont systemFontOfSize:12.0];
NSDictionary<NSAttributedStringKey, id> *attrs = @{
NSFontAttributeName : font,
};
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:content attributes:attrs];
// 输出
(lldb) po attrStr
@ﻩㅤㅤㅤㅤㅤㅤㅤ阿风😻ㅤㅤㅤㅤ{
NSFont = "<UICTFont: 0x7fb879f063c0> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
}
可以看到我们的富文本字体为 ".SFUI-Regular" 。
当我们使用 NSTextStorage 时会是什么结果呢?
NSString *content = @"@ﻩㅤㅤㅤㅤㅤㅤㅤ阿风😻ㅤㅤㅤㅤ";
UIFont *font = [UIFont systemFontOfSize:12.0];
NSDictionary<NSAttributedStringKey, id> *attrs = @{
NSFontAttributeName : font,
};
NSTextStorage *attrStr = [[NSTextStorage alloc] initWithString:content attributes:attrs];
// 输出
(lldb) po attrStr
@{
NSFont = "<UICTFont: 0x7fb9b1b0fe20> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
}ﻩ{
NSFont = "<UICTFont: 0x7fb9b1b10c40> font-family: \"Arial\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
}ㅤㅤㅤㅤㅤㅤㅤ阿{
NSFont = "<UICTFont: 0x7fb9b4604350> font-family: \".AppleSDGothicNeoI-Regular\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
}风{
NSFont = "<UICTFont: 0x7fb9b4604580> font-family: \".PingFangSC-Regular\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
}😻{
NSFont = "<UICTFont: 0x7fb9b46047b0> font-family: \".AppleColorEmojiUI\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
}ㅤㅤㅤㅤ{
NSFont = "<UICTFont: 0x7fb9b4604350> font-family: \".AppleSDGothicNeoI-Regular\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
}
我们可以看到富文本中出现了多种不同的富文本字体。这是因为 NSTextStorage 会主动调用 -[NSMutableAttributedString fixAttributesInRange:] 方法,对富文本的属性进行修正,详细的描述可以查看 API 的介绍。
@interface NSMutableAttributedString (NSAttributedStringAttributeFixing)
// This method fixes attribute inconsistencies inside range. It ensures NSFontAttributeName covers the characters, NSParagraphStyleAttributeName is only changing at paragraph boundaries, and NSTextAttachmentAttributeName is assigned to NSAttachmentCharacter. NSTextStorage automatically invokes this method via -ensureAttributesAreFixedInRange:.
- (void)fixAttributesInRange:(NSRange)range API_AVAILABLE(macos(10.0), ios(7.0));
@end
修正之后,会产生什么影响呢?我们来对比一下:
通过对比,我们发现经过 NSTextStorage 修正过后的尺寸会远大于 NSMutableAttributedString 需要的尺寸。
于是我们将 NSTextStorage 替换为 NSMutableAttributedString 来观察一下渲染效果。
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.whiteColor;
NSString *content = @"@ﻩㅤㅤㅤㅤㅤㅤㅤ阿风😻ㅤㅤㅤㅤ";
UIFont *font = [UIFont systemFontOfSize:12.0];
NSDictionary<NSAttributedStringKey, id> *attrs = @{
NSFontAttributeName : font,
};
// ===== 变更 =====
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:content attributes:attrs];
// ===== 变更 =====
CGSize size = [attrStr boundingRectWithSize:CGSizeMake(200, 0) options:NSStringDrawingUsesFontLeading | NSStringDrawingUsesLineFragmentOrigin context:nil].size;
YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(size.width, CGFLOAT_MAX)];
container.maximumNumberOfRows = 1;
container.truncationType = YYTextTruncationTypeEnd;
YYTextLayout *textLayout = [YYTextLayout layoutWithContainer:container text:attrStr];
CGSize textSize = textLayout.textBoundingRect.size;
YYLabel *yyLabel = [[YYLabel alloc] initWithFrame:CGRectMake(0, 0, textSize.width, textSize.height)];
yyLabel.backgroundColor = UIColor.grayColor;
yyLabel.textLayout = textLayout;
[self.view addSubview:yyLabel];
yyLabel.center = self.view.center;
}
可以看到我们的渲染效果变得更紧凑了。
最终我也是将【利用 TextKit 来计算是否发生了截断,并拼接好目标字符串】的这部分逻辑,替换为 YYText 的实现方式,最终获得了满意的效果。
如果觉得本文不错,给我点个赞吧~❤️