안드로이드 그래들 빌드 속도 올리기

이 글은 2017년 5월 구글I/O의 Speeding Up Your Android Gradle Builds (Google I/O ‘17 세션 중 일부를 정리한 내용입니다.

IMAGE ALT TEXT HERE

발표 영상, 슬라이드는 맨 아래 참고 링크에서 확인 할 수 있습니다. 발표자는 안드로이드 스튜디오 팀의 James Lau 입니다.

영상을 보면, 산타트래커란 샘플 프로젝트에 하나씩 최적화를 적용하면서 어느정도 최적화가 되었는지 보여주는 방식으로 진행됩니다. 빌드속도는 당연히 프로젝트의 구성과 환경에 영향을 받습니다. 발표자가 테스트에 사용한 산타트래커는 아래와 같이 구성되었습니다.

  • 안드로이드 웨어를 포함한 9개의 모듈
  • 500개의 자바 파일
  • 1700개의 xml
  • 3500개의 PNG
  • 멀티덱스
  • 60MB 정도의 apk를 크기
  • 어노테이션프로세서는 사용치 않음

벤치마크는 아래 세 가지 경우로 보여줍니다. incremental build는 구글 개발자 페이지에 증분 빌드로 번역하고 있어서 그대로 사용했습니다. 참고 링크

  • 전체 빌드(Full build)
    • Clean, assemble debug
  • 증분 빌드(Incremental build - Java change)
    • 한 메소드 안에 한줄의 자바코드 수정
  • 증분 빌드(Incremental build - resource changing)
    • 스트링 리소스 한 개를 추가하거나 제거

빠른 빌드를 위한 10가지 팁

세션에서 소개된 팁은 총 10개로 아래 목차와 같습니다.

  1. 최신 안드로이드 그래들 플러그인을 쓸 것
    • Use lastest Android Gradle Plugin
  2. 레거시 멀티덱스를 피할 것
    • Avoid legacy multidex
  3. 멀티 APK를 사용하지 말 것
    • Disable multi-APK
  4. 최소한의 리소스만 포함 시킬 것
    • Include minimal resources
  5. PNG 최적화 설정을 사용하지 말 것
    • Disable PNG crunching
  6. 인스턴트 런을 사용할 것
    • User Instant Run
  7. 의도치 않은 수정은 피할 것
    • Avoid inadvertent changes
  8. 다이나믹 버전은 사용하지 말 것
    • Don’t use dynamic version
  9. 메모리를 확인할 것
    • Watch the memory
  10. 그래들 캐시를 사용할 것
    • Use Gradle Cache

1. 최신 안드로이드 그래들 플러그인을 쓸 것

안드로이드 스튜디오 팀에서는 빌드 속도와 버그수정을 위해 그래들 플러그인을 계속 업데이트 하고 있습니다. 그래들 3.0부터는 구글 메이븐 저장소를 통해 그래들이 배포됩니다. 따라서 메이븐 구글 저장소를 추가해주고, dependency에 gradle 3.0.0을 추가해줍니다. 발표 영상에는 alpha1인데 오늘(2017.09.20일) 확인해본 바로는 beta4까지 나왔네요.

1
2
3
4
5
6
7
8
9
buildscript {
repository {
jcenter()
maven { url 'https://maven.google.com' }
}
dependency {
classpath 'com.android.tools.build:gradle:3.0.0-alpha1'
}
}

결과를 보니 단순히 그래들 버전을 3.x대로 올리는 것만으로 속도가 아래와 같이 엄청나게 향상됩니다. 아래 결과는 발표자 영상에 있는걸 그대로 옮긴거구요, 대강 어느정도 향상되는지 참고 하시면 될 것 같습니다.

빌드 타입 | 속도 차이(초) | 속도 차이(백분율)
— | — |
전체 빌드 | -15s | -25%
증분 빌드(자바) | -10s | -38%
증분 빌드(리소스) | -2.5s | -16%

2. 레거시 멀티덱스를 피할 것

멀티덱스 사용 시 minSdkVersion < 21인 경우 레거시 멀티덱스가 적용됩니다. 그리고 이 레거시 멀티덱스는 빌드 속도를 심각하게 다운 시킵니다. 안드로이드 스튜디오 2.3+에서부터는 런 버튼을 클릭해서 앱을 실행하면, 연결된 디바이스나 에물레이터의 API 버전을 감지해서 가능하면 네이티브 멀티덱스를 적용해준다고 합니다. 참고로 커맨드라인으로 빌드하는 경우엔 이렇게 자동으로 버전을 감지해서 최적화 해주는 기능은 동작하지 않습니다.

만약 프로덕트가 api 레벨 21 미만의 단말도 지원해야 한다면 아래와 같이 빌드 변형 구성을 이용해 개발 용 빌드의 minSdkVersion을 21로 설정할 수 있습니다.

1
2
3
4
5
6
7
productFlavors {
development {
minSdkVersion 21
...
}

}

결과는 아래와 같습니다. 이것도 성능 향상이 꽤 크네요.

빌드 타입 | 속도 차이(초) | 속도 차이(백분율)
— | — |
전체 빌드 | -5.5s | -12%
증분 빌드(자바) | -8s | -53%
증분 빌드(리소스) | same | same

3. 멀티 APK를 사용하지 말 것

멀티 APK는 ABI나 density에 따라 apk를 다르게 빌드하는 기능입니다. 한 프로젝트에 대해 여러 APK가 생성되지만, 각 빌드의 결과물로 나오는 APK 크기가 작아지는 장점이 있습니다. 하지만 개발환경에선 필요없는 기능이죠. 따라서 개발 용 빌드인 경우에는 멀티 APK 기능을 아래와 같이 꺼줍니다.

1
2
3
4
5
6
android {
if (project.hasProperty('devBuild')) {
splits.abi.enable = false
splits.density.enable = false
}
}

위의 그래들 빌드 스크립트를 보면 ‘devBuild’란 속성을 확인하는데 저 속성은 커맨드라인 빌드 시 아래와 같이 넘겨줍니다.

1
./gradlew santa-tracker:assembleDevelopmentDebug -PdevBuild

이걸 안드로이드 스튜디오에 적용한다면 preference -> Build, Execution, Deployment -> Compiler -> Command-line Option 에 -PdevBuild을 적어주면 됩니다.

결과는 아래와 같습니다. 리소스에 대한 증분 빌드 시에 많은 속도 향상이 있네요.

빌드 타입 | 속도 차이(초) | 속도 차이(백분율)
— | — |
전체 빌드 | -4.8s | -12%
증분 빌드(자바) | -0.5s | -6%
증분 빌드(리소스) | -3s | -26%

4. 최소한의 리소스만 포함시킬 것

앱이 다국어를 지원한다면 여러 스트링 리소스를 포함합니다. 다국어를 지원하지 않더라도 대부분의 앱은 하나 이상의 스크린 density를 지원하는 경우가 많습니다. 안드로이드의 빌드 시 기본 설정은 모든 버전의 리소스를 빌드에 포함시키도록 되어있습니다. 하지만 아래와 같이 특정 버전의 리소스들만 포함시키도록 할 수 있습니다. 아래는 개발용 빌드에 특정 리소스만 포함 시키도록 하는 그래들 설정 예시입니다.

1
2
3
4
5
6
7
productFlavor {
developement {
minSdkVersion 21
resConfigs ("en", "xxhdpi")
...
}
}

적용 결과입니다.

빌드 타입 | 속도 차이(초) | 속도 차이(백분율)
— | — |
전체 빌드 | -6s | -17%
증분 빌드(자바) | -1.5s | -24%
증분 빌드(리소스) | -2s | -21%

5. 개발 빌드에는 PNG 최적화를 끄자

안드로이드 빌드 툴은 PNG 크기 최적화를 기본으로 수행합니다. 최적화라면 PNG 파일들을 압축하는 거겠죠. 당연히 PNG 파일이 많으면 빌드 시 많은 시스템 자원을 사용하게 됩니다. 릴리즈 시에는 APK 크기를 줄여주므로 중요한 기능이지만 개발 시에는 별 필요 없는 기능입니다. 역시나 아래와 같이 그래들 빌드 스크립트를 이용해 설정을 살포시 꺼줍니다.

1
2
3
4
5
6
7
8
productFlavor {
developement {
minSdkVersion 21
resConfigs ("en", "xxhdpi")
aaptOptions.cruncherEnabled = false
...
}
}

아니면 아예 webP 포맷을 사용하는 것도 좋은 방법입니다. webP는 PNG 파일보다 최대 25% 작은 크기를 갖는 포맷입니다. 안드로이드 스튜디오 2.3+ 부터는 IDE에서 PNG를 webP로 변환하는 기능을 지원합니다. 불투명 이미지는 안드로이드 API 15+, 투명도를 갖는 이미지는 API 18+부터 지원합니다. 또한 애초에 최적화된 PNG를 사용하면 최적화가 수행되어도 실제 APK의 크기는 별 차이가 없습니다. 그냥 시간만 잡아 먹는 거죠.

아래는 위의 PNG 최적화 기능을 끈 결과입니다.

빌드 타입 | 속도 차이(초) | 속도 차이(백분율)
— | — |
전체 빌드 | -9s | -33%
증분 빌드(자바) | same | same
증분 빌드(리소스) | same | same

6. 인스턴트 런을 사용할 것

인스턴트 런은 안드로이드 스튜디오 2.0 때 런칭된 기능입니다. 발표자 말로는 런칭 이후 인스턴트 런의 신뢰성(reliability)를 위해 많은 노력을 기울였다고 합니다. 바꿔말하면 그동안에 인스턴트 런은 문제가 많았다는 뜻이겠죠. 저 역시도 인스턴트 런을 사용하지 않는데 이유는 수정한 코드가 실제로 빌드에 적용이 되지 않아 삽질을 한 기억이 많기 때문입니다. 수정한 코드가 빌드에 적용이 안된다면 빌드가 아무리 빨리 된들 무슨 소용이 있을까요.

하지만 안드로이드 스튜디오 3.0에 적용된 인스턴트 런은 2.0과 매우 다르다 합니다. 한번 믿어보는것도 좋을것 같습니다. 참고로 플랫폼의 한계에 따라 적용했던 많은 핵들을 신뢰성을 위해 과감히 제거하고 대신 안드로이드 스튜디오 3.0부터의 인스턴트 런은 API 레벨 21 이상에서만 동작한다고 합니다. (minSdk가 21 이상이여야 한다는게 아니라 연결된 단말의 버전이 21 이상이면 된다는 얘기입니다!)

안드로이스 스튜디오 3.0 이상에서는 인스턴트 런과 일반 빌드를 쉽게 구분하기 위해 런 버튼을 Run과 Apply Changes 두 개로 분리 했습니다.

Run 버튼을 클릭하면 콜드 스왑을 시도하고 앱은 재실행 됩니다. 반면 Apply Changes 버튼을 클릭하면 hot or warm swap을 시도합니다. Cold, hot, warm swap에 대한 내용은 이 링크에서 자세히 알 수 있습니다.

인스턴트 런을 실행하면 연결된 디바이스의 API, 스크린 density등을 분석해서 자동으로 필요한 최적화를 수행해 줍니다. 즉 앞서 얘기했던 특정 버전의 리소스만 포함하는 빌드를 생성한다거나 멀티 APK등에 대한 최적화 들을 알아서 해준다는 것 같습니다.

아래는 인스턴트 런을 적용한 결과입니다.

빌드 타입 | 속도 차이(초) | 속도 차이(백분율)
— | — |
전체 빌드 | +7s | +37%
증분 빌드(자바) | -3s | -54%
증분 빌드(리소스) | -3s | -42%

특이한게 인스턴트 런을 적용 했을 때, 풀빌드의 경우 오히려 빌드 시간이 증가했습니다. 이는 인스턴트 런이 sharding을 APK에 적용해야하기 때문이라 합니다. 이 sharding은 swap(cold, hot, warm)을 하기 위해 필요한 미리 수행되어야 하는 작업으로 발표자는 풀빌드에서 시간이 더 걸리지만 이후의 증분 빌드에서 시간을 줄일 수 있다고 이야기합니다. 간단히 위의 결과를 가지고 생각해보면 풀빌드 한번 후 증분 빌드를 2번만 하면 거의 동일하고 2번 이상부터는 매번 3초가량 시간을 벌 수 있겠네요.

7. 의도치 않은 수정은 피할 것

1
2
3
4
5
6
7
8
def buildDateTime = new Date().format('yyMMddHHmm').toInteger()

android {
defaultConfig {
versionCode buildDateTime
...
}
}

위의 코드는 매 빌드가 유일한 버전코드를 갖도록 시간을 가지고 버전 코드를 설정하고 있습니다. 별거 아닌것 같은 코드이지만 이 코드는 매번 빌드를 할때마다 AndroidManifest를 변경합니다. 자세히 얘기하면 versionCode같은 값들은 그래들 빌드 스크립트에 기술되어 있지만 실제 빌드가 되는 과정에서 AndroidManifest.xml에 합쳐(merge)집니다.참고.

위의 코드를 보면 매번 빌드 할때마다 versionCode가 변경되는데 이는 곧 매 빌드 마다 AndroidManifest의 uses-sdk 속성이 변경 되는 것이죠.참고. 아무튼 위의 코드가 적용된 후 결과는 아래와 같습니다.

빌드 타입 | 속도 차이(초) | 속도 차이(백분율)
— | — |
증분 빌드(자바) | +3s | +130%
증분 빌드(리소스) | +3.6s | +90%

이런 문제를 개선하기 위해서는 앞서 개발 빌드 시에 멀티 APK를 적용하지 않았던 방법을 이용하면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
def buildDateTime = project.hasProperty('devBuild')? 100: new Date().format('yyMMddHHmm').toInteger()

android {
...
defaultConfig {
...
versionCode buildDateTime
...
}
...
}

8. 다이나믹 버전은 사용하지 말 것

다이나믹 버전은 아래와 같이 그래들에 외부 라이브러리를 임포트 할 때, 뒤에 +를 붙여서 가장 최근 버전의 라이브러리를 가져올 수 있게하는 기능입니다.

발표자의 설명에 의하면 이 기능이 라이브러리의 새 버전을 추가로 확인하게 만들기 때문에 의존성 결정(dependency resolution) 시간을 증가시킨다고 합니다.

1
2
3
4
5
android {
dependencies {
compile 'com.android.support:appcompat-v7:+'
}
}

또 하나의 문제는 이게 앱을 nondeterministic하게 만든다는 겁니다. 코드의 수정이 전혀 없더라도 라이브러리의 새 버전이 나오면 빌드가 달라진다는 겁니다. 이거는 앱의 형상관리랑도 연관이 있는 부분입니다.

대부분의 프로젝트는 git같은 형상관리 툴을 이용해서 관리합니다. 형상관리 툴의 핵심기능이자 키포인트 중에 하나는, 특정 시점의 빌드를 내가 원할때 빌드해 낼 수 있고 이 빌드는 항상 동일해야 한다는 겁니다. 바이너리 수준에서 완전히 동일하진 않을 수도 있겠지만 적어도 라이브러리가 바뀌면 안되겠죠.

오래 전 내보낸 빌드에 핫픽스가 필요해서 코드의 특정 부분만 고쳐 새로 빌드를 했는데 임포트하는 라이브러리중에 업데이트가 된게 있다면? 최악의 경우 업데이트 된 라이브러리가 하위호환성을 지켜주지 않는다면? 꽤 골치아픈 상황이 되겠죠.

9. 메모리를 확인할 것

그래들에 할당하는 메모리

1
2
#gradle.property 파일
org.gradle.jvmargs=-Xmx1536m

덱스 처리 시 사용하는 메모리

1
2
3
4
#build.gradle 파일
dexOptions {
javaMaxHeapSize = "4g"
}

그래들에 할당하는 메모리의 적정량은 프로젝트 마다 다르니 메모리를 잘 조절하면서 최적의 값을 찾으라고 합니다. 한가지 주의할 점은 무작정 메모리를 많이 할당하면 오히려 성능이 떨어질 수도 있습니다. 본인 시스템의 메모리 용량에 따라 잘 맞춰서 설정해야합니다.

두번째는 덱스 프로세스의 자바 힙 크기를 설정하는 방법입니다. 덱스 프로세스가 기존에는 out-process로 동작했으나 안드로이드 스튜디오 2.1 부터 기본값이 in-process로 변경되어 저 설정은 이제 의미가 없다고 합니다. 있으면 그냥 지워버리라네요. 근데 여기서 out-process랑 in-process가 뭔지 조금 애매합니다. 기존에는 별도의 프로세스로 처리하다가 2.1부터는 안드로이드 스튜디오 프로세스 내에서 처리한다는 걸로 이해했는데 맞는지 모르겠네요. 안드로이드 스튜디오에서 빌드 한번 돌리면 램을 2기가 넘게 써대던데 이것 때문일지도 모르겠습니다.

10. 그래들 캐시를 사용할 것

그래들의 새 캐싱 메카니즘 설정이라는데 모든 작업 결과를 캐시할 수 있다고 합니다. 안드로이드 스튜디오 2.3에 소개됐던 빌드 캐시랑 다른 점은 빌드 캐시는 pre-dexed external libararies만 캐시하는데 요건 가장 최근 빌드 뿐만 아니라 이전 빌드에 대한 캐시도 있다고 합니다. 또한 브랜치를 오가며 빌드하는 경우에도 캐시가 되고 캐싱된걸 배포할수도 있다고 하는데… 뭔 말인지는 잘 모르겠는데 어쨌든 안드로이드 스튜디오 3.0에 제대로 들어갈거라 하니 안드로이드 스튜디오 3.0이 정식 릴리즈 되면 그때 지켜봐야겠습니다.

1
2
# Set this in gradle.properties
org.gradle.caching=true

마무리

위의 최적화를 모두 마친 결과가 아래와 같습니다.

  • 전체 빌드 : 59s -> 19s로 3배 빨라짐
  • 증분 빌드(자바) : 24s -> 2s로 12배 빨라짐
  • 증분 빌드(리소스): 15s -> 4.5s로 3배 빨라짐

팁들을 대강 살펴보면 개발 빌드에 필요 없는 항목들은 빌드를 하지 않는데 초점이 맞쳐져 있는걸 알 수 있습니다. 즉 개발 중에는 위의 팁들을 이용해 빌드 속도가 많이 향상될 수 있지만 릴리즈 빌드 시에는 결국 비슷한 속도로 빌드가 진행되게 됩니다.

또한 위의 팁들은 빌드 툴의 영향을 받는 것들이 대부분입니다. 즉 발표자의 발표 시점과 실제 적용 시에는 차이가 있을 수 있습니다. 따라서 위의 결과들은 앞으로 안드로이드 스튜디오나 빌드 툴이 변해가며 계속 변할 수 있는 것들이라는 점을 유념해야 합니다.

어쨋거나 개발 과정에 빌드 속도는 생산성에 큰 영향을 미치는 요소입니다. 빌드 속도가 빠르면 코드 수정 후 결과를 빠르게 확인 할 수 있고 그만큼 집중력을 유지할 수 있겠죠. 개인적으로는 아주 재미있었던 세션이였습니다. 시간이 되시는 분들은 원래 영상을 꼭 직접 보시면 좋을 것 같습니다.

번외(직접 테스트)

참고로 제가 개발하는 회사 프로젝트에 간단히 적용할 수 있는 최적화를 몇개 해보았는데 전체 빌드 기준 1분41초 -> 56초의 개선이 있었습니다.

적용한 최적화 팁

  • 그래들 버전업(2.2.2 -> 3.0.0-beta6)
  • 레거시 멀티 덱스 우회(개발 빌드 시 minSdk 16 -> 19)
  • PNG Crunch 모드 끄기
  • 필요한 리소스만 빌드(xhdpi만 빌드)
최적화 전 최적화 후
101초 56초

참고 링크

Speeding Up Your Android Gradle Builds (Google I/O ‘17

구글 I/O의 ‘Speeding Up Your Android Gradle Builds’ 세션 영상입니다.

Slide for the speech(Spddeing Up Your Android Gradle Build)

발표자의 발표자료입니다.

Optimize Your Build Speed

안드로이드 개발자 사이트의 Optimize Your Build Speed 문서입니다. 발표 영상에 나오는 대부분의 내용들을 볼 수 있습니다.

Build and Run Your App

안드로이드 빌드 및 실행에 대한 전반적인 설명이 있는 페이지입니다.

Cold, hot, warm swap

콜드, 핫, 웜 스왑에 대한 설명입니다.

Version Your App

안드로이드 버전 설정에 대한 내용입니다.

AndroidManifest uses-sdk property

그래들에 명시한 sdk 관련 설정들이 빌드 과정에서 AndroidManifest에 병합되어 처리된다는 걸 알 수 있습니다.

About Android Plugin for Gradle 3.0.0

그래들 3.0.0 버전에 대한 설명 페이지입니다.