【问题标题】:How to make SpannableStringBuilder.setSpan update the display on a non-UI thread?如何让 SpannableStringBuilder.setSpan 更新非 UI 线程上的显示?
【发布时间】:2017-06-20 16:19:00
【问题描述】:

我正在构建一个语法高亮源代码文件的应用程序。当用户单击文件时,我希望他们尽快看到代码,同时我在后台语法高亮代码(CPU 密集型)。这是我编写的代码(它使用 C#/Xamarin,但 Java 开发人员可以通过一些努力来翻译它):

private void DisplayContent(IColorTheme theme)
{
    var text = new ColoredText(_content);
    _editor.SetText(text, TextView.BufferType.Editable);
    // The last param uses C# 7 syntax for tuples, don't worry about it
    Task.Factory.StartNew(HighlightContent, state: (text, theme));
}

private void HighlightContent(object state)
{
    var (text, theme) = ((ColoredText, IColorTheme))state;
    using (var colorer = TextColorer.Create(text, theme))
    {
        GetHighlighter().Highlight(_content, colorer);
    }
}

重要的部分是我首先用ColoredText 调用SetText(),这是一个自定义类型,只包装了SpannableStringBuilder。然后,我调用Task.Factory.StartNew,这是C# 中在新线程上运行回调(在本例中为HighlightContent)的一种方式。最后,GetHighlighter().Highlight(...) 完成了大部分工作,并在文本的底层SpannableStringBuilder 上调用了SetSpan 数千次。

当我运行我的应用程序时,我看到了文本,但它全是白色的。就好像我完全省略了所有语法高亮代码,只写了_editor.SetText(...)。我强烈怀疑这是因为SetSpan 正在另一个线程上运行。但是,我需要在不同的线程上运行SetSpan,因为通过分析我已经确定它占用了大量的 CPU 周期。如果我在 UI 线程上运行它,我的应用程序会因大文件而冻结。

如何让SetSpan 从非 UI 线程更新显示? (不要建议 runOnUiThread,因为我必须在非 UI 线程上运行此代码。)谢谢。

【问题讨论】:

  • 如果问题是另一个线程,您的应用程序将崩溃,但 UI 无法从该线程更新或将被忽略。看起来在您的情况下它被忽略了。我也不明白为什么你不能只在更新 UI 的部分上使用 runOnUiThread,其他所有东西都可以在不同的线程上运行
  • @YuriS 更新 UI 的部分也是 CPU 密集型和冻结 UI。
  • 你不能从非 UI 线程更新 UI。

标签: java c# android xamarin xamarin.android


【解决方案1】:

更改SpannableStringBuilder 的内容后,需要调用postInvalidate() 或其家族的其他方法。仅调用setSpan() 并不会告诉视图它已更改并需要重绘。

【讨论】:

    【解决方案2】:

    好的,我今天花了相当多的时间,终于想出了一个解决方案。

    问完这个问题后,我在 Visual Studio 中调试了应用程序,发现setSpan 并没有被完全忽略。相反,它挂了——第一个setSpan 之后的语句永远不会运行。经过半个小时的摸索,我意识到每当setSpan在渲染代码之后被调用时就会发生这种情况。

    我的解决方案很老套。我意识到setSpan 如何更新显示是通过检查当前附加到SpannableStringBuilder 的任何SpanWatchers 并通知他们。我修改了ColoredTextimplementation of setSpan,以便检测是否附加了SpanWatcher,如果是,则将其包装在一个新的SpanWatcher 中,该SpanWatcher 将在UI 线程上运行所有代码。

    @Override public void setSpan(Object o, int i, int i1, int i2) {
        if (o instanceof SpanWatcher) {
            o = new UiThreadSpanWatcher((SpanWatcher) o);
        }
        this.builder.setSpan(o, i, i1, i2);
    }
    
    public class UiThreadSpanWatcher implements SpanWatcher {
        private final SpanWatcher watcher;
    
        public UiThreadSpanWatcher(SpanWatcher watcher) {
            this.watcher = watcher;
        }
    
        @Override public void onSpanAdded(...) {
            Threading.postToUiThread(() -> {
                this.watcher.onSpanAdded(...);
            });
        }
    }
    

    现在所有的渲染代码都将发布到 UI 线程。然而,这产生了一个不同的问题:setSpan 被调用了很多次,我向 UI 线程发布了数千个回调,扼杀了性能。为了缓解这种情况,我每次调用重写的方法时都将参数添加到列表中,然后添加一个 flush() 方法,该方法发布一个回调,为每组参数调用底层观察者的方法。

    private final List<Tuple4<...>> onSpanAddedArguments;
    
    @Override public void onSpanAdded(...) {
        this.onSpanAddedArguments.add(new Tuple4<>(...));
    }
    
    public void flush() {
        Threading.postToUiThread(() -> {
            for (Tuple4<...> tuple : this.onSpanAddedArguments) {
                this.watcher.onSpanAdded(tuple.item1, tuple.item2, tuple.item3, tuple.item4);
            }
        });
    }
    

    有很多样板代码涉及,但最后我终于让一切正常了。如果以上对您来说不是最有意义的,如果您有兴趣,可以看看我的项目here

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-02-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多