重现步骤
为文本字段启用拼写检查,并快速输入。
底层平台的SpellcheckService将被调用很多次,因为许多结果会很快被丢弃。
在为Windows版本的应用程序补充当前未实现的拼写检查器时,我发现虽然'fetchSpellCheckSuggestions'确实是异步的,但它预计会很快返回结果。
如果不能这样做,将导致大量调用排队等待完成,停止输入后,您正在等待它们全部完成,影响用户体验。
尝试按键连击或在启用拼写检查的文本字段上按住退格键。
为了减轻这个问题,我在"Win32SpellCheckService"实现中实现了防抖动。我发现这会导致小部件中的渲染失败,如果用户按住退格键-因为当成功防抖动的调用执行时,文本字段中的字符串值现在可能比拼写检查时的字符串值短,这意味着SuggestionSpans可能引用超出范围的索引(这是EditableText小部件报告的错误)。
我建议将防抖动添加到EditableText的内置实现中,也许可以设置一个配置周期。似乎我能找到的所有内联拼写检查实现(Chrome、MS Word等)都使用防抖动来避免检查 transient 文本,这些文本很快就会被渲染脏。
预期结果
当快速输入文本字段时,拼写检查器的运行应该被防抖动。
实际结果
当快速输入文本字段时,拼写检查器的运行没有被防抖动,导致不必要的系统负载,影响用户体验。
代码示例
实际上这只是一个带有防抖动的win32拼写检查器的例子。在没有防抖动的情况下,它工作得很好(尽管性能不佳)。当然,它只能提供下划线,尽管它返回建议,但EditableText在Windows上并不使用它们。
import 'dart:async';
import 'dart:isolate';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
class Win32SpellCheckService implements SpellCheckService {
final Map<String, _SpellCheckIsolate> _isolateCache = {};
@override
Future<List<SuggestionSpan>?> fetchSpellCheckSuggestions(Locale locale, String text) {
final completer = Completer<List<SuggestionSpan>?>();
final languageTag = locale.toLanguageTag();
EasyDebounce2.debounce(
'win32-spellcheck-debouncer',
const Duration(milliseconds: 500),
() {
_processSpellCheck(languageTag, text)
.then(completer.complete)
.catchError(completer.completeError);
},
() => completer.complete([])
);
return completer.future;
}
Future<List<SuggestionSpan>?> _processSpellCheck(String languageTag, String text) async {
if (!_isolateCache.containsKey(languageTag)) {
await _initializeIsolate(languageTag);
}
return await _isolateCache[languageTag]!.check(text);
}
Future<void> _initializeIsolate(String languageTag) async {
final receivePort = ReceivePort();
final isolate = _SpellCheckIsolate();
_isolateCache[languageTag] = isolate;
receivePort.listen(isolate.handleResponsesFromIsolate);
await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort);
final success = await isolate.initialize(languageTag);
if (!success) {
print('Spellcheck initialization failed, locale not supported: $languageTag');
}
}
static void _startRemoteIsolate(SendPort port) {
final receivePort = ReceivePort();
port.send(receivePort.sendPort);
Win32SpellCheckServiceWorker worker = Win32SpellCheckServiceWorker();
receivePort.listen((dynamic message) async {
if (message is SpellCheckIsolateMessage) {
switch(message){
case InitializeSpellCheckIsolateRequest():
var initializeResponse = await worker.initialize(message.languageTag);
port.send(InitializeSpellCheckIsolateResponse(initializeResponse));
case CheckSpellCheckIsolateRequest():
var suggestions = await worker.check(message.text);
port.send(CheckSpellCheckIsolateResponse(message.correlationId, suggestions));
}
}
});
}
}
class _SpellCheckIsolate
{
SendPort? _sendPort;
final Completer _isolateReadyCompleter = Completer();
final Completer<bool> _initializeCompleter = Completer();
final Map<String, Completer<Iterable<CheckError>>> _checkCompleters = {};
late final String _locale;
void handleResponsesFromIsolate(dynamic message) {
if (message is SendPort) {
_sendPort = message;
_isolateReadyCompleter.complete();
} else if (message is SpellCheckIsolateMessage) {
switch (message){
case InitializeSpellCheckIsolateResponse():
if (_initializeCompleter.isCompleted != true)
{
_initializeCompleter.complete(message.success);
}
case CheckSpellCheckIsolateResponse():
var completer = _checkCompleters[message.correlationId];
if (completer?.isCompleted != true)
{
completer?.complete(message.errors);
}
}
}
}
Future<bool> initialize(String locale) async
{
_locale = locale;
await _isolateReadyCompleter.future;
_sendPort!.send(InitializeSpellCheckIsolateRequest(locale));
return await _initializeCompleter.future;
}
Future<List<SuggestionSpan>> check(String text) async
{
await _isolateReadyCompleter.future;
var results = <SuggestionSpan>[];
var localeIsSupported = await _initializeCompleter.future;
if (localeIsSupported)
{
Random random = Random(DateTime.now().microsecondsSinceEpoch);
var correlation = random.nextInt(4294967295).toString();
//var correlation = DateTime.now().microsecondsSinceEpoch.toString();
var completer = Completer<Iterable<CheckError>>();
_checkCompleters[correlation] = completer;
_sendPort!.send(CheckSpellCheckIsolateRequest(correlation, text));
var resultsMap = await completer.future;
for(var result in resultsMap){
var span = SuggestionSpan(
TextRange(start: result.start, end: result.end, ),
result.suggestions
);
results.add(span);
}
} else {
print('Spellcheck failed, locale not supported: $_locale');
}
return results;
}
}
class Win32SpellCheckServiceWorker {
late final ISpellCheckerFactory _spellCheckerFactory;
late final ISpellChecker _spellChecker;
Win32SpellCheckServiceWorker() {
CoInitializeEx(nullptr, COINIT.COINIT_MULTITHREADED);
_spellCheckerFactory = SpellCheckerFactory.createInstance();
}
Future<bool> initialize(String locale) async
{
final languageTagPtr = locale.toNativeUtf16();
final supportedPtr = calloc<Int32>();
final spellCheckerPtr = calloc<COMObject>();
try {
_spellCheckerFactory.createSpellChecker(languageTagPtr, spellCheckerPtr.cast());
_spellCheckerFactory.isSupported(languageTagPtr, supportedPtr);
if (supportedPtr.value == 1) {
_spellChecker = ISpellChecker(spellCheckerPtr);
return true;
} else {
return false; // Return empty result if language is not supported
}
} finally {
free(languageTagPtr);
free(supportedPtr);
// We don't free spellCheckerPtr here as it's now owned by _spellCheckers
}
}
Future<List<CheckError>> check(String text) async {
var result = <CheckError>[];
final errorsPtr = calloc<COMObject>();
final textPtr = text.toNativeUtf16();
try {
final hr = _spellChecker.check(textPtr, errorsPtr.cast());
if (!FAILED(hr)) {
final errors = IEnumSpellingError(errorsPtr);
await _processErrors(errors, _spellChecker, text, result);
errors.detach();
} else {
print("Spellcheck native api call (ISpellChecker.check) failed with code: $hr");
}
} finally {
free(textPtr);
free(errorsPtr);
}
return result;
}
Future<void> _processErrors(IEnumSpellingError errors, ISpellChecker spellChecker, String text, List<CheckError> result) async {
final errorPtr = calloc<COMObject>();
int hr;
while ((hr = errors.next(errorPtr.cast())) == S_OK) {
final error = ISpellingError(errorPtr);
await _processSingleError(error, spellChecker, text, result);
error.detach();
}
if (FAILED(hr)){
print("Spellcheck native api call (IEnumSpellingErrors.next) failed with code: $hr");
}
free(errorPtr);
}
Future<void> _processSingleError(ISpellingError error, ISpellChecker spellChecker, String text, List<CheckError> result) async {
final word = text.substring(error.startIndex, error.startIndex + error.length);
if (error.correctiveAction == CORRECTIVE_ACTION.CORRECTIVE_ACTION_GET_SUGGESTIONS) {
final wordPtr = word.toNativeUtf16();
final suggestionsPtr = calloc<COMObject>();
try {
final hr = spellChecker.suggest(wordPtr, suggestionsPtr.cast());
if (!FAILED(hr)) {
final suggestions = IEnumString(suggestionsPtr);
final suggestionList = await _getSuggestions(suggestions);
suggestions.detach();
result.add(CheckError(
error.startIndex,
error.startIndex + error.length,
suggestionList,
));
} else {
print("Spellcheck native api call (ISpellChecker.getSuggestions) failed with code: $hr");
}
} finally {
free(wordPtr);
free(suggestionsPtr);
}
}
}
Future<List<String>> _getSuggestions(IEnumString suggestions) async {
final suggestionPtr = calloc<Pointer<Utf16>>();
final suggestionResultPtr = calloc<Uint32>();
final suggestionList = <String>[];
try {
int hr;
while ((hr = suggestions.next(1, suggestionPtr, suggestionResultPtr)) == S_OK) {
suggestionList.add(suggestionPtr.value.toDartString());
}
if (FAILED(hr)){
print("Spellcheck native api call (IEnumString.next) failed with code: $hr");
}
} finally {
free(suggestionPtr);
free(suggestionResultPtr);
}
return suggestionList;
}
void dispose() {
_spellChecker.detach();
_spellCheckerFactory.release();
CoUninitialize();
}
}
abstract class SpellCheckIsolateMessage
{
final String correlationId;
SpellCheckIsolateMessage(this.correlationId);
}
class InitializeSpellCheckIsolateRequest extends SpellCheckIsolateMessage
{
final String languageTag;
InitializeSpellCheckIsolateRequest(this.languageTag) : super('');
}
class InitializeSpellCheckIsolateResponse extends SpellCheckIsolateMessage
{
final bool success;
InitializeSpellCheckIsolateResponse(this.success) : super('');
}
class CheckSpellCheckIsolateRequest extends SpellCheckIsolateMessage
{
final String text;
CheckSpellCheckIsolateRequest(super.correlationId, this.text);
}
class CheckSpellCheckIsolateResponse extends SpellCheckIsolateMessage
{
final List<CheckError> errors;
CheckSpellCheckIsolateResponse(super.correlationId, this.errors);
}
class CheckError
{
final int start;
final int end;
final List<String> suggestions;
CheckError(this.start, this.end, this.suggestions);
}
/* a customised EasyDebounce which has a callback for victim invocations */
/// A void callback, i.e. (){}, so we don't need to import e.g. `dart.ui`
/// just for the VoidCallback type definition.
typedef EasyDebounceCallback = void Function();
class _EasyDebounceOperation {
EasyDebounceCallback callback;
EasyDebounceCallback cancelled;
Timer timer;
_EasyDebounceOperation(this.callback, this.cancelled, this.timer);
}
/// A static class for handling method call debouncing.
class EasyDebounce2 {
static Map<String, _EasyDebounceOperation> _operations = {};
/// Will delay the execution of [onExecute] with the given [duration]. If another call to
/// debounce() with the same [tag] happens within this duration, the first call will be
/// cancelled and the debouncer will start waiting for another [duration] before executing
/// [onExecute].
///
/// [tag] is any arbitrary String, and is used to identify this particular debounce
/// operation in subsequent calls to [debounce()] or [cancel()].
///
/// If [duration] is `Duration.zero`, [onExecute] will be executed immediately, i.e.
/// synchronously.
static void debounce(
String tag, Duration duration, EasyDebounceCallback onExecute, EasyDebounceCallback onCancelled) {
if (duration == Duration.zero) {
_operations[tag]?.timer.cancel();
_operations.remove(tag);
onExecute();
} else {
var operation = _operations[tag];
if (null != operation){
operation.timer.cancel();
operation.cancelled();
}
_operations[tag] = _EasyDebounceOperation(
onExecute,
onCancelled,
Timer(duration, () {
_operations[tag]?.timer.cancel();
_operations.remove(tag);
onExecute();
}));
}
}
/// Fires the callback associated with [tag] immediately. This does not cancel the debounce timer,
/// so if you want to invoke the callback and cancel the debounce timer, you must first call
/// `fire(tag)` and then `cancel(tag)`.
static void fire(String tag) {
_operations[tag]?.callback();
}
/// Cancels any active debounce operation with the given [tag].
static void cancel(String tag) {
_operations[tag]?.timer.cancel();
_operations.remove(tag);
}
/// Cancels all active debouncers.
static void cancelAll() {
for (final operation in _operations.values) {
operation.timer.cancel();
}
_operations.clear();
}
/// Returns the number of active debouncers (debouncers that haven't yet called their
/// [onExecute] methods).
static int count() {
return _operations.length;
}
}
截图或视频
- 无响应*
日志
- 无响应*
Flutter Doctor输出
[√] Flutter (频道稳定,3.22.2,在Microsoft Windows [Version 10.0.19045.4529],区域设置 en-NZ)
[√] Windows版本(安装的Windows版本是10或更高版本)
[X] Android工具链 - 为Android设备开发
X 无法定位Android SDK。
从: https://developer.android.com/studio/index.html 安装Android Studio组件以帮助您首次启动。
(或者访问https://flutter.dev/docs/get-started/install/windows#android-setup以获取详细说明)。
如果已将Android SDK安装到自定义位置,请使用 flutter config --android-sdk
更新到该位置。
[√] Chrome - 为Web开发
[√] Visual Studio - 为Windows应用程序开发(Visual Studio Community 2022 17.10.4)
[!] Android Studio(未安装)
[√] VS Code(版本1.91.1)
[√] 已连接设备(3个可用)
[√] 网络资源
3条答案
按热度按时间vyswwuz21#
感谢详细的报告@Adam-Langley
你是否在桌面上看到了报告中提到的行为?还是在其他平台上也有这种情况?
我建议在EditableText的内置实现中添加防抖功能。
似乎已经有社区包提供这个功能了,例如https://pub.dev/packages/flutter_debouncer和https://pub.dev/packages/easy_debounce,你可以检查一下这是否有助于解决你的问题。
9gm1akwq2#
你好,感谢你的参与。
我目前只在为Windows定制/实现拼写检查功能,因此只在Windows上观察到了性能下降的情况。
然而,在深入研究了editabletext和spellcheck代码文件后,我发现完全没有使用防抖动(debouncing)。因此,我的报告针对的是更一般的情况——iOS和Android也很可能执行不必要的拼写检查。这是我的主要关注点。
你会看到我的代码示例已经使用了easydebounce来解决原生防抖动的缺失——然而,iOS和Android应用的开发人员不应该不得不重写并“修补”内置的默认拼写检查服务以避免这种次优行为。
当然,我可以花更多的时间提供移动端的性能指标,但我觉得熟悉这个的人应该能立刻知道我的代码解读是否正确。
如果我的报告没有按照我刚刚描述的目的正确提交,我应该如何修改它?
另外:我已经修改了我的原始报告和代码示例。在我最初的报告中,我提到拼写检查子系统是如何生成索引越界的错误的。这确实发生了,但我找到了这个原因,它可能是另一个错误——所以我会单独报告( #152272 )。我已经删除了那个评论,并修复了代码示例以避免这个错误。
感谢你的反馈
ma8fv8wu3#
我目前仅针对Windows系统定制/实现拼写检查功能,因此在Windows上仅“观察到”了性能下降。
Flutter目前尚未支持桌面上的EditableText拼写检查功能。参见 #122433
然而,在深入研究editabletext和spellcheck代码文件后,我发现完全缺乏防抖动功能。因此,我的报告主要针对更一般的情况 - 即iOS和Android也可能执行不必要的拼写检查。这是我的主要关注点。
请查看使用 https://api.flutter.dev/flutter/material/TextField/enableSuggestions.html 并将其设置为
false
是否仍然具有相同的行为。但我发现了这个原因,这可能是另一个错误 - 所以我将单独报告它( #152272 )。
如果这仅限于桌面,那么很可能是由于我之前分享的关联问题。