Привет. Меня зовут Кирилл Розов и вы если вы интересуетесь разработкой под Android, то скорее всего слышали о Telegram канале "Android Broadcast", с ежедневными новостями для Android разработчиков, и одноимённом YouTube канале. Этот пост является текстовой расшифровкой видео на канале

Система ресурсов в Android очень богата своими возможностями, которые позволяет нам описывать строки, хранить картинки и различные значения. Одной из очень удобных возможностей является эффективное определение ресурсов для различных конфигураций устройства (через квалификаторы): размера экрана, ориентации, локали, код оператора и множество другого. Есть в этом механизме узкое место для скорости сборки ваших проектов - R классы. Сегодня я расскажу вам откуда возникает такая проблема и как её решили в Google, разделив R классы.

Что такое R класс

Все ресурсы Android описываются в XML либо сохраняются файлами, например, drawable или raw ресурсы. Чтобы использовать ресурсы из кода, нам нужен какой способ ссылаться на них. Да, можно было бы хардкорно забивать строки в виде пути к ресурсу, как это сделано для ресурсов в JAR, но в Google сделали иной подход - генерация идентификаторов для каждого ресурса в коде. Для этого создаётся специальный R класс, который содержит вложенные классы по типам ресурсов: string, drawable, integer и др. После этого мы можем передавать идентификатор в специальные методы Resources или Context и получить необходимый ресурс.

Почему ресурсы из библиотек попадают в R класс приложения

Основной момент в R классе, то все подключенные Android Gradle модули или AAB файлы тоже могут содержать ресурсы и все их идентификаторы попадают в модуль, к которому они подключены. Это называется транзитивный R класс. Реализация этого аспекта была связана с удобством доступа ко всем ресурсам и отсутствием необходимости работы со множеством R файлов. Представьте как было бы организовывать доступ ко множеству R классов в Java коде? Пришлось бы использовать полные имена классов либо делать статические импорты для констант.

public class MainActivity extends AppCompatActivity {
  
  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState)
    Resources resources = getResources();
    resources.getString(R.string.app_name);
    resources.getAnimation(androidx.appcompat.R.anim.abc_fade_in);
  }
}

С Kotlin ситуация совсем другая, так как есть именованные импорты, что позволяют управлять легче, но Kotlin появился не сразу в Android.

import org.example.Message
import org.test.Message as TestMessage

Одна из причин появления префиксов у ресурсов в библиотеках - необходимость чтобы их имена были максимально уникальными, например в библиотеке Jetpack AppCompat для ресурсов используются префиксы abc

Нетранзитивные R классы

С одной стороны мы получили удобство, а с другой - проблему для скорости сборки проектов. Фактически при любом добавление ресурсу приходится генерировать R класс для вашего модуля, а затем смёржить его со R классами из зависимостей. Представьте какой размер R файла получается в app модуле? А что если попробовать разделить классы и управлять и генерировать R классы независимо друг от друга? И в Google это сделали. Назвали такую фичу - нетранзитивные R классы. Теперь к ресурсам каждой android зависимости создаётся свой независимый R класс, но это не будет распространяться на уже скомпилированные Android библиотеки.

Преимущества

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

Результаты оптимизации на реальном большом проекте
Результаты оптимизации на реальном большом проекте

Как перевести проект

Я думаю что вас уже заинтересовала миграция. Чтобы это сделать вам необходимо в gradle.properties вашего проекта выставить в true значение property android.nonTransitiveRClass

# gradle.properties & Android Gradle Plugin 7.X
android.nonTransitiveRClass=true

Также вам надо выполнить миграцию в коде, чтобы обращаться к ресурсам из зависимостей через их R класс. Для этого вам нужно либо использовать полные имена пакетов, либо использовать alias для импортов в Kotlin (надеюсь что вы уже с ним)

import dev.androidbroadcast.sample.R

R.layout.activity_main                // App Resources
R.layout.abc_action_bar_up_container  // AndroidX Appcompat
import dev.androidbroadcast.sample.R
import androidx.appcompat.R as AppCompatR

R.layout.activity_main
androidx.appcompat.R.layout.abc_action_bar_up_container // полное имя класса
AppCompatR.layout.abc_action_bar_up_container // с помощью type alias

Автоматическая миграция на нетранзитивные R классы в Android Studio

Проходится руками по всей кодовой базе сложно и никто не станет этого. Чтобы упростить это в Android Studio 2020.3 Arctic Fox добавили возможность автоматической миграции. Одна кнопка, предпросмотр изменений и нажатие "Refactor" позволит вам получить преимущества, о которых я рассказал.


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

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


  1. quaer
    16.09.2021 02:01

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


    1. anegin
      16.09.2021 09:40

      А вы где-то напрямую используете "номера" (числа) ресурсов? или все же используете сгенеренные константы из класса R?


      1. quaer
        16.09.2021 14:10

        Из класса R. Что происходит с этими константами в данном случае? Можно в одном switch использовать константы из таких классов?


        1. anegin
          16.09.2021 14:51
          +1

          В этом плане ничего не меняется - R классы содержат всё те же final int константы и генерятся как и раньше, просто отдельно для каждого package.


          1. kirich1409 Автор
            16.09.2021 14:52

            Всё верно. Правила не меняются, только R классы теперь раздельны


          1. quaer
            16.09.2021 16:15
            +2

            Уникальны ли они при этом? Не может быть так, что константа из одного package совпадет с константой другого? Package обязательно собирать внутри одного проекта? Можно собрать поотдельности?


  1. Agent03
    16.09.2021 15:13

    Ничего непонятно, но безумно интересно

    ????


  1. Firsto
    23.09.2021 17:04

    Затестил на небольшом проекте, заменило пару строк: com.google.android.material.R.style.Theme_MaterialComponents_Light_Dialog и androidx.appcompat.R.attr.colorPrimary, скорость сборки осталась прежней. ¯\_(ツ)_/¯

    Надо на многомодульных проектах потестить.


    1. kirich1409 Автор
      23.09.2021 18:29

      Да, профит именно там есть только


      1. kirich1409 Автор
        23.09.2021 18:29

        На многомодульных проектах имею в виду