import android.support.annotation.IntDef; ... publicabstractclassActionBar { ... // Define the list of accepted constants and declare the NavigationMode annotation @Retention(RetentionPolicy.SOURCE) @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}) public@interface NavigationMode {}
// Declare the constants publicstaticfinalintNAVIGATION_MODE_STANDARD=0; publicstaticfinalintNAVIGATION_MODE_LIST=1; publicstaticfinalintNAVIGATION_MODE_TABS=2;
// Decorate the target methods with the annotation @NavigationMode publicabstractintgetNavigationMode();
// Attach the annotation publicabstractvoidsetNavigationMode(@NavigationModeint mode);
NavigationMode라는 Typedef 어노테이션을 생성합니다. NavigationMode에는 @IntDef를 통해 NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS 세 가지 값만 들어올 수 있음을 명시하고 있고, 각 상수는 아래 public static final int로 선언이 되어 있습니다. **@Retention(RetentionPolicy.SOURCE)**는 컴파일러가 .clss에 @NavigationMode의 데이터를 저장하지 않도록 합니다.
이제 NavigationMode는 함수의 반환값이나 인자, 객체의 필드에 사용될 수 있습니다. 코드에서 보면 getNavigationMode() 함수의 반환값과 setNavigationMode() 함수의 인자에 @NavigationMode 어노테이션이 붙어있습니다.
이를 통해 getNavigationMode()의 반환값이나 setNavigationMode()의 mode 인자에 NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS 외의 값이 들어가면 warning이 발생합니다.
이렇게 생성한 Typedef 어노테이션은 안드로이드 스튜디오의 코드 완성에도 적용되어 간편하게 사용할 수 있습니다. @StringDef를 통해 문자열도 동일하게 사용이 가능합니다.
Typedef 어노테이션 사용 시 주의점
Typedef 어노테이션을 사용한다 해도 빌드는 정상적으로 수행됩니다. 따라서 반드시 IDE 자체에서 실시간으로 보여주는 warning을 확인하거나, 안드로이드 스튜디오의 Analyze -> Code Infection을 통해 warning을 확인 하는 것이 중요합니다.
마무리
이 글은 ENUM 사용 시 주의할 점을 정리한 글이지만 ENUM을 사용하지 말라는 글은 절대 아닙니다.
ENUM이 DEX의 크기를 증가시키는 문제는 있지만 ENUM을 사용하면 코드의 가독성과 타입의 값에 대한 유효성(Type safety)이 좀 더 보장됩니다.
이는 굉장한 장점입니다. 따라서 가독성이나 안정성이 중요시 되는 코드에는 ENUM을 사용하는게 맞습니다.
비슷한 예로 C언어의 매크로 함수가 있습니다. 매크로 함수는 함수처럼 사용할 수 있지만 컴파일 시에 직접 코드로 치환되어 실행파일에 삽입됩니다. 따라서 매크로 함수를 사용하면 함수 호출의 부하를 줄일 수 있습니다.
하지만 매크로 함수도 잘 못 사용하면 오히려 성능을 저하시킵니다. 가령 코드의 너무 많은 곳에서 매크로 함수를 호출하게되면 호출 부에 모두 코드로 치환되기 때문에 코드 영역의 크기가 매우 커질 수 있습니다. 즉 경우에 따라 오히려 성능을 떨어뜨릴 수 있습니다.
ENUM과 Typedef 어노테이션 역시 비슷합니다. 각각의 장단점을 보고 필요한 곳에 잘 사용하는 것이 중요합니다. 또한 Proguard를 사용하면 위의 최적화를 대부분 알아서 수행해 줍니다. 어찌보면 굳이 Typedef 어노테이션을 사용할 필요가 없을지도 모릅니다.
이런 관점에서보면 안드로이드 플랫폼 코드에 Typedef 어노테이션을 권장하는 이유를 알 수 있습니다. 안드로이드 플랫폼 코드는 프로가드를 적용하지 않고, 전체 코드의 크기가 굉장히 크기 때문에 ENUM의 사용을 제한하지 않으면 플랫폼 코드가 대책없이 커질 수 있으니 딱 Typedef 어노테이션을 사용하기 좋은 곳이겠죠.
안드로이드 스튜디오 팀에서는 빌드 속도와 버그수정을 위해 그래들 플러그인을 계속 업데이트 하고 있습니다. 그래들 3.0부터는 구글 메이븐 저장소를 통해 그래들이 배포됩니다. 따라서 메이븐 구글 저장소를 추가해주고, dependency에 gradle 3.0.0을 추가해줍니다. 발표 영상에는 alpha1인데 오늘(2017.09.20일) 확인해본 바로는 beta4까지 나왔네요.
멀티덱스 사용 시 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
멀티 APK는 ABI나 density에 따라 apk를 다르게 빌드하는 기능입니다. 한 프로젝트에 대해 여러 APK가 생성되지만, 각 빌드의 결과물로 나오는 APK 크기가 작아지는 장점이 있습니다. 하지만 개발환경에선 필요없는 기능이죠. 따라서 개발 용 빌드인 경우에는 멀티 APK 기능을 아래와 같이 꺼줍니다.
앱이 다국어를 지원한다면 여러 스트링 리소스를 포함합니다. 다국어를 지원하지 않더라도 대부분의 앱은 하나 이상의 스크린 density를 지원하는 경우가 많습니다. 안드로이드의 빌드 시 기본 설정은 모든 버전의 리소스를 빌드에 포함시키도록 되어있습니다. 하지만 아래와 같이 특정 버전의 리소스들만 포함시키도록 할 수 있습니다. 아래는 개발용 빌드에 특정 리소스만 포함 시키도록 하는 그래들 설정 예시입니다.
안드로이드 빌드 툴은 PNG 크기 최적화를 기본으로 수행합니다. 최적화라면 PNG 파일들을 압축하는 거겠죠. 당연히 PNG 파일이 많으면 빌드 시 많은 시스템 자원을 사용하게 됩니다. 릴리즈 시에는 APK 크기를 줄여주므로 중요한 기능이지만 개발 시에는 별 필요 없는 기능입니다. 역시나 아래와 같이 그래들 빌드 스크립트를 이용해 설정을 살포시 꺼줍니다.
아니면 아예 webP 포맷을 사용하는 것도 좋은 방법입니다. webP는 PNG 파일보다 최대 25% 작은 크기를 갖는 포맷입니다. 안드로이드 스튜디오 2.3+ 부터는 IDE에서 PNG를 webP로 변환하는 기능을 지원합니다. 불투명 이미지는 안드로이드 API 15+, 투명도를 갖는 이미지는 API 18+부터 지원합니다. 또한 애초에 최적화된 PNG를 사용하면 최적화가 수행되어도 실제 APK의 크기는 별 차이가 없습니다. 그냥 시간만 잡아 먹는 거죠.
아래는 위의 PNG 최적화 기능을 끈 결과입니다.
빌드 타입 | 속도 차이(초) | 속도 차이(백분율) — | — | 전체 빌드 | -9s | -33% 증분 빌드(자바) | same | same 증분 빌드(리소스) | same | same
인스턴트 런은 안드로이드 스튜디오 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초가량 시간을 벌 수 있겠네요.
위의 코드는 매 빌드가 유일한 버전코드를 갖도록 시간을 가지고 버전 코드를 설정하고 있습니다. 별거 아닌것 같은 코드이지만 이 코드는 매번 빌드를 할때마다 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()
또 하나의 문제는 이게 앱을 nondeterministic하게 만든다는 겁니다. 코드의 수정이 전혀 없더라도 라이브러리의 새 버전이 나오면 빌드가 달라진다는 겁니다. 이거는 앱의 형상관리랑도 연관이 있는 부분입니다.
대부분의 프로젝트는 git같은 형상관리 툴을 이용해서 관리합니다. 형상관리 툴의 핵심기능이자 키포인트 중에 하나는, 특정 시점의 빌드를 내가 원할때 빌드해 낼 수 있고 이 빌드는 항상 동일해야 한다는 겁니다. 바이너리 수준에서 완전히 동일하진 않을 수도 있겠지만 적어도 라이브러리가 바뀌면 안되겠죠.
오래 전 내보낸 빌드에 핫픽스가 필요해서 코드의 특정 부분만 고쳐 새로 빌드를 했는데 임포트하는 라이브러리중에 업데이트가 된게 있다면? 최악의 경우 업데이트 된 라이브러리가 하위호환성을 지켜주지 않는다면? 꽤 골치아픈 상황이 되겠죠.
#build.gradle 파일 dexOptions { javaMaxHeapSize = "4g" }
그래들에 할당하는 메모리의 적정량은 프로젝트 마다 다르니 메모리를 잘 조절하면서 최적의 값을 찾으라고 합니다. 한가지 주의할 점은 무작정 메모리를 많이 할당하면 오히려 성능이 떨어질 수도 있습니다. 본인 시스템의 메모리 용량에 따라 잘 맞춰서 설정해야합니다.
두번째는 덱스 프로세스의 자바 힙 크기를 설정하는 방법입니다. 덱스 프로세스가 기존에는 out-process로 동작했으나 안드로이드 스튜디오 2.1 부터 기본값이 in-process로 변경되어 저 설정은 이제 의미가 없다고 합니다. 있으면 그냥 지워버리라네요. 근데 여기서 out-process랑 in-process가 뭔지 조금 애매합니다. 기존에는 별도의 프로세스로 처리하다가 2.1부터는 안드로이드 스튜디오 프로세스 내에서 처리한다는 걸로 이해했는데 맞는지 모르겠네요. 안드로이드 스튜디오에서 빌드 한번 돌리면 램을 2기가 넘게 써대던데 이것 때문일지도 모르겠습니다.
그래들의 새 캐싱 메카니즘 설정이라는데 모든 작업 결과를 캐시할 수 있다고 합니다. 안드로이드 스튜디오 2.3에 소개됐던 빌드 캐시랑 다른 점은 빌드 캐시는 pre-dexed external libararies만 캐시하는데 요건 가장 최근 빌드 뿐만 아니라 이전 빌드에 대한 캐시도 있다고 합니다. 또한 브랜치를 오가며 빌드하는 경우에도 캐시가 되고 캐싱된걸 배포할수도 있다고 하는데… 뭔 말인지는 잘 모르겠는데 어쨌든 안드로이드 스튜디오 3.0에 제대로 들어갈거라 하니 안드로이드 스튜디오 3.0이 정식 릴리즈 되면 그때 지켜봐야겠습니다.
1 2
# Set thisin gradle.properties org.gradle.caching=true
마무리
위의 최적화를 모두 마친 결과가 아래와 같습니다.
전체 빌드 : 59s -> 19s로 3배 빨라짐
증분 빌드(자바) : 24s -> 2s로 12배 빨라짐
증분 빌드(리소스): 15s -> 4.5s로 3배 빨라짐
팁들을 대강 살펴보면 개발 빌드에 필요 없는 항목들은 빌드를 하지 않는데 초점이 맞쳐져 있는걸 알 수 있습니다. 즉 개발 중에는 위의 팁들을 이용해 빌드 속도가 많이 향상될 수 있지만 릴리즈 빌드 시에는 결국 비슷한 속도로 빌드가 진행되게 됩니다.
또한 위의 팁들은 빌드 툴의 영향을 받는 것들이 대부분입니다. 즉 발표자의 발표 시점과 실제 적용 시에는 차이가 있을 수 있습니다. 따라서 위의 결과들은 앞으로 안드로이드 스튜디오나 빌드 툴이 변해가며 계속 변할 수 있는 것들이라는 점을 유념해야 합니다.
어쨋거나 개발 과정에 빌드 속도는 생산성에 큰 영향을 미치는 요소입니다. 빌드 속도가 빠르면 코드 수정 후 결과를 빠르게 확인 할 수 있고 그만큼 집중력을 유지할 수 있겠죠. 개인적으로는 아주 재미있었던 세션이였습니다. 시간이 되시는 분들은 원래 영상을 꼭 직접 보시면 좋을 것 같습니다.
번외(직접 테스트)
참고로 제가 개발하는 회사 프로젝트에 간단히 적용할 수 있는 최적화를 몇개 해보았는데 전체 빌드 기준 1분41초 -> 56초의 개선이 있었습니다.