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

이 글은 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 버전에 대한 설명 페이지입니다.

홈스크린에 뷰 띄우기

안드로이드를 쓰다보면 홈화면에 뷰를 띄우는걸 종종 볼 수 있습니다. 대표적으로 네이버앱의 퀵메뉴나 페이스북 메신저의 챗헤드가 있죠. 퀵메뉴는 네이버앱의 각 메뉴로 바로갈 수 있는 런처 역할을, 챗헤드는 앱으로 진입하지 않고 홈화면에서 바로 대화를 이어갈 수 있도록 해줍니다. 이처럼 홈화면은 사용자가 홈버튼을 누르거나 단말을 켰을때 가장 먼저 보는 기본 화면이기에 접근성이 높아 이를 잘 이용하는 것은 매우 중요합니다.

quickmenu

오늘은 이렇게 홈스크린에 직접 뷰를 띄우는 방법에 대해 공유하려고 합니다. 이 방식은 __윈도우(Window)__를 직접 생성하고 여기에 뷰를 넣는 방식입니다. 안드로이드에서는 홈 화면을 이용할 수 있도록 위젯을 제공하지만 기본으로 제공하는 위젯은 크기와 위치가 제한됩니다. 하지만 이 방법은 위젯과는 달리 위치와 크기를 자유롭게 조절할 수 있기 때문에 활용도가 매우 높습니다.

윈도우란

아까 잠시 언급했듯이 이 방법은 직접 윈도우를 생성하고 뷰를 넣는 방식입니다. 그래서 자세한 방법을 설명하기 전에 윈도우에 대해 간단히 알고 넘어가야합니다.

윈도우는 화면에 UI를 그리기 위한 Surface를 갖습니다. 즉 화면에 뭔가 그려지고 있다는 것은 윈도우가 있는 것입니다. 또한 윈도우는 사용자와의 인터렉션을 위한 키, 터치 이벤트 등을 처리할 수 있는 컴포넌트입니다.

기본적으로 액티비티를 생성하면 액티비티는 윈도우를 갖습니다. 또한 아래 그림과 같이 토스트, 스테이터스 바, 다이얼로그등도 개별적인 윈도우를 가집니다.

토스트, 스테이터스바 다이얼로그 액티비티
ex1 ex2 ex3

윈도우 타입과 이벤트 처리

윈도우의 목적과 종류에 따라 윈도우는 서로 다른 타입을 가집니다. 예를들어 액티비티에 주로 사용되는 타입(TYPE_BASE_APPLICATION)의 윈도우는 키, 터치 이벤트를 받지만 토스트에 사용되는 타입(TYPE_TOAST)의 윈도우는 키, 터치 이벤트를 받지 못합니다. 이러한 이벤트는 윈도우의 타입에 따라 다르고 윈도우 생성 시 플래그를 통해 어느정도 제어가 가능합니다.

우선순위

윈도우의 타입에 따라 우선순위가 달라집니다. 우선순위는 화면에 그려지는 순서를 결정하는데 우선순위가 높을수록 나중에 그려져서 우선순위가 낮은 윈도우를 덮을 수 있습니다. 가령 토스트의 윈도우 타입인 TYPE_TOAST는 액티비티가 사용하는 TYPE_BASE_APPLICATION보다 우선순위가 높기때문에 액티비티에서 토스트를 띄우면 토스트가 액티비티 위에 그려지게 됩니다. 우선순위는 안드로이드 버전에 따라 조금씩 다릅니다.

구현하기

홈스크린에 뷰를 넣는 방법은 아래와 같습니다.

  1. 원하는 뷰를 생성한다.
  2. 적절한 타입과 속성을 갖는 윈도우 레이아웃 파라미터(WindowManager.LayoutParam)을 생성한다.
  3. 윈도우 매니저의 addView(View, ViewGroup.LayoutParams params)을 이용하여 윈도우를 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 뷰 생성
ImageView floatingView = new ImageView(this);
floatingView.setImageResource(R.drawable.ic_pholar);

// 2. 윈도우 레이아웃 파라미터 생성 및 설정
mWindowLp = new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_PHONE);
mWindowLp.width = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowLp.height = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowLp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

// 3. 윈도우 생성
mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
mWindowManager.addView(floatingView, mWindowLp);

홈화면과 일반 액티비티는 윈도우 타입이 TYPE_BASE_APPLICATION 입니다. 반면 위 코드에서 사용한 TYPE_PHONE은 TYPE_BASE_APPLICATION이기 때문에 홈화면이나 액티비티 보다 우선순위가 높아 홈 또는 액티비티 위에 그려집니다. 또한 TYPE_PHONE는 status_bar위로 올라갈 수는 없으며 터치 이벤트를 받을 수 있습니다.

TYPE_PHONE은 아래 권한이 필요합니다.

android.permission.SYSTEM_ALERT_WINDOW

WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE는 현재 윈도우보다 하위 윈도우가 터치 이벤트를 받을 수 있도록 하는 플래그 입니다. 이걸 세팅하지 않으면 생성한 윈도우보다 낮은 순위를 갖는 홈이나 액티비티가 터치 이벤트를 받을 수 없습니다.

마지막에 보면 윈도우 매니저를 통해 윈도우를 생성하고 있습니다. 윈도우는 안드로이드의 윈도우매니저를 통해서 접근이 가능합니다. addView()를 통해 윈도우를 추가하였지만 삭제할때는 removeView()를 하면됩니다. 윈도우의 크기나 위치가 변경 되었다면 updateViewLayout()를 통해 변경사항을 적용해줍니다.

윈도우의 라이프사이클이 생성한 곳에 따라 달라집니다. 액티비티에서 생성하면 액티비티가 종료될때 사라지므로 서비스에서 생성하도록 합니다. 또한 서비스가 종료될때는 윈도우를 제거하도록 처리해주는 것도 중요합니다.

아래 이미지는 위의 코드로 생성한 홈 위젯입니다.

ex4

드래깅 구현하기

여기에 퀵메뉴나 챗헤드 처럼 자유롭게 움직일 수 있도록 하고 싶다면 아래와 같이 터치 이벤트를 처리해줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
floatingView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartingX = ev.getRawX();
mStartingY = ev.getRawY();

mWidgetStartingX = mWindowLp.x;
mWidgetStartingY = mWindowLp.y;
return false;
case MotionEvent.ACTION_MOVE:
float deltaX = mStartingX - ev.getRawX();
float deltaY = mStartingY - ev.getRawY();
mWindowLp.x = (int) (mWidgetStartingX - deltaX);
mWindowLp.y = (int) (mWidgetStartingY - deltaY);
mWindowManager.updateViewLayout(floatingView, mWindowLp);
return true;
}
return false;
}
});

이벤트 처리시에 ev.getRawX(), ev.getRawY()를 사용했는데 이렇게 하면 스크린에서의 터치 위치를 가져올 수 있습니다. 이 뷰는 스크린내에서 움직여야 하기때문에 getRawX, getRawY를 사용합니다.

생성 및 제거 애니메이션 처리하기

윈도우가 생성되고 제거될때의 애니메이션은 시스템에서 제공하는 애니메이션만 적용이 가능합니다. 적용하는 방법은 아래와 같이 WindowManager.LayoutParam을 이용합니다.

mWindowLp.windowAnimations = android.R.style.Animation_Toast;

생성 및 제거 시 애니메이션만 위와 같은 제약이 있고 뷰나 레이아웃 자체에 주는 애니메이션은 자유롭게 적용이 가능합니다.

그 외 재미있는 것들

스테이터스바 높이 구하기

안드로이드 개발자 페이지에 가면 다른 튜토리얼이나 가이드는 굉장히 잘 되어있는데 이상하게 윈도우에 대한 설명은 매우 적습니다. 그래서 윈도우에 대해 제대로 이해하기가 어려웠는데요. 윈도우에 대해 앞서 설명한 정도의 배경만 알아도 많은 도움이 되는 것 같습니다. 가령 개발을 하다보면 스테이터스 바의 높이를 구하고 싶을때가 있는데요. 스택 오버플로우에서 찾아보면 아래와 같은 코드가 나옵니다.

1
2
3
4
Rect rectangle= new Rect();
Window window= getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
int statusBarHeight= rectangle.top;

처음에는 이 코드를 이해를 못하고 그냥 가져다 썼는데요. 위에 정리한 내용을 보고 다시 보면..먼저 현재 액티비티의 윈도우를 가져오고 윈도우의 Rect를 가져옵니다. 일반적인 윈도우는 스테이터스 바 아래 위치하기 때문에 액티비티 윈도우의 top이 바로 스테이터스바의 높이가 됩니다.

키보드가 올라왔는지 체크하기

또 아래 코드는 키보드가 현재 올라와 있는지를 체크하는 코드입니다. 안드로이드는 키보드가 올라와 있는지 내려가 있는지 알 수 있는 API를 제공해주지 않기 때문에 여러가지 꼼수를 써서 이를 체크해야하는데요. 가장 많이 사용되는 것이 아래의 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
contentView.getViewTreeObserver().addOnGlobalLayoutListener(new 	ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {

Rect r = new Rect();
contentView.getWindowVisibleDisplayFrame(r);
int screenHeight = contentView.getRootView().getHeight();

// r.bottom is the position above soft keypad or device button.
// if keypad is shown, the r.bottom is smaller than that before.
int keypadHeight = screenHeight - r.bottom;

Log.d(TAG, "keypadHeight = " + keypadHeight);

if (keypadHeight > screenHeight * 0.15) { // 0.15 ratio is perhaps enough to determine keypad height.
// keyboard is opened
}
else {
// keyboard is closed
}
}
});

이것도 앞서 윈도우에 대해 정리한걸 바탕으로 보면 아주 간단합니다. 현재 액티비티 윈도우의 높이를 구하고, 이 높이와 스크린의 높이를 대강 비교해서 액티비티의 높이가 키보드가 올라왔다고 판단할 수 있을만큼 작아졌다면 키보드가 올라왔다고 판단하는 것입니다.(키보드가 올라오면 액티비티의 윈도우는 그만큼 줄어듭니다)

마무리

사실 예전에 네이버 퀵메뉴를 만들때는 온전히 이해하지 못하고 적용을 했었는데 핑퐁을 하면서 정리하다보니 이제서야 오히려 더 이해를 깊게 하게 된것 같습니다. 정리하는 내내 재미있었네요. 윈도우 타입과 속성을 잘 조합하면 재미있는 것들을 많이 해볼 수 있을 것 같습니다.