一次NSMutableAttributedString误用的思考

缘起一次简单的测试,代码如下:

ini 复制代码
1. NSString *yyStr = @"[YYLabel]:This string is used for test YYLabel attributes, which affect the displsy content. add some substring for truncating";

2. NSString *uiStr = @"[UILabel]:This string is used for test UILabel attributes, which affect the displsy content. add some substring for truncating";

3. NSMutableAttributedString *yyText = [self lineBreakTextWithString2:yyStr];
4. NSMutableAttributedString *uiText = [self lineBreakTextWithString1:uiStr];

// 设置 UILabel 的 lineBreakModel 为 NSLineBreakByCharWrapping.
5. NSMutableParagraphStyle *uiParagraphStyle = [[NSMutableParagraphStyle alloc] init];
6. uiParagraphStyle.lineBreakMode = NSLineBreakByCharWrapping;
7. [uiText addAttribute:NSParagraphStyleAttributeName value:uiParagraphStyle range:NSMakeRange(0, uiText.length)];

8. self.uiContentLabel.attributedText = uiText;
9. self.uiContentLabel.numberOfLines = 2;

// 设置 YYLabel 的 lineBreakModel 为 NSLineBreakByCharWrapping.
10. yyText.yy_lineBreakMode = NSLineBreakByCharWrapping;

11. self.yyContentLabel.attributedText = yyText;
12. self.yyContentLabel.numberOfLines = 2;

结果如下图所示:

可以看出 UILabel(YYLabel的展示,YYLabel本身的代码有问题,这里不作讨论,如果有兴趣可以查看我的微博) 展示的第二行并不是 NSLineBreakByCharWrapping 的效果,按理说应该是按照 char 截断。 为此看了下按照上述代码,都在哪里执行了设置 lineBreakMode。

ini 复制代码
###### 1 ######
// 设置 UILabel 的 lineBreakModel 为 NSLineBreakByCharWrapping.
NSMutableParagraphStyle *uiParagraphStyle = [[NSMutableParagraphStyle alloc] init];
uiParagraphStyle.lineBreakMode = NSLineBreakByCharWrapping;
[uiText addAttribute:NSParagraphStyleAttributeName value:uiParagraphStyle range:NSMakeRange(0, uiText.length)];

###### 2 ######
// 设置 YYLabel 的 lineBreakModel 为 NSLineBreakByCharWrapping.
yyText.yy_lineBreakMode = NSLineBreakByCharWrapping;

// 当设置 YYLabel 的 attributeText 属性时,内部会先 mutableCopy yyText。
self.yyContentLabel.attributedText = yyText;
_innerText = yyText.mutableCopy;

###### 3 ######
// 设置 _innerText.yy_lineBreakMode,_lineBreakMode 默认为 NSLineBreakByTruncatingTail
_innerText.yy_lineBreakMode = _lineBreakMode;
switch (_lineBreakMode) {
    case NSLineBreakByWordWrapping:
    case NSLineBreakByCharWrapping:
    case NSLineBreakByClipping: {
        ###### 4 ######
        // 设置 _innerText.yy_lineBreakMode
        _innerText.yy_lineBreakMode = _lineBreakMode;
    } break;
    case NSLineBreakByTruncatingHead:
    case NSLineBreakByTruncatingTail:
    case NSLineBreakByTruncatingMiddle: {
        ###### 4 ######
        // 设置 _innerText.yy_lineBreakMode
        _innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;
    } break;
    default: break;
}

从上面的代码可以看出,uiText,yyText 和 innerText 均设置了 lineBreakModel。可以看出 UILabel 的表现更像是 NSLineBreakByWordWrapping,而在给 YYLabel 设置 attributedText 后,确实会走到_innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;的逻辑。这里我开始猜想内部是不是有这么一种共享 attributes 的机制。

在 Xcdoe debug 时,NSMutableAttributedString 的有一个为类型为 NSMutableRLEArray 的 mutableAttributes 的属性,如下图所示:

在上述代码执行到创建好 uiText 和 yyText后,也即第 4 行代码执行完毕,未开始执行第 5 行代码,断点在此处。打印出 uiText 和 yyText 的 mutableAttributes 的信息。如下图所示:

  • uiText
  • yyText

从上面可以看出 uiText 和 yyText 的 mutableAttributes 数组内的元素相同,均为 0x106b68bc0。从这里可以看出 uiText 和 yyText 共享了 attributes。

代码继续执行下去,断点在第 8 行,也即 uiText 添加了 paraStyle 属性。打印出 uiText 和 yyText 的 mutableAttributes 的信息。如下图所示:

  • uiText
  • yyText

从上面可以看出 uiText 和 yyText 的 mutableAttributes 数组内的元素地址不相等,数组元素内容也不同。uiText 数组元素多了 paraStyle attributes 信息

代码继续执行下去,当执行完第 10 行代码,也即设置完 yyText 的 yy_lineBreakMode 属性。断点在 11 行,打印结果如下图所示:

  • uiText
  • yyText

从上面的打印结果可以看出,uiText 和 yyText 的 mutableAttributes 数组内的一个元素地址相同,均为 0x106951020从这里可以推测:uiText 和 yyText 内部共享了 attributes。但是这是怎么造成的呢?下面会给出我的猜测。

如果对一个 string 执行 copy,会不会共享 attributes 呢? 代码继续执行到第 11 行 self.yyContentLabel.attributedText = yyText; 内部会执行 _innerText = yyText.mutableCopy; 打印的信息如下:

  • yyText(也即下图显示的 attributedText,- (void)setAttributedText:(NSAttributedString *)attributedText
  • innerText

从上面的比较结果可以看出,mutable string 不会创建新的 attributes 元素 。所以至此 uiText,yyText 和 innerText 的 mutableAttributes 数组共享一套 attributes 数组元素

所以 UILabel 的 lineBreakMode 表现为 NSLineBreakByTruncatingMiddle 就不奇怪了,因为最终 innerText 的 yy_lineBreakMode 按照 YYLabel 的内部逻辑就是设置为 NSLineBreakByTruncatingMiddle,_innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;

那么问题回到为什么 yyText.yy_lineBreakMode = NSLineBreakByCharWrapping; 执行完之后,yyText 和 uiText 就共享一套 attributes 属性。

先从 CoreText 中的理念入手,在 CoreText 有 CTRun的概念,简单说就是:对于一个 attributeString,其中连续的字符具备相同的attribuites,就生成一个 CTRun。

以 "Hello World" 字符串入手,假如整个字符串的字体都为 FontA,但是 "Hello " 颜色为 blue,而 "World" 为 red。那么针对 "Hello World" 生成的 attributes 数组元素就会有两个,一个包括(FontA,颜色 blue,子字符串range1),另外一个(FontA,颜色 red,子字符串range2)。

系统内部为了优化内存,针对 attributes 设定了一套共享策略。 可将 attributes 分为可变和不可变,区分的依据就是:比如 paraStyle 设定的是 NSMutableParagraphStyle,那么就是可变的。如果是 NSParagraphStyle 则表示的就是不可变的。注意这里可变和不可变不是指内存上不可更改,而是一种描述

在系统内部维护了一套 mutable attributes,使用 hash 存储,key 为各个 attribute 的hash 结果

我们来分析前面的代码,因为最开始 uiText 和 yytext 未设定 paraStyle,所以使用的都是 immtuable attributes。当 uiText 设定 NSMutableParagraphStyle,uiText 的 attributes 不能再和 yyText 共享,需要创建新的 attributes 元素,并且是 mutable,存储到系统内部的 mutable attributes hash中

当 yyText 设定 yy_lineBreakMode 的值和 uiText 设定的 lineBreakMode 一致时,先去把更改的 attribute 元素计算 hash,并去 mutable attributes table 中查找是否有对应的 value(如果不是mutable则不要去查找,直接自己独自一份)。如果找到,则直接共享 attribute 元素,如果没有,则独自创建一份(根据自身是否是 mutabel 决定是否放入 mutabel attribute table)。

所以至此,yyText 和 uiText 已经共享了一套 attributes 元素,后续如果不打破这种关系,yyText 针对 lineBreakMode 的更改也会影响到 UILabel 的展示。

如果把 uiText 设定的 paraStyle 设定为 immutable,结果就会不一样。 代码修改为:

go 复制代码
[uiText addAttribute:NSParagraphStyleAttributeName value:uiParagraphStyle.copy range:NSMakeRange(0, uiText.length)];

结果如下图所示:可以看到 UILabel 已经是正常的展示了。因为当 yyText 设定 lineBreakMode 时,重新生成的 attribute 元素是不可变的,不能和其它 text 共享。所以后续 yyText 的后续更改都不影响 UILabel 的展示

针对上面提到的计算 hash 作为 key,可以再做一个测试,保持最开始的代码不变,仅改成:NSLineBreakByWordWrapping,而不是之前的NSLineBreakByCharWrapping

ini 复制代码
yyText.yy_lineBreakMode = NSLineBreakByWordWrapping;

这里也从侧面证明我的猜想,根据 attribute 的值计算整体的 hash 值作为 key,去获取对应 attribute 元素。如果没有,则创建新的 attribute 元素,也即不是 uiText 和 yyText 共享 attributes 元素的目的。

最后还是对于 NSMutableAttributedString 的误用,尤其在设定 paraStyle 的时候,如果不希望意外的修改,最好传入一个 immutable paraStyle。

相关推荐
程序员卷卷狗2 小时前
Java转Go面试速记:Go基础22问,一篇理清高频易错点一篇理清高频易错点
java·面试·golang
swipe2 小时前
DeepAgents middleware 工程实战:把复杂 Agent 的运行时基建交给可组合中间件
前端·面试·llm
小江的记录本4 小时前
【JVM虚拟机】类加载机制:类加载全流程:加载→验证→准备→解析→初始化(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·算法·安全·spring·面试
西安邮电大学4 小时前
Redis为什么快?
java·redis·后端·其他·面试
折哥的程序人生 · 物流技术专研4 小时前
《Java 100 天进阶之路》第39篇:Java泛型方法的定义和使用
java·开发语言·后端·面试·求职招聘
人月神话-Lee5 小时前
【图像处理】Core Image 与 GPU 渲染管线——让滤镜飞起来
图像处理·人工智能·ios·chatgpt·ai编程·swift·gpu
小江的记录本6 小时前
【Spring AI】Spring AI中RAG误触发与系统提示词泄露问题解决方案(完整版+代码方案)
java·人工智能·spring boot·后端·python·spring·面试
zhang_adrian6 小时前
【使用Github Copilot自动按规范文档生成全部代码】
人工智能·github·copilot
swipe6 小时前
LangSmith 全链路观测:从 Agent 调试到 RAG 量化评估
后端·面试·llm