다중 APK 빌드

주의: 2021년 8월부터 모든 신규 앱은 App Bundle로 게시해야 합니다. 앱을 Google Play에 게시하는 경우 Android App Bundle을 빌드하고 업로드합니다. 이렇게 하면 Google Play에서 각 사용자의 기기 설정에 최적화된 APK를 자동으로 생성하고 제공하므로 사용자는 앱을 실행하는 데 필요한 코드와 리소스만 다운로드합니다. AAB 형식을 지원하지 않는 스토어에 게시하는 경우 다중 APK 게시가 유용합니다. 이 경우 각 APK를 직접 빌드, 서명 및 관리해야 합니다.

가능하면 모든 대상 기기를 지원하는 단일 APK를 빌드하는 것이 더 좋지만 이 경우 다양한 화면 밀도 또는 Application Binary Interface (ABI)를 지원하는 파일 때문에 APK가 매우 커질 수 있습니다. APK 크기를 줄이는 한 가지 방법은 특정 화면 밀도 또는 ABI용 파일이 포함된 다중 APK를 만드는 것입니다.

Gradle은 각 밀도 또는 ABI에 관련된 코드와 리소스만 포함되어 있는 별도의 APK를 만들 수 있습니다. 이 페이지에서는 빌드를 구성하여 다중 APK를 생성하는 방법을 설명합니다. 화면 밀도나 ABI에 기반하지 않는 다양한 버전의 앱을 만들어야 한다면 빌드 변형을 대신 사용하세요.

다중 APK를 위한 빌드 구성

다중 APK를 위한 빌드를 구성하려면 splits 블록을 모듈 수준 build.gradle 파일에 추가합니다. splits 블록 내에 Gradle이 밀도별 APK를 생성하는 방법을 지정하는 density 블록 또는 Gradle이 ABI별 APK를 생성하는 방법을 지정하는 abi 블록을 제공합니다. 개발자가 밀도와 ABI 블록을 모두 제공하면 빌드 시스템에서 각 밀도와 ABI 조합을 위한 APK를 만듭니다.

화면 밀도를 위한 다중 APK 구성

다양한 화면 밀도를 위한 별도의 APK를 만들려면 splits 블록 내에 density 블록을 추가합니다. density 블록에서 원하는 화면 밀도와 호환 가능한 화면 크기의 목록을 제공합니다. 각 APK의 매니페스트에 특정 <compatible-screens> 요소가 필요한 경우에만 호환되는 화면 크기의 목록을 사용합니다.

다음 Gradle DSL 옵션은 화면 밀도를 위한 다중 APK를 구성하는 데 사용됩니다.

enable: Groovy, isEnable: Kotlin 스크립트용
이 요소를 true로 설정하면 Gradle은 정의된 화면 밀도를 기반으로 다중 APK를 생성합니다. 기본값은 false입니다.
exclude
Gradle이 별도의 APK를 생성하지 않을 밀도를 쉼표로 구분된 목록으로 지정합니다. 대부분의 밀도에 APK를 생성하고 싶지만 앱에서 지원하지 않는 일부 밀도는 제외해야 한다면 exclude를 사용하세요.
reset()

화면 밀도의 기본 목록을 삭제합니다. include 요소와 결합하여 추가하려는 밀도를 지정할 때만 사용합니다.

다음 스니펫은 reset()을 호출하여 목록을 삭제한 다음 include를 사용하여 밀도 목록을 ldpixxhdpi로 설정합니다.

reset()                  // Clears the default list from all densities
                         // to no densities.
include "ldpi", "xxhdpi" // Specifies the two densities to generate APKs
                         // for.
include
Gradle이 APK를 생성할 밀도를 쉼표로 구분된 목록으로 지정합니다. 정확한 밀도 목록을 지정하기 위해서만 reset()와 함께 사용합니다.
compatibleScreens

쉼표로 구분된 호환 가능한 화면 크기의 목록을 지정합니다. 이렇게 하면 각 APK의 매니페스트에 일치하는 <compatible-screens> 노드가 삽입됩니다.

이 설정을 사용하면 동일한 build.gradle 섹션에서 화면 밀도와 화면 크기를 모두 편리하게 관리할 수 있습니다. 그러나 <compatible-screens>를 사용하면 앱이 작동하는 기기 유형이 제한될 수 있습니다. 다양한 화면 크기를 지원하는 다른 방법은 화면 호환성 개요를 참고하세요.

화면 밀도에 기반한 각 APK에는 APK가 지원하는 화면 유형에 관한 특정 제한이 있는 <compatible-screens> 태그가 포함되어 있기 때문에(여러 APK를 게시하더라도 일부 새 기기는 여러 APK 필터와 일치하지 않습니다.) 따라서 Gradle은 항상 모든 화면 밀도를 위한 애셋을 포함하고 <compatible-screens> 태그를 포함하지 않는 추가적인 범용 APK를 생성합니다. 이 범용 APK를 밀도별 APK와 함께 게시하여 APK와 <compatible-screens> 태그가 일치하지 않는 기기에 대체 기능을 제공합니다.

다음 예에서는 ldpi, xxhdpi, xxxhdpi를 제외하고 각 화면 밀도에 별도의 APK를 생성합니다. 이 작업은 exclude를 사용하여 모든 밀도의 기본 목록에서 이 세 개의 밀도를 삭제하면 완료됩니다.

Groovy

android {
  ...
  splits {

    // Configures multiple APKs based on screen density.
    density {

      // Configures multiple APKs based on screen density.
      enable true

      // Specifies a list of screen densities you don't want Gradle to create multiple APKs for.
      exclude "ldpi", "xxhdpi", "xxxhdpi"

      // Specifies a list of compatible screen size settings for the manifest.
      compatibleScreens 'small', 'normal', 'large', 'xlarge'
    }
  }
}

Kotlin

android {
    ...
    splits {

        // Configures multiple APKs based on screen density.
        density {

            // Configures multiple APKs based on screen density.
            isEnable = true

            // Specifies a list of screen densities you don't want Gradle to create multiple APKs for.
            exclude("ldpi", "xxhdpi", "xxxhdpi")

            // Specifies a list of compatible screen size settings for the manifest.
            compatibleScreens("small", "normal", "large", "xlarge")
        }
    }
}

밀도 이름과 화면 크기 이름의 목록은 다양한 화면 크기 지원을 참고하세요. 특정 화면 유형 및 기기에 앱의 다양한 빌드 변형을 맞춤설정하는 방법에 관한 자세한 내용은 제한적인 화면 지원 선언을 참고하세요.

ABI를 위한 다중 APK 구성

다양한 ABI를 위한 별도의 APK를 만들려면 splits 블록 내에 abi 블록을 추가합니다. abi 블록에 원하는 ABI 목록을 제공합니다.

다음 Gradle DSL 옵션은 ABI별로 다중 APK를 구성하는 데 사용됩니다.

Groovy의 경우 enable, Kotlin 스크립트의 경우 isEnable
이 요소를 true로 설정하면 Gradle은 정의된 ABI를 기반으로 다중 APK를 생성합니다. 기본값은 false입니다.
exclude
Gradle이 별도의 APK를 생성하지 않도록 할 ABI를 쉼표로 구분된 목록으로 지정합니다. 대부분의 ABI를 위한 APK를 생성하되 앱에서 지원하지 않는 소수의 ABI는 제외해야 하는 경우 exclude를 사용합니다.
reset()

ABI의 기본 목록을 지웁니다. include 요소와 결합하여 추가하려는 ABI를 지정할 때만 사용합니다.

다음 스니펫은 reset()을 호출하여 목록을 지운 다음 include를 사용하여 ABI 목록을 x86x86_64로 설정합니다.

reset()                 // Clears the default list from all ABIs to no ABIs.
include "x86", "x86_64" // Specifies the two ABIs we want to generate APKs for.
include
Gradle이 APK를 생성할 ABI를 쉼표로 구분된 목록으로 지정합니다. 정확한 ABI 목록을 지정하기 위해서만 reset()와 함께 사용합니다.
Groovy의 경우 universalApk, Kotlin 스크립트의 경우 isUniversalApk

true인 경우 Gradle은 ABI별 APK 외에 범용 APK를 생성합니다. 범용 APK에는 단일 APK에 있는 모든 ABI의 코드와 리소스가 포함됩니다. 기본값은 false입니다.

이 옵션은 splits.abi 블록에서만 사용할 수 있습니다. 화면 밀도에 기반한 다중 APK를 빌드할 때 Gradle은 항상 모든 화면 밀도의 코드와 리소스가 포함된 범용 APK를 생성합니다.

다음 예에서는 각 ABI를 위한 별도의 APK인 x86x86_64를 생성합니다. reset()를 사용하여 빈 ABI 목록으로 시작한 후 각각 APK를 가져오는 ABI 목록과 함께 include를 사용하면 됩니다.

Groovy

android {
  ...
  splits {

    // Configures multiple APKs based on ABI.
    abi {

      // Enables building multiple APKs per ABI.
      enable true

      // By default all ABIs are included, so use reset() and include to specify that you only
      // want APKs for x86 and x86_64.

      // Resets the list of ABIs for Gradle to create APKs for to none.
      reset()

      // Specifies a list of ABIs for Gradle to create APKs for.
      include "x86", "x86_64"

      // Specifies that you don't want to also generate a universal APK that includes all ABIs.
      universalApk false
    }
  }
}

Kotlin

android {
  ...
  splits {

    // Configures multiple APKs based on ABI.
    abi {

      // Enables building multiple APKs per ABI.
      isEnable = true

      // By default all ABIs are included, so use reset() and include to specify that you only
      // want APKs for x86 and x86_64.

      // Resets the list of ABIs for Gradle to create APKs for to none.
      reset()

      // Specifies a list of ABIs for Gradle to create APKs for.
      include("x86", "x86_64")

      // Specifies that you don't want to also generate a universal APK that includes all ABIs.
      isUniversalApk = false
    }
  }
}

지원되는 ABI 목록은 지원되는 ABI를 참고하세요.

네이티브/C++ 코드가 없는 프로젝트

네이티브/C++ 코드가 없는 프로젝트의 경우 Build Variants 패널에는 그림 1과 같이 ModuleActive Build Variant라는 두 개의 열이 있습니다.

빌드 변형 패널
그림 1. 네이티브/C++ 코드가 없는 프로젝트의 경우 두 개의 열이 있는 Build Variants 패널

모듈의 Active Build Variant 값에 따라 편집기에 배포되고 표시되는 빌드 변형이 결정됩니다. 변형 간에 전환하려면 모듈의 Active Build Variant 셀을 클릭하고 목록 필드에서 원하는 변형을 선택합니다.

네이티브/C++ 코드가 있는 프로젝트

네이티브/C++ 코드가 있는 프로젝트의 경우 Build Variants 패널에는 그림 2와 같이 Module, Active Build VariantActive ABI라는 세 개의 열이 있습니다.

그림 2. 네이티브/C++ 코드가 있는 프로젝트의 경우 Active ABI 열이 추가된 Build Variants 패널

모듈의 Active Build Variant 값을 통해 배포되고 편집기에 표시되는 빌드 변형을 판단합니다. 네이티브 모듈의 경우 Active ABI 값에 따라 편집기에서 사용하는 ABI가 결정되지만 이 값은 배포되는 항목에 영향을 주지 않습니다.

빌드 유형 또는 ABI를 변경하려면 다음 단계를 따르세요.

  1. Active Build Variant 또는 Active ABI 열의 셀을 클릭합니다.
  2. 목록 필드에서 원하는 변형 또는 ABI를 선택합니다. 새 동기화가 자동으로 실행됩니다.

앱 또는 라이브러리 모듈의 열을 변경하면 모든 종속 행에 변경사항이 적용됩니다.

버전 관리 구성

기본적으로 Gradle에서 다중 APK를 생성하는 경우 각 APK는 모듈 수준 build.gradle 또는 build.gradle.kts 파일에 지정된 것과 동일한 버전 정보를 가집니다. Google Play 스토어에서는 버전 정보가 모두 동일한 동일한 앱의 다중 APK를 허용하지 않으므로 Play 스토어에 업로드하기 전에 각 APK에 고유한 versionCode가 있는지 확인해야 합니다.

각 APK의 versionCode를 재정의하도록 모듈 수준 build.gradle 파일을 구성할 수 있습니다. 여러 APK를 구성하는 각 ABI 및 밀도에 고유한 숫자 값을 할당하는 매핑을 만들면 defaultConfig 또는 productFlavors 블록 내에 정의된 버전 코드와 밀도 또는 ABI에 할당된 숫자 값을 결합한 값으로 출력 버전 코드를 재정의할 수 있습니다.

다음 예에서 x86 ABI의 APK는 2004라는 versionCode를 가져오고 x86_64 ABI는 versionCode 3004를 가져옵니다.

1000과 같이 큰 단위로 버전 코드를 할당하면 나중에 앱을 업데이트해야 할 때 고유한 버전 코드를 할당할 수 있습니다. 예를 들어 defaultConfig.versionCode가 후속 업데이트에서 5로 반복되면 Gradle은 x86 APK에 2005 versionCode를, x86_64 APK에 3005를 할당합니다.

도움말: 빌드에 범용 APK가 포함되어 있으면 다른 APK보다 낮은 versionCode을 할당하세요. Google Play 스토어에서는 대상 기기와 호환되며 가장 높은 versionCode가 있는 앱 버전을 설치하므로 낮은 versionCode를 범용 APK에 할당하면 Google Play 스토어에서는 범용 APK로 대체하기 전에 APK 중 하나를 설치하려고 시도하게 됩니다. 다음 샘플 코드는 범용 APK의 기본 versionCode를 재정의하지 않고 이 작업을 처리합니다.

Groovy

android {
  ...
  defaultConfig {
    ...
    versionCode 4
  }
  splits {
    ...
  }
}

// Map for the version code that gives each ABI a value.
ext.abiCodes = ['armeabi-v7a':1, x86:2, x86_64:3]

// For per-density APKs, create a similar map:
// ext.densityCodes = ['mdpi': 1, 'hdpi': 2, 'xhdpi': 3]

import com.android.build.OutputFile

// For each APK output variant, override versionCode with a combination of
// ext.abiCodes * 1000 + variant.versionCode. In this example, variant.versionCode
// is equal to defaultConfig.versionCode. If you configure product flavors that
// define their own versionCode, variant.versionCode uses that value instead.
android.applicationVariants.all { variant ->

  // Assigns a different version code for each output APK
  // other than the universal APK.
  variant.outputs.each { output ->

    // Stores the value of ext.abiCodes that is associated with the ABI for this variant.
    def baseAbiVersionCode =
            // Determines the ABI for this variant and returns the mapped value.
            project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))

    // Because abiCodes.get() returns null for ABIs that are not mapped by ext.abiCodes,
    // the following code doesn't override the version code for universal APKs.
    // However, because you want universal APKs to have the lowest version code,
    // this outcome is desirable.
    if (baseAbiVersionCode != null) {

      // Assigns the new version code to versionCodeOverride, which changes the
      // version code for only the output APK, not for the variant itself. Skipping
      // this step causes Gradle to use the value of variant.versionCode for the APK.
      output.versionCodeOverride =
              baseAbiVersionCode * 1000 + variant.versionCode
    }
  }
}

Kotlin

android {
  ...
  defaultConfig {
    ...
    versionCode = 4
  }
  splits {
    ...
  }
}

// Map for the version code that gives each ABI a value.
val abiCodes = mapOf("armeabi-v7a" to 1, "x86" to 2, "x86_64" to 3)

// For per-density APKs, create a similar map:
// val densityCodes = mapOf("mdpi" to 1, "hdpi" to 2, "xhdpi" to 3)

import com.android.build.api.variant.FilterConfiguration.FilterType.*

// For each APK output variant, override versionCode with a combination of
// abiCodes * 1000 + variant.versionCode. In this example, variant.versionCode
// is equal to defaultConfig.versionCode. If you configure product flavors that
// define their own versionCode, variant.versionCode uses that value instead.
androidComponents {
    onVariants { variant ->

        // Assigns a different version code for each output APK
        // other than the universal APK.
        variant.outputs.forEach { output ->
            val name = output.filters.find { it.filterType == ABI }?.identifier

            // Stores the value of abiCodes that is associated with the ABI for this variant.
            val baseAbiCode = abiCodes[name]
            // Because abiCodes.get() returns null for ABIs that are not mapped by ext.abiCodes,
            // the following code doesn't override the version code for universal APKs.
            // However, because you want universal APKs to have the lowest version code,
            // this outcome is desirable.
            if (baseAbiCode != null) {
                // Assigns the new version code to output.versionCode, which changes the version code
                // for only the output APK, not for the variant itself.
                output.versionCode.set(baseAbiCode * 1000 + (output.versionCode.get() ?: 0))
            }
        }
    }
}

대체 버전 코드 체계에 관한 더 많은 예를 보려면 버전 코드 할당을 참고하세요.

다중 APK 빌드

모듈 수준 build.gradle 또는 build.gradle.kts 파일을 구성하여 다중 APK를 빌드한 후에는 Build > Build APK를 클릭하여 Project 창에서 현재 선택된 모듈의 모든 APK를 빌드합니다. Gradle은 프로젝트의 build/outputs/apk/ 디렉터리에 각 밀도 또는 ABI의 APK를 만듭니다.

Gradle은 다중 APK를 구성하는 각 밀도 또는 ABI를 위한 APK를 빌드합니다. 밀도와 ABI 모두를 위해 다중 APK를 사용 설정하면 Gradle은 각 밀도와 ABI 조합을 위한 APK를 만듭니다.

예를 들어, 다음 build.gradle 스니펫을 사용하면 mdpihdpi 밀도는 물론 x86x86_64 ABI를 위한 다중 APK를 빌드할 수 있습니다.

Groovy

...
  splits {
    density {
      enable true
      reset()
      include "mdpi", "hdpi"
    }
    abi {
      enable true
      reset()
      include "x86", "x86_64"
    }
  }

Kotlin

...
  splits {
    density {
      isEnable = true
      reset()
      include("mdpi", "hdpi")
    }
    abi {
      isEnable = true
      reset()
      include("x86", "x86_64")
    }
  }

구성 예의 출력에는 다음 네 개의 APK가 포함됩니다.

  • app-hdpiX86-release.apk: hdpi 밀도와 x86 ABI용 코드 및 리소스를 포함합니다.
  • app-hdpiX86_64-release.apk: hdpi 밀도와 x86_64 ABI용 코드 및 리소스를 포함합니다.
  • app-mdpiX86-release.apk: mdpi 밀도와 x86 ABI용 코드 및 리소스를 포함합니다.
  • app-mdpiX86_64-release.apk: mdpi 밀도와 x86_64 ABI용 코드 및 리소스를 포함합니다.

화면 밀도에 기반한 다중 APK를 빌드할 때 Gradle은 항상 밀도별 APK 외에 모든 밀도의 코드 및 리소스를 포함하는 범용 APK를 생성합니다.

ABI에 기반한 다중 APK를 빌드할 때 Gradle은 build.gradle 파일(Groovy용)의 splits.abi 블록에서 universalApk true을 지정하고 build.gradle.kts 파일(Kotlin 스크립트용)의 splits.abi 블록에 isUniversalApk = true를 지정하는 경우에만 모든 ABI의 코드와 리소스를 포함하는 APK를 생성합니다.

APK 파일 이름 형식

여러 APK를 빌드할 때 Gradle은 다음 체계를 사용하여 APK 파일 이름을 생성합니다.

modulename-screendensityABI-buildvariant.apk

체계 구성요소는 다음과 같습니다.

modulename
빌드 중인 모듈 이름을 지정합니다.
screendensity
화면 밀도를 위한 다중 APK가 사용 설정된 경우 mdpi와 같이 APK의 화면 밀도를 지정합니다.
ABI

ABI용 다중 APK가 사용 설정된 경우 APK의 ABI를 지정합니다(예: x86).

화면 밀도와 ABI 모두를 위한 다중 APK가 사용 설정된 경우 Gradle은 밀도 이름을 ABI 이름과 연결합니다(예: mdpiX86). ABI별 APK에 universalApk가 사용 설정된 경우 Gradle은 범용 APK 파일 이름의 ABI 부분으로 universal를 사용합니다.

buildvariant
빌드 중인 빌드 변형을 지정합니다(예: debug).

예를 들어 myApp 디버그 버전의 mdpi 화면 밀도 APK를 빌드할 때 APK 파일 이름은 myApp-mdpi-debug.apk입니다. mdpi 화면 밀도와 x86 ABI 모두를 위해 다중 APK를 빌드하도록 구성된 myApp 출시 버전의 APK 파일 이름은 myApp-mdpiX86-release.apk입니다.