Привет, Хабр! Настало время, когда можно сказать, что «new build system» Gradle является стандартом отрасли Android-разработки. Инструмент сделан настолько просто и удобно, что большинство разработчиков не испытает трудностей, даже не зная, как он устроен, и какие дополнительные возможности в нём есть — возникающие проблемы легко решаются с помощью 5 минут на StackOverflow, путем копирования «магического кода» в конфигурационные файлы. Возможно, в том числе из-за этого не все разработчики изучают Gradle детально и не знают о многих его полезных возможностях, которые существенно облегчают жизнь.



Речь пойдет о:
  1. Увеличении быстродействия
  2. Расширении BuildConfig
  3. Использовании переменных
  4. Выключении Crashlytics
  5. Уменьшении количества конфигураций ресурсов


1. Увеличиваем быстродействие


Время сборки напрямую влияет на скорость разработки. Тесты показывают, что каждая из версий, начиная с Gradle 2.0, становилась медленнее предыдущей. Однако затем разработчики исправились и хорошенько поработали над быстродействием в Gradle 2.4.

1. Поэтому первым делом следует убедиться, что вы используете актуальную версию Gradle 2.4+

	sm:~ sm$ gradle -v
	Gradle 2.4


2. Затем, удостоверившись, что вы пытаетесь ускорить свою рабочую машину, а не сервер CI, включить Gradle демон — это даст значительный прирост в скорости сборки.
Строки конфигурации стоит добавлять в файл ./%project%/gradle.properties, если вы хотите распространить конфигурацию на все проекты, то необходимо конфигурировать файл, лежащий в домашней папке вашего пользователя
~/.gradle/gradle.properties

	org.gradle.daemon=true # включаем демон


Почему на CI-сервере не стоит включать Gradle Daemon
Gradle демон позволяет делать сборки более быстро, но при Gradle переиспользует runtime предыдущей сборки, по этой причине крайне важные требования для CI не выполняются. А именно стабильность и предсказуемость, чистота runtime и полная изолируемость от предыдущих сборок.


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

	org.gradle.parallel=true # включаем режим параллельного выполнения


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

	org.gradle.configureondemand=true


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


2. Расширяем BuildConfig


Как известно, файл конфигурации сборки (build.gradle) предоставляет возможность определить Product Flavors и Build Types, что даёт нам массу вариантов для разделения сборок по назначению. Например, “Сборка с тестовым сервером”, “Сборка с боевым сервером”, ”Сборка с логированием” и другие. Использование их для расширения BuildConfig (который генерируется каждый раз при сборке) даёт нам потрясающую гибкость. Например, удобное переключение между back-end-сервером, с которым работает наше приложение; включение/выключение определённого функционала – например, логи.

В build.gradle:

android {
    ...
    buildTypes {
        debug {
            buildConfigField "String", "SERVER_PREFIX", "\"test.\""
        }
        release {
            buildConfigField "String", "SERVER_PREFIX", "\"\""
        }
    }    
}


В java коде:

// …
public final class NetworkConstants {
    // …

    public static final String SERVER_ADDRESS = "http://" + BuildConfig.SERVER_PREFIX + "server.com/";

    // …
}


Разница между Product Flavors и Build Types
Product Flavor — это механизм, который позволяет нам определять различные варианты сборки приложения. У одного проекта могут быть различные варианты сборки(flavors), при выборе варианта(flavor) будет изменяться генерируемое приложение.
Build Type — конфигурация того, как приложение будет упаковано. У каждого приложения по умолчанию есть два Build Type – debug и release. Можно сделать и другие. Идеологически Build Type не предназначен для изменения приложения, только упаковки. Собственно это и является основным различием, которое выливается в различные наборы параметров, которые предоставляются для настройки Product Flavors и Build Types.


3. Используем переменные


Время не стоит на месте, а значит инструменты, библиотеки и Android имеют свойство обновляться. И если приложение развивается, то приходится открывать build.gradle и менять как минимум compileSdkVersion, buildToolsVersion, версии Android Support Library и Google Play Services. А если у нас в проекте используется много модулей или различные части библиотеки Google Play Services, это ведет к большому количеству мест изменений, и можно легко потерять время из-за опечатки в каком-то из файлов. Кроме того, возможно использование различных библиотек и инструментов в разных проектах, что плохо и может стать причиной проблем. Избежать подобной ситуации помогут gradle-переменные.

В самый верхний build.gradle добавляем
// …
ext.compileSdkProjectVersion= 23
ext.buildToolsProjectVersion= '23.0.1'
ext.supportLibraryVersion = '23.1.0'
ext.googlePlayVersion = '8.3.01’


В остальных ./%module%/build.gradle их можно будет использовать, выглядеть это будет примерно так:
android {
    compileSdkVersion compileSdkProjectVersion
    buildToolsVersion buildToolsProjectVersion
    //…
}

dependencies {
    compile "com.android.support:appcompat-v7:$supportLibraryVersion"
    compile "com.google.android.gms:play-services-base:$googlePlayVersion"
    compile "com.google.android.gms:play-services-maps:$googlePlayVersion"
    compile "com.google.android.gms:play-services-location:$googlePlayVersion"
}



4. Выключаем Crashlytics


В большинстве случаев собирать аварийные завершения необходимо только в Release-сборках, которые мы выпускаем для пользователей/тестирования. Debug сборки разработчик использует для себя, и аварийные завершения будут видны ему в лог-файле, значит, дабы не засорять список реальных аварийных завершений у пользователей, необходимо выключить Crashlytics для Debug-сборок.
Задачу можно выполнить банальной проверкой типа сборки:

// App.java
// …
public final class App extends Application {
// ...
    @Override
    public void onCreate() {
        // …
        if ( !BuildConfig.DEBUG ) {
            Fabric.with(this, new Crashlytics());
        }
        // …
    }
// …
}


Но это не самое лучшее решение, т.к. плагин Fabric Gradle всё равно будет тратить время на генерацию и встраивание в ресурсы приложения уникального id сборки, чтобы Crashlytics back-end затем понял, какая сборка прислала данные. Поэтому применим более удобное решение, которое позволит ускорить время сборки debug-версии приложения.

В build.gradle:
android {
    //…

    buildTypes {
        debug {
          ext.enableCrashlytics = false
          // …
        }
    }

    // …
}


После этого debug-сборки не будут получать id, и процесс сборки ускорится, но следует учитывать, что если разработчик попытается инициализировать в такой сборке Crashlytics, то приложение упадёт с выводом:

com.crashlytics.android.core.CrashlyticsMissingDependencyException:
This app relies on Crashlytics. Please sign up for access at https://fabric.io/sign_up`


Т.е. обязательно оставьте проверку на тип сборки и используйте Crashlytics только для Release сборок или воспользуйтесь решением, приведённым в документации к Crashlytics на сайте Fabric:

// App.java
// …
public final class App extends Application {
    // ...

    @Override
    public void onCreate() {
        // …
        //Создаём Crashlytics,выключенный для debug сборок
        Crashlytics crashlyticsKit = new Crashlytics.Builder()
            .core(new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build())
            .build()

        // Инициализируем Fabric с выключенным crashlytics.
        Fabric.with(this, crashlyticsKit);
        // …
    }

    // …
}



5. Уменьшаем количество конфигураций ресурсов


В разрабатываемых нами приложениях мы часто используем сторонние библиотеки, например, Android Support Library, Google Play Services и другие. Многие из библиотек поставляются с различными внутренними ресурсами, которые в наших приложениях абсолютно не нужны. Например, Google Play Services поставляется с переводом на языки, которые ваше приложение не поддерживает. Вероятно, вы также не захотите поддерживать mdpi или tvdpi-разрешение в своём приложении.

Благодаря Android Gradle Plugin мы можем установить языки и разрешения, которые используются в приложении, остальные будут исключены, что позволит уменьшить вес приложения.

// build.gradle 

android {
    defaultConfig {
        resConfigs "en", "ru"
        resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
    }
}


При необходимости можно подойти более радикально и начать использовать multi-APK, тем более когда появился новый удобный механизм Splits.

Вместо заключения


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

Проголосовало 202 человека. Воздержалось 43 человека.

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

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


  1. archinamon
    19.11.2015 18:06

    Выключаем Crashlytics

    В предыдущем проекте пришёл к выводу, что получается очень удобно если просто вынести все метрики и аналитики в отдельный флавор. Таким образом, в дев-версию сборки, где не нужен крашлитикс и аналитика, соответствующие зависимости даже не попадут в classpath.
    Для этого:
    dependencies {
        productionCompile("com.crashlytics.sdk.android:crashlytics:$fabricVersion") { transitive = true; }
    }
    


    1. scottKey
      19.11.2015 21:35

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


      1. archinamon
        20.11.2015 18:45

        Не обязательно. Можно сделать через flavor dimensions. Например, есть дименшен device: phone, tablet, wear; и есть дименшен environment: production, develop, marketing. На выходе получаем device.count * environment.count * buildType.count вариантов (18 вариантов сборки).


        1. scottKey
          20.11.2015 21:17

          Да, всё верно. И насколько я понимаю вы используете как phoneProductionDebug, так и phoneProductionRelease варианты сборки?


          1. archinamon
            20.11.2015 21:55

            Оба варианта используются, в итоге. Релиз берёт CI-машина, а дебаг обычно собирается только на локальных машинах, если нужен.


            1. scottKey
              20.11.2015 22:21

              Тогда у вас хорошее решение, спасибо что поделились!


  1. agent10
    19.11.2015 19:28

    А указание resConfigs влияет только на последний шаг сборки, грубо говоря, только на размер apk? Т.е. разнообразный мёржинг ресурсов остаётся и на скорость сборки это не влияет?


    1. scottKey
      19.11.2015 20:13

      Да, всё верно. Ресурсы подготовит, смержит, а уже во время запаковки – выкинет.


      1. agent10
        19.11.2015 20:16

        Напрашивается вопрос — а зачем?) Я, конечно, детально со всеми этапами не знаком, но на лицо куча лишних действий)


        1. scottKey
          19.11.2015 20:29

          Этот аспект отражён в статье, только ради уменьшения размера apk.


          1. agent10
            19.11.2015 20:45

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

            Например, у меня 10 языков, в каждом по 2 тысяче строк.
            Но у меня не для всех flavors нужны все языки, где-то надо всего 2 языка.
            Если бы ресурсы игнорировались на начальном этапе это могло бы немного, но повысить скорость сборки.


            1. scottKey
              19.11.2015 21:29

              Было бы удобно, но пока, насколько мне известно, так сделать нельзя.


            1. archinamon
              20.11.2015 18:53

              Кстати, он не должен мёржить ресурсы из других флаворов. Только из тех, вариант сборки которых выбран.

              Но если говорить о произвольной выборке в пределах выбранного варианта, то Вы забываете про lint проверки. Некоторые проблемы с ресурсами выяляются только на этапе сборки. Это и отсутствие переводов строк, и коллизии/баги с 9патч. Представьте себе обработку 10-20 языков, по 2-5 тысяч записей в каждом, в фоне во время основной работы с проектом. Могу ошибаться, но по-моему логичнее незначительно увеличить время сборки, но убедиться в корректности всех ресурсов.


  1. stepanp
    19.11.2015 19:43

    Неужели в 2.4 он стал сносно по скорости работать?


    1. scottKey
      19.11.2015 20:18

      Вопрос сносности это всё же субъективный вопрос, есть объективные тесты с цифрами – разница очень существенная. Также могу сказать что разработчики Gradle обещали ускорить и следующие версии, т.е. 2.8/2.9 должны быть еще быстрее, но тестов я не проводил.


  1. forceLain
    20.11.2015 06:04

    Мне кажется, самая главная полезность — это осознание, что gradle — это не только система сборки, но и язык программирования (groovy), который в процессе билда дает возможность создавать и рушить вселенные, ну или еще что-нибудь полезное. У нас, к примеру, генерируется файл с историей версий (релизов) на основе гит тэгов.

    Кусочек скрипта
    def String[] tags = formatTagNames(getTagList())
    def AppHistory appHistory = getAppHistory(tags)
    def String jsonHistory = new GsonBuilder().setPrettyPrinting().create().toJson(appHistory)
    new File("app_history.json").withWriter{ it << jsonHistory }
    


    1. scottKey
      20.11.2015 21:24

      Спасибо за полезный пример!
      Стоит уточнить что Gradle использует Gradle DSL. Gradle DSL в свою очередь основан на Groovy, но расширяет его для более удобной работы с билд скриптами.
      Если говорить о самом полезном в Gradle, думаю, это декларативность. Декларативность позволяет создавать и рушить вселенные очень удобно и легко, не задумываясь о очень многих вещах.