У меня есть некоторые проблемы, которые трудно решить. Они случаются в приложении время от времени не в обычном порядке. Я думаю, что проблема может быть с некоторыми условиями гонки и синхронизацией.
Я использую такой шаблон.
1) reloadData ()
2) reloadSection1 (), reloadSection2 (), reloadSection2 () и т. Д.
3) У меня есть события касания, которые могут выполнять перезагрузку, какreloadData ()
4) Есть также сообщения Socket.IO, которые могут вызвать reloadData (), reloadSectionN ()
5) Я пытаюсь использовать debouncers для выполнения только последней перезагрузки данного типа.
6) debouncers используют Serial Queue для последовательного выполнения задачи по очереди в случае, если запрос-ответ-перезагрузка длится дольше, чем наступает новое событие socket.io
7) должна произойти перезагрузка раздела / таблицыв потоке пользовательского интерфейса, поэтому в конце я перехожу к нему, используя DispatchQueue.main.async {}
8) Я даже пытался обернуть излишки / перезагрузки данных в семафоры, чтобы заблокировать изменение другими потоками.
Но могут случиться ошибки и приложение может аварийно завершиться, несмотря на это. И я понятия не имею, что может вызвать это.
Ниже я размещаю наиболее важные части кода.
У меня есть такие свойства экземпляра:
private let debouncer = Debouncer()
private let debouncer1 = Debouncer()
private let debouncer2 = Debouncer()
private let serialQueue = DispatchQueue(label: "serialqueue")
private let semaphore = DispatchSemaphore(value: 1)
Здесь метод экземпляра Debouncer.debounce
func debounce(delay: DispatchTimeInterval, queue: DispatchQueue = .main, action: @escaping (() -> Void) ) -> () -> Void {
return { [weak self] in
guard let self = self else { return }
self.currentWorkItem?.cancel()
self.currentWorkItem = DispatchWorkItem {
action()
}
if let workItem = self.currentWorkItem {
queue.asyncAfter(deadline: .now() + delay, execute: workItem)
}
}
}
Здесь обсуждаются повторные перезагрузки представления таблицы и ее разделов
func debounceReload() {
let debounceReload = debouncer.debounce(delay: .milliseconds(500), queue: serialQueue) {
self.reloadData()
}
debounceReload()
}
func debounceReloadOrders() {
let debounceReload = debouncer1.debounce(delay: .milliseconds(500), queue: serialQueue) {
self.reloadOrdersSection(animating: false)
}
debounceReload()
}
Эти методы debounce могут вызываться при нажатии, обновлении экрана для навигации по экрану или событиях Socket.IO (здесь возможно несколько событий одновременноесли есть несколько пользователей).
Каждая перезагрузка, вызываемая перезагрузкой debounce, запускается и заканчивается такими методами (между ними есть синхронные запросы к удаленному API, которые могут занять некоторое время (и выполняются в этой очереди serial). Всеdebouncers повторно используют одну и ту же последовательную очередь (поэтому они не должны конфликтовать / состязаться друг с другом и вызывать несогласованность данных при перезагрузке представления таблицы или ее разделов).
private func startLoading() {
print("startLoading")
activityIndicator.startAnimating()
activityIndicator.isHidden = false
tableView.isHidden = true
// cancel section reload
debouncer1.currentWorkItem?.cancel()
debouncer2.currentWorkItem?.cancel()
}
private func stopLoading() {
guard debouncer.currentWorkItem?.isCancelled != true else { return }
self.semaphore.wait()
print("stopLoading")
tableView.reloadData()
activityIndicator.isHidden = true
refreshControl.endRefreshing()
tableView.isHidden = false
self.semaphore.signal()
}
Выше добавлены эти дополнительные семафоры в качестве дополнительной проверки для обеспечения данныхсогласованность.
func startLoading(section: Int, animating: Bool = true) {
self.semaphore.wait()
print("startLoading section \(section)")
tableView.beginUpdates()
if animating {
self.data[section] = .loadingSpinner
}
tableView.reloadSections([section], with: .none)
tableView.endUpdates()
self.semaphore.signal()
}
func stopLoading(section: Int, model: Model) {
self.semaphore.wait()
print("stopLoading section \(section)")
if section == 0 {
guard debouncer1.currentWorkItem?.isCancelled != true else { return }
} else if section == 1 {
guard debouncer2.currentWorkItem?.isCancelled != true else { return }
}
tableView.beginUpdates()
self.data[section] = model
tableView.reloadSections([section], with: .none)
tableView.endUpdates()
self.semaphore.signal()
}
private func clearData() {
self.semaphore.wait()
print("clearData")
data.removeAll()
self.semaphore.signal()
}
Я думаю, что эти дополнительные семафоры не требуются, посколькуэто использует последовательную очередь, поэтому все запрос-ответ-перезагрузка выполняются в последовательной очереди. Может быть, проблема в том, что мне нужно переключиться с последовательной очереди на основную, чтобы очистить / добавить спиннер-перезагрузить, а затем заполнить данные / перезагрузить таблицу или раздел. Но я думаю, что это должно длиться короче, чем следующая замена данных. Я рассматриваю перемещение self.data.append (model1) в критическую секцию семафора в stopLoading () для reloadData (), используя self.data = назначение данных в этой критической секции.
Пример ошибки, с которой я столкнулся:
Неустранимое исключение: NSInternalInconsistencyException Недопустимое обновление: недопустимое количество строк в разделе 0. Количество строк в существующем разделе после обновления (3) должно быть равно количеству строк, содержащихся в этом разделе до обновления (5), плюс или минус количество строк, вставленных или удаленных из этого раздела (0 добавлено, 0 удалено) и плюс или минус количество перемещенных строкв или из
0x195bef098 +[_CFXNotificationTokenRegistration keyCallbacks]
3 Foundation 0x1966b2b68 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:]
4 UIKitCore 0x1c23ecc78 -[UITableView _endCellAnimationsWithContext:]
5 UIKitCore 0x1c24030c8 -[UITableView endUpdates]
6 MyApplication 0x104b39230 MyViewController.reload(section:) + 168
> that section (0 moved in, 0 moved out).
Я также видел ошибки в функции cellForRow, эти ошибки происходят несколько раз в неделю, приложение довольно часто используется несколько раз в день, поэтому трудно повторить эту ошибку. Я пытался отправить простые обновляющие сокеты из POSTman, но они не меняют базовые данные (количество строк), и я думаю, что все работает хорошо.
ОБНОВЛЕНИЕ Я обновил stopLoading () для stopLoading (data :), чтобы иметь обновление источника данных и tableView.reload в главной очереди. Поэтому все методы startLoading, stopLoading и reload выполняются в DispatchQueue.main.async {}.
private func stopLoading(data: [Model]) {
guard debouncer.currentWorkItem?.isCancelled != true else { return }
self.semaphore.wait()
print("stopLoading")
self.data = data
tableView.reloadData()
activityIndicator.isHidden = true
refreshControl.endRefreshing()
tableView.isHidden = false
self.semaphore.signal()
}