Всем привет, меня зовут Саша и я Android разработчик в Совкомбанк. Занимаюсь здесь проектом Совкомбанк Страхование, а именно приложение по работе со всем, что связано с дополнительной страховкой: записи к врачу, просмотр полисов и оплата визитов сверх ДМС.

На днях поступила моя любимая нетривиальная задача: перевести наше Android приложение на Gradle 8.1.1.

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

Это помогло нам столкнуться с основными проблемами и найти им решение.

Зачем

Как и у всех разработчиков, у меня в первую очередь возник вопрос, для чего нам это

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

  • В первую очередь запрос был от бизнеса, так как это было пожелания от EDNA

  • Вышла новая версия Android Studio, которая обязует переходить на Gradle 8.1.1

  • Gradle обещает значительное ускорение инкрементальной сборки до 2х раз за счёт нового кэширования и особого анализа связей между классами

  • Поддержка Java 17

Как всё должно было пройти

Однажды я с дуру повысил targetSdk, не смотря на документацию и увеличил количество крашей в приложении Skyeng с 3% до 50%, с тех пор я внимательно изучаю документацию при переходе на любые обновления.

При изменении версии, Android Studio предлагает вполне подробную инструкцию о переходе и о том какие build options поменялись (ссылка).

К тому же, если build option не deprecated, IDE автоматически добавляет его с предыдущим значением, тем самым защищая вас от неожиданностей.

К примеру у нас появились вот такие строчки в gradle.properties:

android.defaults.buildfeatures.buildconfig=true //генерация BuildConfig
android.nonFinalResIds=false //все поля R классов были финальными, должно помочь при многомодульной архитектуре

Также с 8 Gradle есть требование размещать имя пакетов не в манифесте приложения, а в build.gradle модуля.

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

android {
    namespace = "ru.sovcombank.common.core"
}

То есть в идеальном мире, мне оставалось только поменять несколько цифр, проставив новую версию градла, и Java:

//build.gradle ваших модулей
compileOptions {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString()

//gradle/libs.versions.toml
gradle = "8.1.4"

//gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://nexus.sovcombank.group/repository/gradle/gradle/gradle-8.1.1-bin.zip

и наслаждаться

Но что-то пошло не так

Наверное всё это могло сработать на проекте, который не пользуется сторонними библиотеками или не работает с минификацией.

Но если ваш проект использует какие-то из популярных библиотек, таких как Room, Retrofit, Gson - тогда у вас и возникнут основные трудности.

Одно из главных нововведений этой версии, это дефолтное включение R8 fullMode, его можно отключить добавив соответственное поле в gradle.properties

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

Далее расскажу о том что конкретно у меня возникало и как я это решил.

1) Sealed classes are not supported as program classes

Если вы ещё не перешли на студию с ёжиком, тогда при включенной минификации, окажется что студия ещё не умеет работать с kotlin классами: подробности.
Решается это просто: достаточно добавить в ваш settings.gradle.kts внутри pluginManagement следующий код:

//  добавлено до перехода на версию студии Hedgehog 
    buildscript {
        repositories {
            mavenCentral()
            maven {
                url = uri("https://storage.googleapis.com/r8-releases/raw")
            }
        }
        dependencies {
            classpath("com.android.tools:r8:8.2.24")
        }
    }

2) Unresolved reference: ext

Также стоит посмотреть ваши build.gradle файлы на наличие теперь нераспознаваемых полей, к примеру, в нашем случае, необходимо было заменить задавание номера билда

//было
ext {
    set("buildNumber", System.getenv("BUILD_COUNTER") ?: 0)
}

//стало
ext.set("buildNumber", System.getenv("BUILD_COUNTER") ?: 0)

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

3) java.lang.StringConcatFactory

Когда начинаешь билдить проект, студия будет ругаться на разные классы, которые её беспокоят. Но там всё просто, для каждого модуля создаётся файл missing_rules.txt в котором пишется какие именно классы нужно внести в proguard-rules/consumer-rules.

К примеру в моём случае очень сильно потребовалась строчка:
-dontwarn java.lang.invoke.StringConcatFactory
(подробнее)

Важная информация: в отличие от -keep и подобных методов размещённых в app/proguard-rules.pro правила -dontwarn не распространяются на все сабмодули, и надо расписывать их в proguard-rules/consumer-rules каждого модуля.

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

4) В missing_rules.txt также есть R классы

В нашем приложении, есть несколько core модулей, хранящих ресурсы, такие как строчки или картинки.

Поэтому фиче-модули, использующие такие core модули начинали ругаться на ссылки R-классов.

На сколько я понял в изучении этого вопроса, android.nonFinalResIds=false должен был решить эту проблему, но в моём случае этот флаг совершенно не имел эффекта.

Чтобы не вычленять все такие core-классы, пришлось добавить небольшое улучшение для proguard, его также пришлось добавить во все модули, использующие ресурсы других модулей:

-dontwarn **.R$*

Такое правило игнорирует любые возмущения на любые R классы любых модулей, также как и с предыдущей проблемой, это необходимо внести во все proguard-rules/consumer-rules.

5) Register an InstanceCreator or a TypeAdapter for this type. Class name

В документации есть решение этой проблемы, достаточно добавить в ваш proguard-rules

-keepclassmembers,allowobfuscation class * {
 @com.google.gson.annotations.SerializedName <fields>;
}

Однако, почему-то это не работает, мне помогло изменённое решение, взятое тут

-keep class * {
 @com.google.gson.annotations.SerializedName <fields>;
}

6) java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to

В нашем приложении, иногда есть не совсем каноничное, но удобное использование иерархии объектов для хранения в Room, когда есть некоторая вложенность объектов

class OneRoomEntityClass(
  val id: Int,
  val text: String,
  val complicatedObject : SecondData
)

data class SecondData(
  val id: Int,
  val something: String,
  val listOfComplicatedObjects : List<ThirdData>
)

data class ThirdData(
  val smth1 : String
  val smth2 : String
)

То есть основной объект хранящийся в базе данных(OneRoomEntityClass), содержит в себе другой объект(SecondData), который в свою очередь содержит лист третьих объектов(ThirdData).

Дело в том что Room умеет обработать автоматом правильное сохранение объекта SecondData, а вот объекты вложенные в него (ThirdData), уже будут сохранены так, как вы решите это сделать, в нашем случае было достаточно простое решение.

@TypeConverter
fun secondEntityToJson(second: Second): String? {
    return gson.toJson(second)
}

@TypeConverter
fun jsonToPolicyAdditionalInfoEntity(value: String): Second {
    return gson.fromJson(value, Second::class.java)
}

То есть объект ThirdData уже сохраняется по правилам Gson, и при восстановлении Gson кастит List<ThirdData> в List<LinkedTreeMap>

В документации есть решение этой проблемы, нужно просто добавить в proguard-rules

-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken

Но я также не получил никакого эффекта от этих строк, и ловил ошибку java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to ThirdData.

Я нашёл несколько решений этого.

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

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

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

-keepclassmembernames class <имя_класса> {
    <fields>;
}


//или вариант попроще, через добавление всех data классов, сохраняющихся в базы данных:
-keepclassmembernames class ru.sovcombank.**.database_model_package.** {
    public ** component1();
    <fields>;
}

7) Что-то странное в сторонних библиотеках

Иногда бывает что ты вроде ничего не делал, а приходится чинить сломанное. И у меня получилось так, что внутренняя библиотека, которая использует стороннюю библиотеку, которая начала ломаться при обфускации. Я не смог разобраться подробно, какая именно часть там всё ломает, поэтому всю стороннюю библиотеку внёс в proguard, тем более что она у нас используется только на QA сборке, а значит на прод не повлияет.

К примеру если вы используете библиотеку ch.qos.logback, это может выглядеть вот так:

-keep class ch.qos.logback.** { *; }
-keep interface ch.qos.logback.** { *; }
-keep enum ch.qos.logback.** { *; }

У меня всё, но кое-что ещё

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

# для котлин suspend функций, которые используют Дженерики требуется Signature (то, что выше)
# но для того чтобы работало для открытых классов библиотек, требуется добавить
-keep class kotlin.coroutines.Continuation

# для включения R8 если используются дженерики в возвращаемых объектах ретрофита, можно будет удалить после перехода на версию 2.9.1
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>

# тоже самое для классов библиотек, которые открыты для других модулей:
-keep,allowobfuscation,allowshrinking class retrofit2.Response

Буду благодарен, любым дополнениям, задача у меня ещё не закрыта, может успею что-то пофиксить.

Ссылки:

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


  1. Rusrst
    09.12.2023 18:44
    -1

    Да, переезд на новый градл был весёлый, и java 17 и r8 fullmode.

    Библиотека которую вы используете (которая использует стороннюю) isminifyEnabled=true?