主线程中从HTML创建NSAttributedString的行为就像多线程一样

7
我正在将一些 HTML 转换为主线程上的 NSAttributedString(这是苹果告诉你该做的方式)。它需要一些时间,然后才会继续执行其他部分的代码块。
现在,如果另一个代码块也排队在主线程中运行(例如,在从 HTTP 请求获取响应后),我希望它在所有其他操作完成后再运行,但实际情况并非如此:它们以并行方式运行,就像它们在不同的线程上一样。我确保它们都在主线程上使用了 assert 语句。
我创建了一个名为 "Single View App" 的实验项目来测试这个问题,其中包含一个包含非常长的 html 字符串的文件,例如 "

lorem

ipsum dolor sit amet",以及以下代码的视图控制器:
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        dispatchStuff()
        for _ in 0..<10 {
            // slowOperation()
            parseHTML()
        }
    }

    func dispatchStuff() {
        for i in 0..<10 {
            let wait = Double(i) * 0.2
            DispatchQueue.main.asyncAfter(deadline: .now() + wait) {
                assert(Thread.isMainThread, "not main thread!")
                print(" dispatched after \(wait) seconds")
            }
        }
    }

    // just loads a big lorem ipsum full of html tags
    let html: String = {
        let filepath = Bundle.main.path(forResource: "test", ofType: "txt")!
        return try! String(contentsOfFile: filepath)
    }()

    var n = 0
    func slowOperation() {
        n += 1
        assert(Thread.isMainThread, "not main thread!")
        print("slowOperation \(n) START")
        var x = [0]
        for i in 0..<10000 {
            x.removeAll()
            for j in 0..<i {
                x.append(j)
            }
        }
        print("slowOperation \(n) END")
        print("")
    }

    var m = 0
    func parseHTML() {
        m += 1
        assert(Thread.isMainThread, "not main thread!")
        print("parseHTML \(m) START")
        let options = [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html]
        let attrString = try! NSAttributedString(data: Data(html.utf8), options: options, documentAttributes: nil)
        print("parseHTML \(m) END")
        print("")
    }
}

如果你运行它,控制台会显示以下内容:

parseHTML() uncommented

所有内容都混杂在一起,这是令人惊讶的(对我来说)行为。

但是如果在 viewDidLoad() 中注释调用parseHTML()并取消注释slowOperation(), 你将得到像下面这样的结果:

slowOperation() uncommented

这就是我所期望的。那么,这里发生了什么?我的线程工作方式的理解是不是完全错误?


1
我所能想到的唯一可能是 NSAttributedString 初始化器必须要进行一些“运行循环”调用,这使得在本应该是同步过程中偶尔运行了一些排队的闭包。 - rmaddy
1个回答

9

我的原始怀疑是正确的。调用NSAttributedString init(data:options:documentAttributes:)的实现会调用CFRunLoopRun()。这样做可以使队列中排队的其他块/闭包(在这种情况下是主队列)运行。

这就是为什么您会在主队列上看到看似异步的输出的原因。

我将您的代码放入了一个简单的命令行应用程序中,并在dispatchStuff中的print上设置了断点。堆栈跟踪显示,在调用NSAttributedString init期间,存在对_CGRunLoopRun的内部调用,这导致从dispatchStuff排队的闭包之一被调用。

enter image description here


哇,太疯狂了!非常感谢!!我想我应该将实例化属性字符串的代码作为异步调用,并带有回调,以避免意外行为 :) - maltalef
2
NSAttributedString init(data:options:documentAttributes:) 的文档说明如下:“不应该从后台线程调用 HTML 导入器(也就是说,选项字典包括值为 html 的 documentType)。它将尝试与主线程同步,失败并超时。从主线程调用它可以工作(但如果 HTML 包含对外部资源的引用,则仍可能超时,应尽量避免这种情况)。” - rmaddy
1
是的!这就是我想表达的:https://gist.github.com/maltalef/f5b95beda1acee8a1278454f2a24216f - maltalef

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接