2017-06-20 48 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()不會告訴視圖已更改並需要重繪。