generated from cotes2020/chirpy-starter
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b967318
commit b21bba7
Showing
3 changed files
with
191 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
--- | ||
title: "iOS ≥ 18 NSAttributedString attributes Range 合併的一個行為改變" | ||
author: "ZhgChgLi" | ||
date: 2024-09-20T13:03:42.359+0000 | ||
last_modified_at: 2024-09-20T13:03:42.359+0000 | ||
categories: "ZRealm Dev." | ||
tags: ["ios-app-development","nsattributedstring","ios-18","ios","swift"] | ||
description: "iOS ≥ 18 開始 NSAttributedString attributes Range 合併會參考 Equatable" | ||
image: | ||
path: /assets/9e43897d99fc/1*PJ_qm75Yz_7y0UUBk8X6bg.jpeg | ||
render_with_liquid: false | ||
--- | ||
|
||
### iOS ≥ 18 NSAttributedString attributes Range 合併的一個行為改變 | ||
|
||
iOS ≥ 18 開始 NSAttributedString attributes Range 合併會參考 Equatable | ||
|
||
|
||
|
||
![Photo by [C M](https://unsplash.com/@ubahnverleih?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash){:target="_blank"}](/assets/9e43897d99fc/1*PJ_qm75Yz_7y0UUBk8X6bg.jpeg) | ||
|
||
Photo by [C M](https://unsplash.com/@ubahnverleih?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash){:target="_blank"} | ||
#### 問題起因 | ||
|
||
|
||
[![](https://repository-images.githubusercontent.com/602927147/57ce75c1-8548-449c-b44a-f4b0451ed5ea)](https://github.com/ZhgChgLi/ZMarkupParser){:target="_blank"} | ||
|
||
|
||
iOS 18 2024/9/17 上線後,之前做的開源專案 [ZMarkupParser](https://github.com/ZhgChgLi/ZMarkupParser){:target="_blank"} 就有開發者回報 iOS 18 在解析部分 HTML 時會發生閃退。 | ||
|
||
看到這個 Issue 有點困惑,因為程式在以前都沒問題,iOS 18 開始才會閃退,不符合常理,應該是 iOS 18 底層 Foundation 有什麼調整導致。 | ||
#### Crash Trace | ||
|
||
Trace Code 後定位到閃退問題點是遍歷 `.breaklinePlaceholder` Attributes 並針對 Range 進行刪除操作時會發生閃退: | ||
``` | ||
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 標籤資訊,優化換行符號使用: | ||
```swift | ||
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** 例如: | ||
```swift | ||
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 合併結果:** | ||
```swift | ||
<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...)` 時會得到以下結果: | ||
```swift | ||
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 為例: | ||
```swift | ||
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 合併結果:** | ||
```bash | ||
<div>{ | ||
breaklinePlaceholder = "NSAttributedStringCrash.BreaklinePlaceholder(rawValue: 1)"; | ||
}<div>{ | ||
breaklinePlaceholder = "NSAttributedStringCrash.BreaklinePlaceholder(rawValue: 1)"; | ||
}<p>{ | ||
breaklinePlaceholder = "NSAttributedStringCrash.BreaklinePlaceholder(rawValue: 1)"; | ||
}Test{ | ||
} | ||
``` | ||
#### iOS ≥ 18 會得到以下 Attributes 合併結果: | ||
```xml | ||
<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 的結果](/assets/9e43897d99fc/1*0TKpBawJoLZUbUKwovRUJQ.png) | ||
|
||
比較 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\._** | ||
|
||
|
||
|
||
|
||
|
||
有任何問題及指教歡迎 [與我聯絡](https://www.zhgchg.li/contact){:target="_blank"} 。 | ||
|
||
|
||
|
||
_[Post](https://medium.com/zrealm-ios-dev/ios-18-nsattributedstring-attributes-range-%E5%90%88%E4%BD%B5%E7%9A%84%E4%B8%80%E5%80%8B%E8%A1%8C%E7%82%BA%E6%94%B9%E8%AE%8A-9e43897d99fc){:target="_blank"} converted from Medium by [ZMediumToMarkdown](https://github.com/ZhgChgLi/ZMediumToMarkdown){:target="_blank"}._ |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.