Привет! Меня зовут Абакар и я работаю главным тех лидом в Альфа Банке. Меня часто посещает вопрос — «А какой навык всё-таки самый полезный для разработчика?». Понятное дело, что ответ на этот вопрос обязан быть комплексным и скилл сет разраба не должен ограничиваться одним навыком. Но умение дебажить действительно хороший показатель уровня разработчика. Давай разберем на нескольких примерах почему я так считаю.

Пример №1

У нас на проекте используется дизайн-система и фичи строят свои экраны из компонентов дизайн-системы. Допустим, мы столкнулись с тем, что нам нужно занулять клик лисенер для компонента при определенных кейсах. Ну, в общем-то, дело не самое хитрое:

view.setOnСlickListener(null)

Запускаем код и он не работает.

Какие есть мысли почему не работает? Таких мыслей может быть много:

  • возможно кто-то проставляет клик лисенер после того, как мы проставили его в null;

  • может просто сам метод не отрабатывает так, как нужно;

  • а может мы зануляем клик лисенер не у той вьюхи;

  • а может это как-то связано с версией андроида;

  • а возможно наш компонент использует для своих нужд вьюху не из support library, в которой есть баги.

Какой самый топорный вариант проверить все эти гипотезы? Просто пройтись по ним и вычислить методом тыка.

Но это плохой способ:

  • На него тратится много времени, потому что каждый из вариантов надо пройти, а это — внесение изменения, перекомпиляция проекта, проверка того, что решения для ваших гипотез не мешают друг другу и так далее.

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

У тебя может возникнуть логичный вопрос — «А как тогда лучше всего решить проблему выше?» Половина решения проблемы — это нахождения её причины. Давай попробуем найти причину.

Нам помогут два самых главных навыка разработчика — умение читать чужой код и разбираться в нем + умение дебажить. Но перед тем как идти дальше давай разберемся какие виды дебага существуют?


Виды дебага (aka виды отладки)

Давай пройдемся по тому, как можно дебажить наше приложение. Можешь в комментах накидать какие виды дебага ты применял или какие знаешь =) У меня их пять.

№1. Просмотр исходного кода

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

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

№2. Дебаг логгированием

В андроиде, например, уже есть много системных логов, но также мы можем добавлять свой вывод, как просто через system.out.println("my_text"), так и методы класса
Log.v(), Log.d(), Log.i(), Log.w(), и Log.e() — про каждый из этих методов можно прочитать отдельно в главе Log документации Android.

Мемный пример того, как используется логирование в Android SDK.

ActivityThread
ActivityThread

Но и оно может принести пользу, если я разработчик SDK и мне понадобилось получить нужные логи при дебаге ActivityThread — просто меняю тут флажочек и смотрю вывод в консоли.

Вот как могут выглядеть логи в консоли:

Логи android
Логи android

Ну и самый важный момент — мы можем добавлять свои логи и выводить любую информацию, которая может помочь при отладке. Важно ещё уметь читать ту информацию, которая выводится в логах. Давай пройдемся по нескольким примерчикам (они взяты из реальной жизни) и ты попробуешь по аутпуту понять, в чём проблема. Сразу под картинками с логами будут ответы.

1) Тест не проходит и выдает вот такую ошибку:

Not Mocked
Not Mocked

Ответ: У мока не хватает описания — что делать, когда вызывается метод putSerializable. Если добавить мок этого метода, то тест начнет проходить. На самом деле именно об этом нам говорят логи, и если внимательно их прочитать, то можно всё понять.

2) Открываем экран и он крашит в рантайме.

Multiple fields
Multiple fields

Ответ: Dto, сущность, которая описана в data слое, объявляет два поля с дублирующими именами. Чтобы избавиться от краша, достаточно исправить проблему с дублированием нейминга полей.


№3. Дебаг по брейкпоинтам

Это вид отладки, когда мы проверяем работу программы по заданными нами брейкпоинтам. При этом конкретно Андроид Студия даёт богатое количество возможностей при данном виде отладки:

  • можем посмотреть какие значения в каких переменных проставлены;

  • проставить значение в переменную и пустить выполнение программы дальше;

  • также можем настраивать так называемые conditional breakpoints — брейкпоинты, на которых остановка происходит только при выполнении определенного условия.

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

Вот как может выглядеть дебаг панель
Вот как может выглядеть дебаг панель

№4. Дебаг профилированием

Вообще профилирование довольно обширное понятие, но мы его будем обсуждать с точки зрения андроида. С помощью профилирования (один из вариантов systrace) мы можем посмотреть, сколько времени занимает выполнение интересующего нас метода, например, если мы занимаемся оптимизациями. Также мы можем снимать дампы памяти если занимаемся поиском и устранением утечек памяти. И многое многое другое.

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

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

Пример дампа памяти
Пример дампа памяти

№5. Дебаг с помощью Layout Inspector (актуален только для приложений с UI)

Если делаешь приложение с пользовательским UI, то часто будешь сталкиваться с багами, которые связаны именно с отрисовкой. В таких случаях очень помогает наличие инструмента, который помогает отлаживать UI. В случае андроида — это Layout Inspector. На моей практике он очень сильно помогал и в нем также много возможностей. Если тебе еще не приходилось с ним сталкиваться — максимально рекомендую изучить.

Отладка с помощью Layout Inspector
Отладка с помощью Layout Inspector

Вернёмся к примеру №1

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

hmmm
hmmm

Мы видим, что у компонента переопределен метод setOnClickListener. И даже если мы передаем в него null , он всё равно вешает клик лисенер. Вопрос «Зачем это было сделано изначально?» мы с тобой рассматривать сейчас не будем, я и сам был удивлен :)

Здорово, что нам удалось сэкономить много времени отладки просто путём просмотра исходного кода. Но это не всегда так работает, иногда придется и подебажить, да ещё и погрузиться на самое дно в недра исходников.

На эту тему от меня уже есть статья «Самый запутанный краш в моей жизни» (кейс, когда я столкнулся с очень странным багом и исследовал его).

Пример №2

Исходные условия всё те же, экранчики нашего приложения строятся из компонентов дизайн-системы как из кирпичиков. Открываем приложение и видим такое:

тестовый пользователь
тестовый пользователь

Иконка как-то сильно прижалась к верху. Здесь используется компонент, который мы называем AlertView. Так вот из интересного — в компонент AlertView никаких изменений не вносилось, а ошибка возникла. Давай разбираться, в чём же дело.

В первую очередь заглянем в модель AlertView и посмотрим как она настраивается.

data class AlertViewModel(
    val icon: IconElementModel,
    val texts: Texts = Texts.None,
    val buttons: Buttons = Buttons.None,
): Serializable

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

Давай заглянем внутрь IconElementModel. Важный дисклеймер в IconElementModel тоже никаких изменений не вносилось:

data class IconElementModel constructor(
    override val icon: Image? = null,
    @Transient  override val horizontalPaddingNew: HorizontalPadding,
    @Transient  override val verticalPadding: VerticalPadding
): Serializable

Видим, что паддинги, как вертикальные, так и горизонтальные, помечены как Transient. А модель AlertView как раз передается в шторку через Bundle и механизм сериализации и десериализации. Попробуем продебажить нашу гипотезу:

hmmm
hmmm

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

Но, есть один важный нюанс — это ведь как-то работало до этого. Почему оно сломалось?

Не буду томить, в итоге оказалось, что на это поведение повлияло повышение targetSdk. А повлияло потому, что при передаче нашей модельки, мы опирались на внутреннюю реализацию SDK, а именно на кейс, когда передача объекта происходила по ссылке, а не через реальную сериализацию и десериализацию. Подробнее про это в статье «Кюветы Android, Часть 1: SDK», на StackOverflow («Bundle.putParcelable») и в исходном коде («BaseBundle.java»).

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

Пример №3

Давай представим, что у нас с тобой есть RecyclerView. Мы задали ему адаптер, передаем данные и при этом видим пустоту на экране.

Void
Void







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

Logs
Logs

В логах все штатно, никаких ошибок нет. Давай прибегнем к LayoutManager, посмотрим, что он нам скажет:

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

onLayout
onLayout

Видим, что метод onLayout у RecyclerView вызывается, пока всё идет по плану. Если ещё не сталкивался с методами измерения и отрисовки view и viewgroup — эта статья может быть интересной. Провалимся внутрь dispatchLayout:

dispatchLayout
dispatchLayout

А вот мы и нашли проблему.

Мы просто забыли выставить LayoutManager для RecyclerView. Но при этом приложение не крашится, пользователь просто видит пустой экран.

Но ты мог заметить, что в логи сообщение все же выводится «No layout manager attached; skipping layout». И в скриншоте с логами, который был выше это сообщение также есть =) Это была проверка на внимательность и напоминание о том, что очень важно уметь смотреть и понимать логи, они могут сэкономить тебе кучу времени.
Итак давай проставим LayoutManager и попробуем:

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

Итоги

Даже на не сложных примерчиках, которые мы рассмотрели выше, становится понятно, что навык отладки — один из самых важных для андроид-разработчика. Мы каждый день сталкиваемся с проблемами в ходе разработки и не всегда решение проблемы можно найти на StackOverflow или в открытых источниках. Но опция, которую мы имеем всегда — это возможность отладить наше приложение, найти корень проблемы и решить её системно. Конечно, бывают случаи когда мы находим корень проблемы, но понимаем, что текущими ресурсами и средствами решить её не можем. В таком случае мы хотя бы имеем возможность осознанно выбрать обходные пути.

Самое важное в решении проблемы или фиксе бага — найти корень.

Комментарии (16)


  1. NickDoom
    26.06.2024 08:41
    +4

    Я даже больше скажу.

    Изначально, когда прога ещё не существует, она состоит из бага на 100%: в ней всё не так, потому что её ещё и вовсе нет.

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

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

    Но, увы, метрики все крайне приближённые и нелинейные, где-то бага стало меньше, а число выловленных метриками только выросло…


    1. Ab0cha Автор
      26.06.2024 08:41

      Интересные мысли !)


    1. mm3
      26.06.2024 08:41

      Обычно наоборот:

      Для непонятливых, краткая история. Проект Wine был основан в 1993 году. Он представлял собой проект размером 0 байт. И был идеален по архитектуре и составу. Потом в него начали добавлять баги. Проект разрастался, к проекту стали подключаться новые разработчики, которые добавляли ещё больше багов. И поэтому при каждом новом релизе принято спрашивать "Чо опять сломали?!".


    1. gres_84
      26.06.2024 08:41

      Не бывает безбаговых программ, бывают недотестированные.


  1. pomponchik
    26.06.2024 08:41

    А как насчет "дебага тестами"? Лично я при наличии возможности стараюсь прибегать именно к нему.

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


    1. Rsa97
      26.06.2024 08:41
      +2

      Когда тест падает — разбираться и фиксить.

      Вот внутри "разбираться и фиксить" и находится собственно дебаг. Тест показывает, что в коде есть ошибка, а дебаг - процесс поиска и устранения этой ошибки.


      1. pomponchik
        26.06.2024 08:41

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


      1. Gorthauer87
        26.06.2024 08:41

        Тест чем хорош, что это автоматизированный минимально воспроизводимый (в идеале) пример бага.

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


    1. Ab0cha Автор
      26.06.2024 08:41

      Валидная идея)


  1. AlexXYZ
    26.06.2024 08:41
    +1

    Для прояснения - "нельзя просто так взять и начать программировать/тестировать/отлаживать программу". К отладке надо готовиться заранее.

    Одна из распространённых проблем при логировании - очень неконкретные описания, например, как в вашем примере выше написано (в вашем же компоненте):

    Компонент не объясняет что именно ему не понравилось "Declares multiple JSON fields..." очень расплывчатое "объяснение". Особенно с учётом того, что не указаны какие вообще данные подверглись обработке и что именно в этих данных задублировалось (не указано имя поля, которое считается дубликатом. Будет очень неприятно выяснить, что условие сработало всё-таки ложно). А самое неприятное в таких логах - абсолютная оторванность от исходного кода, когда один и тот же текст ошибки неожиданно может использоваться в нескольких местах. Если в разных функциях, то ещё может выручить stacktrace, а вот если в одной функции то всё очень плохо - контекст ошибки почти потерян. Из своей практики - я пишу уникальные коды ошибок в каждом сообщении. Найти ошибку по уникальному коду быстрее, чем с помощью stacktrace. (но это чисто субъективный подход, дело не в скорости поиска, а в точности поиска места ошибки)


    1. Ab0cha Автор
      26.06.2024 08:41
      +1

      Согласен, корректное описание в логах - это очень важная история)


  1. andreyiq
    26.06.2024 08:41
    +1

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


    1. Ab0cha Автор
      26.06.2024 08:41

      Такой подход тоже вполне валиден)
      Главное, чтобы в этих логах не проскакивали sensitive данные на релизных сборках)


  1. Dominux
    26.06.2024 08:41

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


    1. Ab0cha Автор
      26.06.2024 08:41

      спасибо вам за фидбэк !)
      буду улучшаться


  1. Mexator
    26.06.2024 08:41

    Могу добавить, что если просмотр исходного кода не привел к результату, надо обязательно локализовывать баг: убирать все "лишние" сущности и смотреть, остаётся ли ошибка.

    Часто помогает, например, с RecyclerView: если странно себя ведёт, сначала убираешь ItemAnimator, потом меняешь LayoutManager на дефолтный (если использовался кастомный), затем "отключаешь" часть элементов (не передаёшь их в адаптер). Когда доходит до этого, уже всё должно проясниться.

    Без локализации бага можно долго тыкаться не в те места.

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

    P.S:

    Давай прибегнем к LayoutManager, посмотрим, что он нам скажет:

    Опечатка, должен быть Layout Inspector