Ошибка Табличного Представления при перезагрузке секций - кажется, некоторое условие гонки - PullRequest
0 голосов
/ 30 сентября 2019

У меня есть некоторые проблемы, которые трудно решить. Они случаются в приложении время от времени не в обычном порядке. Я думаю, что проблема может быть с некоторыми условиями гонки и синхронизацией.

Я использую такой шаблон.

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()
    }
...