Как мы делали приложение с crash-free 99.5%

Как мы делали приложение с crash-free 99.5%

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

Мы будем рассматривать приложение Sweetmeet. Это продукт для онлайн знакомств, разработанный в компании Фотострана. Основные возможности в приложении – смахивание карточек, профиль пользователя, чаты. Проект разрабатывался и поддерживался в течение 2,5 лет.

Как выглядит приложение

Немного о том, из чего состоит приложение: - на текущий момент проект содержит 72 000 строк кода. - 69 разных экранов (View Controller) - поддерживает iPhone, iPad iOS 9+ - для постоянного соединения с сервером используются веб-сокеты - база данных Realm для локального хранения сообщений и настроек приложения (вместо UserDefaults) - поддерживаются Apple Watch.

На базе одного проекта реализовано 2 разных версии (в виде target): для РФ и для зарубежной аудитории, которые представлены в магазине разными приложениями.

Есть два основных критерия качества: - Защита от падений - Отсутствие логических ошибок

В данном материале мы акцентируем внимание на том, как обеспечить высокий уровень качества с точки зрения минимизации падений. Прежде всего мы начали с правильного тестирования приложения.

Подход к тестированию

Для полноценного тестирования приложения необходимо полностью определить кейсы, которые важно проверять. Для себя мы выбрали следующий подход. Приложение отправляется на review в Apple еженедельно по пятницам в первой половине дня. Это позволяет планировать итерации разработки на календарные недели с понедельника по пятницу. Модерация приложения занимает в среднем 2-3 дня, поэтому настоящий релиз состоится в понедельник-вторник. Как раз в это время команда полна сил, чтобы в случае обнаружения ошибок быстро предпринять шаги по устранению ошибок. Перед каждым релизом собирается набор кейсов, которые входят в план тестирования. Набор зависит от того, какие функции были реализованы, что могло быть затронуто (это определяется еще по коммитам). Также обязательно тестируются критически важные области (регистрация, личные сообщения).

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

Как измерять crash-free? Ниже рассмотрим инструменты, которые помогают нам делать приложение лучше.

Fabric (Crashlytics)

Это набор для разработчика, позволяющий проводить beta тестирование (не через Testflight) и собирающее аналитику. В том числе этот сервис собирает краши приложения. Изначально разрабатывался твиттером, позднее был куплен гуглом. Есть метрика Crash-free users: это процент пользователей, у которых не было падений приложения за последние 7 дней. На эту метрику мы опирались, как KPI качества работы приложения. Хорошим значением crash-free считается показатель от 99 до 99,5%. Сюда входят как краши внутри системы, так и краши сторонних библиотек, на которые мы, к сожалению, повлиять не можем.

Crash free

Следующим шагом было подключение crashlytics в slack канал, чтобы своевременно получать уведомления.

Slack alerts

Сразу после релиза приложения, пока еще мало пользователей обновилось, значение crash-free обычно выше.

XCode

Не многие знают, что XCode умеет показывать краши, причём очень хорошо. Window -> Organizer -> Crashes

Xcode crash view

Ошибку можно открыть в проекте, XCode выделит строку с ней и покажет кучу дополнительной информации об окружении (потоки, память и пр).

FLEX

Этот инструмент мы включаем только на тестовых сборках.

В основном мы используем 2 функции: просмотр истории запросов по сети и просмотр иерархии View.

Стоит заметить, что в XCode тоже есть инструмент для отладки иерархии представлений, но он не такой удобный.

Swiftlint

Swiftlint - инструмент от создателей realm для автоматической проверки Swift кода.

Мы обязали всех разработчиков установить и использовать Swiftlint. Для этого в Build Phases добавили секцию

if which swiftlint >/dev/null; then
  swiftlint
else
  echo "error: SwiftLint does not exist, download it from https://github.com/realm/SwiftLint"
  exit 1
fi

После установки Swiftlint при каждой сборке проекта будет происходить проверка по различным правилам.

Много правил выключено, чтобы было проще начать использовать Swiftlint и не исправлять тысячи ошибок. Свод правил позволяет повысить читаемость кода и запрещает небезопасные операции (например force_unwrapping).

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

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

disabled_rules:
  - unused_closure_parameter
  - closure_parameter_position
  - discarded_notification_center_observer
  - notification_center_detachment
opt_in_rules:
  - closure_spacing
  - missing_docs
  - empty_count
  - force_unwrapping
  - vertical_whitespace
  - overridden_super_call
  - prohibited_super_call
  - redundant_nil_coalescing
  - object_literal

Исключить каталоги с исходными кодами для проверки

excluded: # paths to ignore during linting.
  - Carthage
  - Pods
  - Library/External

Максимальная длина файла. После 1500 строк генерируется предупреждение (warning), после 1700 - ошибка

file_length:
  - 1500
  - 1700

Правила по длине функций, переменных, названиям…

type_body_length:
  - 400
  - 400

type_name:
  min_length: 3
  max_length:
    - 50
    - 50
  excluded:
    - iPhone # разрешить название переменной iPhone

variable_name:
  min_length: 3
  max_length:
    - 40
    - 40
  excluded: # разрешить переменным называться так
    - i
    - x
    - y
    - z
    - id
    - nc
    - to
    - vc

# постепенно уменьшить до 40
function_body_length:
  - 50
  - 50

# уменьшить ширину строки до 120
line_length:
  - 125
  - 125

Количество параметров в функции

function_parameter_count:
  - 5
  - 5

Цикломатическая сложность

cyclomatic_complexity:
  - 13
  - 13

Force unwrapping

force_unwrapping:
  severity: error

Другие правила для повышения читаемости кода

trailing_whitespace:
  ignores_empty_lines: true

colon:
  flexible_right_spacing: false
  severity: error

empty_parentheses_with_trailing_closure:
  severity: error

empty_count:
  severity: error

vertical_whitespace:
  severity: error

prohibited_super_call:
  severity: error

overridden_super_call:
  severity: error

statement_position:
  statement_mode: default

Ошибки будут выдаваться как стандартные ошибки при компиляции XCode

reporter: "xcode" # reporter type (xcode, json, csv, checkstyle)

Ограничим количество предупреждений

warning_threshold: 10

Самодельные правила

custom_rules:

  comma_space_rule:
    regex: ",[ ]{2,}"
    message: "Expected only one space after ',"

  empty_line_after_guard:
      name: "Empty Line After Guard"
      regex: "(^ *guard[ a-zA-Z0-9=?.\(\),><!]*\{[ a-zA-Z0-9=?.\(\),><!]*\}\n *(?!(?:return|guard))\S+)"
      message: "There should be an empty line after a guard"
      severity: error

Допускается иметь в проекте 10 предупреждений, если их будет больше проект не скомпилируется. Доступные правила можно найти по ссылке.

Репозиторий

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

Мы считаем ветку origin/master главной. То есть, исходный код в ней должен находиться в состоянии production-ready в любой произвольный момент времени. Ветвь origin/develop мы считаем главной ветвью для разработки. Хранящийся в ней код в любой момент времени должен содержать самые последние изданные изменения, необходимые для следующего релиза.

Когда исходный код в ветви разработки (develop) достигает стабильного состояния и готов к релизу, все изменения должны быть определённым способом влиты в главную ветвь (master) и помечены тегом с номером релиза (v.1.2.3).

Ветка master считается заблокированной. Нельзя делать push в master. Нельзя делать push в developer. Все изменения должны быть в личных ветках. После готовности новых изменений создается Merge Request на ответственного соседа. Все изменения в develop вносятся через merge request.

Кроме того в репозитории реализованы хуки. При наличии `//fixme с описанием нельзя сделать push в репозиторий. Это позволяет временно ломать приложение, чтобы протестировать новую фичу или найти ошибку, но не даст забыть что-то и отправить неработающий код в репозиторий.

Fastlane

Еще одна разработка twitter. Мы автоматизировали сборку приложения и отправку на тестирование через fastlane. Позволяет в одну команду скомпилировать приложение с необходимыми Provision Profile и отправить в fabric или в testflight. Это экономит нам порядка 12 минут на каждую сборку. Вот lane, который мы используем для автоматической сборки и отправки в fabric.

# Компиляция

gym(
  clean: true,
  silent: true,
  configuration: "Debug",
  scheme: "sweetmeet2",
  export_method: "development",
  include_bitcode: false,
  verbose: true,
  export_team_id: "YOUR_TEAM_ID",
  output_directory: "build",
  output_name: "sweetmeet2.ipa",
  xcargs: "ARCHIVE=YES" # Used to tell the Fabric run script to upload dSYM file
)

# Отправить в Crashlytics

crashlytics(
  ipa_path: "build/sweetmeet2.ipa",
  api_token: "TOKEN",
  build_secret: "SECRET",
  groups: ['SweetMeet'],
  notes: "AUTO BUILD"
)

# Получить версию и номер билда

version = `/usr/libexec/plistbuddy -c Print:CFBundleShortVersionString '../Application/sweetmeet2.Info.plist'`.strip
build = `/usr/libexec/plistbuddy -c Print:CFBundleVersion '../Application/sweetmeet2.Info.plist'`.strip

# При желании можно отправлять сообщение в Slack или Telegram

slack(
  message: "App Successfully uploaded to Crashlytics.",
  default_payloads: [], # reduce the notification to the minimum
  payload: {
    "Version" => version,
    "Build" => build
  }
)

Резюмируя

Первым делом мы привели в порядок процесс тестирования. Добавили маст-хев в виде тест-кейсов и обязательных ежемесячных полных проверок. Далее научились правильно собирать и анализировать краши со всех возможных источников. Далее занялись кодом. Выделили группы ошибок, которые могут привести к падению приложения. Проанализировали всю работу с сетью. Вынесли все запросы в отдельным модуль. Проверили, что при любых ответах сервера (в том числе и неполучении ответа) приложение ведет себя корректно. 99% запросов могут повторяться, если ответ не был получен или была получена 500 ошибка сервера.

Вторым шагом избавились от конструкций UIStoryboard(named: )... и вынесли всю навигацию в отдельный модуль. Это позволило иметь одну точку входа в приложение в том числе пуш-уведомления, переход по App Links, переход по Deep Links. Появилась сущность Router, которая руководит всем переходами и всеми анимациями внутри переходов.

Частой ошибкой является утечка памяти. К счастью у нас она была всего в одном месте. Найти помогло простое решение, в базовом классе ViewController в конструкторе добавили инкремент кол-ва открытых контроллеров, в deinit добавили декремент. В отладочном меню всегда можно посмотреть это значение. Если оно превышает пороговое значение (в нашем случае это 8) то пишется лог. Кстати, отладочное меню для тестировщиков содержит расширенную информацию о текущем состоянии внутри приложения (память, пользователь, различные состояния приложения).

Очень сильно помогло избавиться от фантомных крашей избавление от всех небезопасных действий. Это force_unwrap и принудительное приведение типов. В этом нам помог SwiftLint. Мы стали больше использовать Optional типов вместо заранее определенных. На момент начала работы над crash-free приложения его значение было примерно 95%. За 6 релизов и примерно 2 месяца нам удалось добиться 99,5% crash-free.

Crash-free результаты

Комментарии

comments powered by Disqus