Connect with us

Разработка

Взгляд на мой процесс отладки (с реальными примерами)

Главный вывод заключается в том, что знание инструментов и системный подход к отладке сэкономят вам бесчисленное количество часов. Будь то логи сбоев, инструменты профилирования или старое доброе комментирование кода, правильный инструмент для каждой задачи имеет решающее значение.

Опубликовано

/

     
     

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

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

Я работаю iOS-разработчиком уже более 7 лет и за это время работал над множеством приложений разного размера и сложности. Хотя проекты различались по тематике и технологическому стеку, одно оставалось неизменным — необходимость диагностики и исправления ошибок.

Знание того, как отлаживать приложение, и знание или хотя бы знакомство с имеющимися в вашем распоряжении инструментами — это навык, который сэкономит вам много времени и позволит быстрее перейти к следующей задаче. Это стало для меня особенно важно при работе над собственными инди-приложениями, поскольку ресурсы и время ограничены, и мне нужно использовать их максимально эффективно, при этом ставя на первое место удобство пользователей.

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

Ошибка 1: Сбой

Одна из самых распространённых проблем, требующих вашего полного внимания, — это сбой.

Недавно несколько пользователей Helm сообщили, что функция перевода падает сразу после нажатия кнопки «Перевести всё»:

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

Если вы когда-либо пытались просмотреть файл лога со сбоями, то знаете, что его сложно прочитать, и иногда он указывает на какую-то внутреннюю системную библиотеку, которая практически не связана с вашим кодом. В последнем случае становится гораздо сложнее понять, что происходит, не говоря уже об устранении сбоя.

Вот фрагмент трассировки стека для отчёта о сбое:

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libsystem_kernel.dylib        	       0x199001388 __pthread_kill + 8
1   libsystem_pthread.dylib       	       0x19903a88c pthread_kill + 296
2   libsystem_c.dylib             	       0x198f43c60 abort + 124
3   libsystem_c.dylib             	       0x198f42eec __assert_rtn + 284
4   AppKit                        	       0x19de49ff0 _nsis_frameInEngine + 1608
5   AppKit                        	       0x19de51acc -[NSView(NSConstraintBasedLayoutInternal) systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:] + 356
6   SwiftUI                       	       0x1ca5de204 PlatformViewHost.intrinsicContentSize.getter + 264
7   SwiftUI                       	       0x1ca5de0d4 @objc PlatformViewHost.fittingSize.getter + 40
8   AppKit                        	       0x19de51d70 -[NSView(NSConstraintBasedLayoutInternal) measureMin:max:ideal:stretchingPriority:] + 300
9   SwiftUI                       	       0x1ca5decbc PlatformViewHost.intrinsicLayoutTraits() + 384
10  SwiftUI                       	       0x1ca5dc804 PlatformViewHost.layoutTraits() + 608
11  SwiftUI                       	       0x1ca59c848 closure #1 in ViewLeafView.layoutTraits() + 224
12  SwiftUI                       	       0x1ca59c744 ViewLeafView.layoutTraits() + 52
13  SwiftUI                       	       0x1ca59c480 closure #1 in ViewLeafView.sizeThatFits(in:environment:context:) + 1420
14  SwiftUICore                   	       0x200429490 specialized static Update.syncMain(_:) + 84
15  SwiftUI                       	       0x1ca599b8c closure #1 in PlatformViewLayoutEngine.sizeThatFits(_:) + 112
16  SwiftUICore                   	       0x2004d32c4 ViewSizeCache.get(_:makeValue:) + 360
17  SwiftUI                       	       0x1ca599ad4 PlatformViewLayoutEngine.sizeThatFits(_:) + 264

Как видите, он содержит только записи из фреймворков SwiftUI и AppKit, что мне не особо помогло. В этот момент я обратился к ChatGPT, чтобы лучше разобраться в логе и выяснить, знает ли он больше о причинах сбоя в этих внутренних методах.

Вот какой ответ я получил:

Взгляд на мой процесс отладки (с реальными примерами)

Опять же, хотя ChatGPT не указал точную причину проблемы, он указал мне верное направление следующим утверждением: «_nsis_frameInEngine — это внутренний метод AppKit, который срабатывает при нарушении согласованности состояния макета». Хотя я не был до конца уверен, какое именно представление вызывает проблему, я понимал, что мне нужно сосредоточиться на коде пользовательского интерфейса и попытаться выяснить, что может привести к неправильной компоновке макета.

Поскольку из отчёта пользователя я знал, что проблема возникала при нажатии кнопки «Перевести всё» на экране переводов, я обратился к отдельному источнику информации, чтобы лучше понять контекст, в котором произошёл сбой: Diagnostics.

Если вы не знакомы с Diagnostics, это замечательная библиотека с открытым исходным кодом, созданная Антуаном ван дер Ли. Она собирает множество полезной информации, включая логи, сетевые запросы и многое другое, из сеанса пользователя в вашем приложении, а затем генерирует понятный HTML-отчёт, которым можно поделиться с разработчиками приложения.

В этой конкретной ситуации Diagnostics дал нам ключевую информацию:

Взгляд на мой процесс отладки (с реальными примерами)

Как видите, отчёт показывает, что сетевой запрос на перевод всего контента не удалось выполнить непосредственно перед сбоем. Может ли это объяснить, почему нам не удалось воспроизвести проблему на нашей стороне? Может ли быть, что сбой произошёл только тогда, когда сетевой запрос на перевод всего контента не удалось выполнить?

Чтобы проверить эту гипотезу, я обратился к инструменту, который я чаще всего использую для отладки сетевых проблем (или даже иногда для проверки корректности своей реализации) — Proxyman, приложению для macOS, которое позволяет перехватывать и изменять сетевые запросы и ответы.

Я нашёл запрос на перевод, который отправляло приложение, и добавил его в «Список блокировки» в Proxyman, чтобы он не выполнялся в приложении:

Взгляд на мой процесс отладки (с реальными примерами)

И сразу же, как только я нажал кнопку «Перевести всё», приложение упало. Воспроизведя ошибку, я вскоре понял, что она произошла при отображении нашего пользовательского представления об ошибке Toast, поскольку оно размещалось в HStack, не имея достаточного места для удовлетворения его ограничений.

Я превратил его в overlay, гарантируя отсутствие сбоев в макете, и приложение смогло отобразить представление без падения.

Ошибка 2: Снижение производительности

Некоторое время назад мы заметили, что одно из представлений в нашем приложении загружается дольше, чем в предыдущих версиях. Хотя рост и усложнение приложения неизбежно по мере добавления новых функций, важно, чтобы влияние этих функций на производительность было минимальным.

Представление, вызвавшее снижение производительности, было, пожалуй, самым важным: экран версии. Это приложение выполняет множество одновременных сетевых запросов для получения всей необходимой информации, и оно должно выполнять их все параллельно, чтобы пользователь мог видеть актуальную информацию как можно скорее.

Взгляд на мой процесс отладки (с реальными примерами)

Первое, что я сделал — использовал шаблон Time Profiler в «Инструментах» Xcode, чтобы увидеть, на что тратится время:

Взгляд на мой процесс отладки (с реальными примерами)

После профилирования приложения мне стало совершенно очевидно, что некоторые сетевые запросы выполнялись последовательно, хотя должны были выполняться параллельно. Это приводило к увеличению времени загрузки.

В этот момент я обратился к кодовой базе, чтобы исправить проблему с помощью структурированного параллелизма. Я взял следующий код, который выполняет каждую асинхронную задачу последовательно:

let submission: Void = await SubmissionManager.shared.setup(for: app, platform: version.platform)
let versionInfo = try await app.getInformation(
    forVersion: version,
    otherPlatforms: app
        .availableVersions
        .reduce(into: [AvailableVersion](), { partialResult, iteratorVersion in
            guard version != iteratorVersion, iteratorVersion.platform != version.platform else { return }
            if !partialResult.contains(where: { $0.platform == iteratorVersion.platform }) {
                partialResult.append(iteratorVersion)
            }
        })
)
let updatedApp: AppStoreConnectApp? = try await {
    guard self.appID != app.id || forceRefresh else { return app }

    return try await store.getApp(byID: app.id, forceRefresh: true)
}()

И переделал его в код, который запускает все запросы параллельно:

async let submissionsRequest: Void = SubmissionManager.shared.setup(for: app, platform: version.platform)
async let appInformation = try app.getInformation(
    forVersion: version,
    otherPlatforms: app
        .availableVersions
        .reduce(into: [AvailableVersion](), { partialResult, iteratorVersion in
            guard version != iteratorVersion, iteratorVersion.platform != version.platform else { return }
            if !partialResult.contains(where: { $0.platform == iteratorVersion.platform }) {
                partialResult.append(iteratorVersion)
            }
        })
)
async let refreshedAppInfo: AppStoreConnectApp? = {
    guard self.appID != app.id || forceRefresh else { return app }

    return try await store.getApp(byID: app.id, forceRefresh: true)
}()

let (versionInfo, updatedApp, _) = try await (appInformation, refreshedAppInfo, submissionsRequest)

Как и в этом примере, большую часть времени обычно тратят на поиск проблемы, а не на её устранение. Именно поэтому так важно знать, как использовать инструменты, чтобы максимально эффективно найти ошибку.

Ошибка 3: Неожиданные системные запросы

Это, безусловно, одна из самых сложных ошибок, которые мне приходилось исправлять за последнее время. Один из наших пользователей сообщил, что приложение выдавало системный запрос на доступ к локальным устройствам в его сети сразу после открытия приложения. Этот отчёт нас удивил, поскольку мы не пытаемся получить доступ к функциям локальной сети в приложении и поэтому не спрашиваем у пользователя разрешения на это. Это вызвало бы ненужные трудности и испортило бы первое впечатление:

Взгляд на мой процесс отладки (с реальными примерами)

Я начал расследование, чтобы понять, почему Apple считает, что наше приложение пытается получить доступ к локальной сети и соответствующим разрешениям. И вот тут-то история стала интересной:

  • Предупреждение появлялось на устройстве только один раз, а в macOS этот параметр нельзя ресетнуть. Как вы можете себе представить, воспроизведение этой ситуации было настоящим кошмаром: мне приходилось постоянно перезагружать компьютер, чтобы увидеть предупреждение.
  • В консоли нет никаких логов или какой-либо конкретной информации, которая помогла бы мне понять причину появления предупреждения.
  • Причиной могли быть сторонние библиотеки, и, поскольку мы используем их достаточно, пришлось написать много кода.

Мы знали, что запрос отображается при запуске приложения, поэтому весьма вероятно, что метод, вызывающий его появление, вызывается в при инициализации структуры нашего приложения. Эта информация дала мне очень надежную отправную точку и оказалась решающей для поиска первопричины проблемы.

Вот всё, что мы инициализируем при запуске нашего приложения:

@main
struct HelmApp: App {
    // ...
    init() {
        do {
            try DiagnosticsLogger.setup()
        } catch {
            ErrorLogger.log("Failed to setup the Diagnostics Logger", for: .generic)
        }
        AIProxy.configure(
            logLevel: .debug,
            printRequestBodies: false,
            printResponseBodies: false,
            resolveDNSOverTLS: true,
            useStableID: true
        )
        HelmProManager.configureRevenueCat()
        let config = TelemetryDeck.Config(appID: Keys.telemetryDeckAppID)
        TelemetryDeck.initialize(config: config)
        
        NSWindow.allowsAutomaticWindowTabbing = false
        validator = AppStoreAPI.shared
        helmPro   = HelmProManager.shared
        let versionHistoryManager = VersionHistoryManager(storage: StorageManager.history)
        versionRefresher = VersionsRefresher(versionHistoryManager: versionHistoryManager)
        store = Store()
    }
}

Первым делом я закомментировал всё в методе init(), чтобы убедиться, что именно один из этих вызовов вызывал появление запроса. После того, как я всё закомментировал и снова запустил приложение, запрос больше не появлялся. Затем я начал раскомментировать вызовы один за другим, пока не нашёл метод, обращающийся к функциям локальной сети.

Определив метод, вызывающий проблему, я исследовал внутренние процессы, приводящие к появлению запроса. Поскольку файлы зависимостей удалённого SPM нельзя редактировать непосредственно в исходном коде, я клонировал репозиторий и заменил удалённый пакет Swift на новый клонированный.

Этот шаблон очень помог мне в прошлом и обеспечивает очень простой способ отладки и понимания проблем со сторонними библиотеками. Получив локальную копию пакета, я повторил тот же процесс: я закомментировал всё в коде библиотеки и постепенно добавлял логику, пока запрос не появился снова. И наконец, мне удалось определить точный фрагмент кода, который вызывал появление запроса:

enum Device {
    static var systemName: String {
        #if os(macOS)
        //  This is the culprit line!
        return ProcessInfo().hostName
        #else
        return UIDevice.current.systemName
        #endif
    }
}

Мы исправили это, форкнув библиотеку и заменив приведённый выше код другой реализацией, чтобы как можно скорее выпустить релиз, а также уведомили сопровождающего библиотеки о проблеме, чтобы её можно было исправить в следующем релизе.

Хотя процесс выявления причины этой ошибки был не самым простым, для устранения некоторых ошибок достаточно нескольких добрых традиционных операторов print, комментариев и изрядной доли терпения.

Заключение

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

  • Сбои часто требуют анализа трассировки стека с использованием диагностических инструментов, таких как Diagnostics, и инструментов сетевой отладки, таких как Proxyman.
  • Проблемы с производительностью можно эффективно выявить с помощью Xcode Instruments и чаще всего устранить, если правильно понимать такие возможности языка Swift, как структурированный параллелизм.
  • Загадочное поведение системы может оказаться самым сложным для исправления и часто требует систематического подхода к поиску проблемы и терпения для отслеживания кода сторонних библиотек.

Главный вывод заключается в том, что знание инструментов и системный подход к отладке сэкономят вам бесчисленное количество часов. Будь то логи сбоев, инструменты профилирования или старое доброе комментирование кода, правильный инструмент для каждой задачи имеет решающее значение.

Помните: большую часть времени отладки тратят на поиск проблемы, а не на её устранение. Инвестируйте в хорошее изучение инструментов отладки — это время окупится на протяжении всей вашей карьеры разработчика.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: