iOS 18 NSAttributedString attributes Range 合并行为改变|Equatable 影响解析与闪退问题排除
iOS 18 起 NSAttributedString attributes 合并机制改为参考 Equatable,导致自订属性 Range 合并行为异动,造成 HTML 解析闪退。深入剖析底层合并逻辑与 Swift Equatable 实作,快速掌握修正关键,避免闪退并优化字串属性管理。
Click here to view the English version of this article.
點擊這裡查看本文章正體中文版本。
基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。
iOS ≥ 18 NSAttributedString attributes Range 合并的一个行为改变
iOS ≥ 18 开始 NSAttributedString attributes Range 合并会参考 Equatable
Photo by C M
问题起因
iOS 18 2024/9/17 上线后,之前做的开源专案 ZMarkupParser 就有开发者回报 iOS 18 在解析部分 HTML 时会发生闪退。
看到这个 Issue 有点困惑,因为程式在以前都没问题,iOS 18 开始才会闪退,不符合常理,应该是 iOS 18 底层 Foundation 有什么调整导致。
Crash Trace
Trace Code 后定位到闪退问题点是遍历 .breaklinePlaceholder
Attributes 并针对 Range 进行删除操作时会发生闪退:
1
2
3
4
5
6
mutableAttributedString.enumerateAttribute(.breaklinePlaceholder, in: NSMakeRange(0, NSMakeRange(0, mutableAttributedString.string.utf16.count))) { value, range, _ in
// ...if condition...
// mutableAttributedString.deleteCharacters(in: preRange)
// ...if condition...
// mutableAttributedString.deleteCharacters(in: range)
}
.breaklinePlaceholder
是我自行扩充的一个 NSAttributedString.Key,用来标记 HTML 标签资讯,优化换行符号使用:
1
2
3
4
5
6
7
8
9
10
11
struct BreaklinePlaceholder: OptionSet {
let rawValue: Int
static let tagBoundaryPrefix = BreaklinePlaceholder(rawValue: 1)
static let tagBoundarySuffix = BreaklinePlaceholder(rawValue: 2)
static let breaklineTag = BreaklinePlaceholder(rawValue: 3)
}
extension NSAttributedString.Key {
static let breaklinePlaceholder: NSAttributedString.Key = .init("breaklinePlaceholder")
}
但核心问题不是这里 ,因为在 iOS 17 以前,输入的
mutableAttributedString
在执行以上操作时不会有问题;代表输入的资料内容在 iOS 18 有所变动。
NSAttributedString attributes: [NSAttributedString.Key: Any?]
在深入挖掘问题之前先介绍一下 NSAttributedString attributes 的 合并机制 。
NSAttributedString attributes 会 自动比较 .key 相同的相邻 Range Attributes 物件是否相同,相同则合并成同个 Attribute 例如:
1
2
3
4
5
let mutableAttributedString = NSMutableAttributedString(string: "", attributes: nil)
mutableAttributedString.append(NSAttributedString(string: "<div>", attributes: [.font: UIFont.systemFont(ofSize: 14)]))
mutableAttributedString.append(NSAttributedString(string: "<div>", attributes: [.font: UIFont.systemFont(ofSize: 14)]))
mutableAttributedString.append(NSAttributedString(string: "<p>", attributes: [.font: UIFont.systemFont(ofSize: 14)]))
mutableAttributedString.append(NSAttributedString(string: "Test", attributes: [.font: UIFont.systemFont(ofSize: 12)]))
最终 Attributes 合并结果:
1
2
3
4
5
<div><div><p>{
NSFont = "<UICTFont: 0x101d13400> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 14.00pt";
}Test{
NSFont = "<UICTFont: 0x101d13860> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
}
在 enumerateAttribute(.breaklinePlaceholder...)
时会得到以下结果:
1
2
NSRange {0, 13}: <UICTFont: 0x101d13400> font-family: ".SFUI-Regular"; font-weight: normal; font-style: normal; font-size: 14.00pt
NSRange {13, 4}: <UICTFont: 0x101d13860> font-family: ".SFUI-Regular"; font-weight: normal; font-style: normal; font-size: 12.00pt
NSAttributedString attributes 合并 — 底层实践方式推测
推测底层是使用 Set<Hashable>
做为 Attributes 容器,会自动排除相同的 Attriubte 物件。
但是为了使用方便, NSAttributedString attributes: [NSAttributedString.Key: Any?]
Value 物件是宣告成 Any?
Type,没有限制 Hashable。
也因此推测系统在底层会在 Conform as? Hashable
然后使用 Set 合并管理物件。
这次的 iOS ≥ 18 调整差异推测就是这边底层的实现问题。
以下是以我们自订的 .breaklinePlaceholder
Attributes 为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct BreaklinePlaceholder: Equatable {
let rawValue: Int
static let tagBoundaryPrefix = BreaklinePlaceholder(rawValue: 1)
static let tagBoundarySuffix = BreaklinePlaceholder(rawValue: 2)
static let breaklineTag = BreaklinePlaceholder(rawValue: 3)
}
extension NSAttributedString.Key {
static let breaklinePlaceholder: NSAttributedString.Key = .init("breaklinePlaceholder")
}
//
let mutableAttributedString = NSMutableAttributedString(string: "", attributes: nil)
mutableAttributedString.append(NSAttributedString(string: "<div>", attributes: [.breaklinePlaceholder: NSAttributedString.Key.BreaklinePlaceholder.tagBoundaryPrefix]))
mutableAttributedString.append(NSAttributedString(string: "<div>", attributes: [.breaklinePlaceholder: NSAttributedString.Key.BreaklinePlaceholder.tagBoundaryPrefix]))
mutableAttributedString.append(NSAttributedString(string: "<p>", attributes: [.breaklinePlaceholder: NSAttributedString.Key.BreaklinePlaceholder.tagBoundaryPrefix]))
mutableAttributedString.append(NSAttributedString(string: "Test", attributes: nil))
iOS ≤ 17 前会得到以下 Attributes 合并结果:
1
2
3
4
5
6
7
8
<div>{
breaklinePlaceholder = "NSAttributedStringCrash.BreaklinePlaceholder(rawValue: 1)";
}<div>{
breaklinePlaceholder = "NSAttributedStringCrash.BreaklinePlaceholder(rawValue: 1)";
}<p>{
breaklinePlaceholder = "NSAttributedStringCrash.BreaklinePlaceholder(rawValue: 1)";
}Test{
}
iOS ≥ 18 会得到以下 Attributes 合并结果:
1
2
3
4
<div><div><p>{
breaklinePlaceholder = "NSAttributedStringCrash.BreaklinePlaceholder(rawValue: 1)";
}Test{
}
可以看到同样的程式在不同版本的 iOS 有不同的结果,这最终导致了后续的
enumerateAttribute(.breaklinePlaceholder..)
中的处理逻辑不合预期造成闪退。
⭐️ iOS ≥ 18 NSAttributedString attributes: [NSAttributedString.Key: Any?] 会多参考 Equatable ==⭐️
比较 iOS 17/18 有无实现 Equatable/Hashable 的结果
⭐️⭐️ iOS ≥ 18 会多参考
Equatable
,iOS ≤ 17 则不会。⭐️⭐️
结合前述, NSAttributedString attributes: [NSAttributedString.Key: Any?]
Value 物件是宣告成 Any?
Type, 就观测结果, iOS ≥ 18 会先参考 Equatable
判断是否相同,然后再使用 Hashable
Set 合并管理物件。
结论
NSAttributedString attributes: [NSAttributedString.Key: Any?] 在合并 Range Attribute 时,iOS ≥ 18 会多参考 Equatable,这点与以往不同。
另外在 iOS 18 开始如果只宣告 Equatable
XCode Console 也会输出 Warning:
Obj-C ` -hash
invoked on a Swift value of type
BreaklinePlaceholder` that is Equatable but not Hashable; this can lead to severe performance problems.
有任何问题及指教欢迎 与我联络 。
本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。