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

Я делаю приложение Альфа Мобайл для физических лиц. Однажды с утра пораньше мне прилетел тикет, в котором была описана проблема с лагающим UI. Этот баг воспроизводился только на 21 api. Собственно как делали наши предки для начала я попытался воспроизвести этот баг. Запускаю билд и сразу вижу вот такое:

Но погоди ка...
Как тестер смог дойти до нужного экрана если на актуальной ветке приложение крашится сразу при входе ?

Я отправился в путешествие до тестировщика, и действительно у него приложение открывалось как нужно. И тут я вспомнил, что тестеры берут apk с binary, куда они загружаются нашим CI и самый важный момент в том, что там уже включен R8, а на локальных сборках R8 не включается.
Ну что ж, вот оно различие и проблема может быть в R8 ведь так ?

Кто такой этот R8 если ты не знаешь - https://developer.android.com/studio/build/shrink-code

Но как окажется в будущем, проблема была вовсе не в R8, R8 просто был ключом к решению данной загадки. И как правило разработчики сталкиваются с обратной ситуацией, когда из-за включенного R8 приложение крашится. Усугубляло проблему еще и то, что краш происходил только на 21 api, версии выше функционировали штатно.

Давай остановимся на минутку и ты подумаешь в чем же может быть дело ?

А теперь продолжим !

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

Давай присмотримся к скриншоту. Проблема материализуется при создании сущности TextViewModel(), а она соответственно обращается к TextViewStyle.H1 и на этом все, мы ловим NoClassDefFoundError. Еще раз прошу тебя обратить внимание на то что проблема возникает только на 21 api и только на сборке без R8.

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

Сразу после этого я получил такой результат:

"Ну хотя бы другая ошибка, уже лучше, чем ничего" - подумал я. Но не тут то было, стоило поскролить стектрейс и:

Проблема ClassNotFoundException осталась. И она и не могла уйти от такого, загвоздка лежала глубже чем я думал. Но на этот раз я прочитал лог внимательнее и понял, что проблема где-то в dex файлах. Еще один важный момент, проблема появилась спонтанно, в одной из версий нашего приложения, причем с момента ее появления уже прошло несколько релизов, но она не была обнаружена, так как возникает только на локальных сборках именно под 5 андроид. Ну что дружище пора погрузиться на дно, в святую святых - в исходники андроида. Причем нас интересует именно 21 версия api андроида. Я заглянул в файлик под названием runtime/dex_file.cc, а конкретно метод bool DexFile::OpenFromZip

Если не знаешь, что такое dex файл рекомендую сходить сюда, если совсем просто и коротко то в него упаковывается исходный код вашего приложения - https://source.android.com/docs/core/runtime/dex-format?hl=en

 while (i < 100) {
      std::string name = StringPrintf("classes%zu.dex", i);
      std::string fake_location = location + kMultiDexSeparator + name;
      std::unique_ptr<const DexFile> next_dex_file(Open(zip_archive, name.c_str(), fake_location,
                                                        error_msg, &error_code));
      if (next_dex_file.get() == nullptr) {
        if (error_code != ZipOpenErrorCode::kEntryNotFound) {
          LOG(WARNING) << error_msg;
        }
        break;
      } else {
        dex_files->push_back(next_dex_file.release());
      }
      i++;
    }

Что же я там увидел. Тут даже не нужно быть знатоком C или C++. Ограничение в 100 dex файлов. Если их будет больше то они просто отбросятся и мы как раз получим ту самую NoClassDefFoundError, просто потому что класс был в dex файле, который отбросили. И в тот момент я подумал - "Не может ведь быть, чтобы у нас на локальной сборке было больше 100 dex файлов, бред какой-то." Но реальность полна разочарований, как говорил Танос:

Не нужно быть Евклидом или Пуанкаре, чтобы понять количество dex файлов, которые представлены на скриншоте. Даже я понял, что наша апка точно переваливает за лимит в 100 файлов. Как ты понимаешь с включенным R8 dex файлов уже гораздо меньше чем 100. Мне стало интересно почему приложение не крашится на версиях api выше 21. И я заглянул в тот же runtime/dex_file.cc только уже для 6 андроида:

for (size_t i = 1; ; ++i) {
      std::string name = GetMultiDexClassesDexName(i);
      std::string fake_location = GetMultiDexLocation(i, location.c_str());
      std::unique_ptr<const DexFile> next_dex_file(Open(zip_archive, name.c_str(), fake_location,
                                                        error_msg, &error_code));
      if (next_dex_file.get() == nullptr) {
        if (error_code != ZipOpenErrorCode::kEntryNotFound) {
          LOG(WARNING) << error_msg;
        }
        break;
      } else {
        dex_files->push_back(std::move(next_dex_file));
      }
      if (i == kWarnOnManyDexFilesThreshold) {
        LOG(WARNING) << location << " has in excess of " << kWarnOnManyDexFilesThreshold
                     << " dex files. Please consider coalescing and shrinking the number to "
                        " avoid runtime overhead.";
      }
      if (i == std::numeric_limits<size_t>::max()) {
        LOG(ERROR) << "Overflow in number of dex files!";
        break;
      }
    }

Ограничения уже нет и будут обработаны все dex файлы. Но есть забавный комментарий:

// Technically we do not have a limitation with respect to the number of dex files that can be in a
// multidex APK. However, it's bad practice, as each dex file requires its own tables for symbols
// (types, classes, methods, ...) and dex caches. So warn the user that we open a zip with what
// seems an excessive number.
static constexpr size_t kWarnOnManyDexFilesThreshold = 100;

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

P.S.

Может возникнуть вопрос, "а как же мы решили проблему с крашем ?". Об этом я расскажу в следующей статье-расследовании.
Прикладываю ссылки на исходники, в которые я залезал:
https://android.googlesource.com/platform/art/+/android-6.0.0_r26/runtime/dex_file.cc
https://android.googlesource.com/platform/art/+/lollipop-release/runtime/dex_file.cc

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


  1. vadimk91
    18.12.2022 10:37
    +6

    Этоwhile (i < 100) чем-то напомнило "всем хватит 640кб памяти" :)

    У меня на телефоне с 8 версией андроида с какого-то момента сломалось обновление некоторых приложений. Причём если откатить Play Market до той версии, что была в начальном прошивке, все благополучно обновляется. Потом приезжает свежая версия маркета, и привет, хочешь обновить приложение - сначала удали её совсем и поставь заново. Надоело с эти бороться, похоже внутри версий андроида такой бардак...


  1. aml
    18.12.2022 11:16
    +1

    Первое, о чем подумал, когда увидел стектрейс, что превысилось число классов в dex. Почти угадал :)


  1. EvgenWithYou
    18.12.2022 18:04

    Интересная статья


  1. Pampam83
    18.12.2022 20:24

    У меня все также не было идей

    I've still had no idea.


  1. ReadOnlySadUser
    19.12.2022 00:12
    +6

    Последнее время слишком часто слышал слово "краш" в контексте "влюбленность", что не сразу понял о чём заголовок)


    1. Ab0cha Автор
      19.12.2022 10:00

      ахаххахахах))


  1. lnskkh
    19.12.2022 09:59
    +1

    По заголовку статьи подумал, что перепутал и попал на женский форум


  1. MrBillioaiRe
    19.12.2022 10:00

    У меня тоже проблемы на API 21 на флаттере. Я уже открыл issue, прошел год. Безрезультатно


    1. rafuck
      19.12.2022 12:56

      А можно подробнее? Хотя бы ссылку на issue