2017-06-20 27 views
0

我正在构建一个语法高亮源代码文件的应用程序。当用户点击文件时,我希望他们尽快查看代码,同时在后台语法突出显示代码(CPU密集型)。这是我写的代码(它使用C#/ Xamarin,但Java的开发者可以用一些精力翻译本):如何使SpannableStringBuilder.setSpan更新非UI线程上的显示?

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); 
    } 
} 

的重要组成部分,是我第一次打电话SetText()ColoredText,这是一个自定义类型只是包装SpannableStringBuilder。然后,我打电话给Task.Factory.StartNew,这是C#中的一种方式,在新线程上运行回调(在这种情况下为HighlightContent)。最后,GetHighlighter().Highlight(...)完成了大部分工作,并且在文本的基础SpannableStringBuilder上调用SetSpan数千次。

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

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

+0

如果问题是一个不同的线程,你的应用程序会崩溃,但无法从该线程更新UI,否则它将被忽略。看起来很喜欢它在你的情况下被忽略。我也不明白为什么你只能在那个更新UI的部分上使用runOnUiThread,其他的东西都可以在不同的线程上运行 –

+0

@YuriS更新UI的部分也是CPU密集型的并冻结UI。 –

+0

您无法从非UI线程更新UI。 –

回答

0

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

问完这个问题后,我调试了Visual Studio中的应用程序,注意到setSpan并没有完全被忽略。相反,它是挂起的 - 第一个setSpan永远不会运行后的声明。经过半个小时的头部划痕,我意识到这种情况会在渲染代码后调用setSpan时发生。

我的解决方案是hackish。我意识到setSpan更新显示是通过检查当前连接到SpannableStringBuilder的任何SpanWatcher并通知他们。我修改了ColoredTextimplementation of setSpan,因此它会检测到是否连接了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,如果您有兴趣的话。

0

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