16KB 페이지 크기 지원

이전에는 Android에서 4KB 메모리 페이지 크기만 지원하여 Android 기기가 일반적으로 보유한 평균 총 메모리에 맞게 시스템 메모리 성능을 최적화했습니다. Android 15부터 AOSP는 페이지 크기 16KB (16KB 기기)를 사용하도록 구성된 기기를 지원합니다. 앱이 NDK 라이브러리를 직접 또는 SDK를 통해 간접적으로 사용하는 경우 이러한 16KB 기기에서 작동하도록 앱을 다시 빌드해야 합니다.

기기 제조업체가 더 많은 실제 메모리 (RAM)가 있는 기기를 계속 빌드함에 따라 이러한 기기 중 다수가 기기 성능을 최적화하기 위해 16KB (결국 더 큰) 페이지 크기를 채택할 것입니다. 16KB 페이지 크기 기기에 대한 지원을 추가하면 앱이 이러한 기기에서 실행될 수 있으며 앱이 관련 성능 개선의 이점을 누릴 수 있습니다. 다시 컴파일하지 않으면 앱이 향후 Android 출시에서 16KB 기기에서 작동하지 않습니다.

앱 지원을 추가할 수 있도록 앱이 영향을 받는지 확인하는 방법, 앱을 다시 빌드하는 방법 (해당하는 경우), 에뮬레이터 (Android Emulator용 Android 15 시스템 이미지 포함)를 사용하여 16KB 환경에서 앱을 테스트하는 방법에 관한 안내를 제공해 드렸습니다.

Google Play 호환성 요구사항

앱이 최신 버전의 Android에서 올바르게 작동하도록 하려면 Android 15(API 수준 35) 이상을 타겟팅하는 모든 앱이 Google Play의 64비트 기기에서 16KB 메모리 페이지 크기를 지원해야 합니다. 2027년 2월 1일부터 앱 업데이트가 16KB 메모리 페이지 크기를 지원하지 않으면 업데이트를 출시할 수 없게 됩니다.

2027년 2월 1일까지 앱 업데이트가 16KB 메모리 페이지 크기를 지원해야 한다는 Google Play Console 경고
그림 1. Google Play Console 호환성 경고

이점 및 성능 향상

16KB 페이지 크기로 구성된 기기는 평균적으로 약간 더 많은 메모리를 사용하지만 시스템과 앱 모두에서 다양한 성능이 개선됩니다.

  • 시스템에 메모리 문제가 있는 동안 앱 실행 시간 단축: 평균 3.16% 감소, 테스트한 일부 앱의 경우 더 큰 개선 (최대 30%)
  • 앱 실행 중 전원 소모 감소: 평균 4.56% 감소
  • 카메라 실행 속도 향상: 평균 핫 스타트 속도가 4.48%, 콜드 스타트 속도가 6.60% 빨라짐
  • 시스템 부팅 시간 개선: 평균 8% (약 950밀리초) 개선됨

이러한 개선사항은 초기 테스트를 기반으로 하며 실제 기기의 결과는 다를 수 있습니다. 테스트를 계속하면서 앱의 잠재적 이점에 관한 추가 분석을 제공할 예정입니다.

앱이 영향을 받는지 확인하기

앱에서 네이티브 코드를 사용하는 경우 16KB 기기를 지원하도록 앱을 다시 빌드해야 합니다. 앱에서 네이티브 코드를 사용하는지 확실히 알 수 없는 경우 APK 분석기를 사용하여 네이티브 코드가 있는지 확인한 다음 찾은 공유 라이브러리의 ELF 세그먼트 정렬을 확인하면 됩니다. Android 스튜디오는 정렬 문제를 자동으로 감지하는 데 도움이 되는 기능도 제공합니다.

앱에서 모든 라이브러리나 SDK를 비롯하여 Java 프로그래밍 언어나 Kotlin으로 작성된 코드만 사용한다면 이미 16KB 기기를 지원하는 것입니다. 하지만 16KB 환경에서 앱을 테스트하여 앱 동작에 예기치 않은 회귀가 없는지 확인하는 것이 좋습니다.

앱에서 네이티브 코드를 사용하는지 확인

다음 중 하나라도 해당하는 경우 앱에서 네이티브 코드를 이용하는 것입니다.

  • 앱에서 C/C++(네이티브) 코드를 사용합니다. 앱에서 Android NDK를 사용하는 경우 앱에서 네이티브 코드를 사용하는 것입니다.
  • 앱이 이를 사용하는 서드 파티 네이티브 라이브러리 또는 종속 항목(예: SDK)과 연결됩니다.
  • 앱이 기기에서 네이티브 라이브러리를 사용하는 서드 파티 앱 빌더로 빌드되었습니다.

APK Analyzer를 사용하여 네이티브 라이브러리 식별

APK Analyzer는 빌드된 APK의 다양한 측면을 평가할 수 있는 도구입니다. 앱에서 네이티브 코드를 사용하는지 확인하려면(16KB 호환 여부와 관계없이) 다음 단계를 따르세요.

  1. Android 스튜디오를 열고 File > Open을 클릭한 후 프로젝트를 선택합니다.
  2. 메뉴 바에서 Build > Analyze APK…를 클릭합니다.

    APK Analyzer를 실행하는 스튜디오 빌드 메뉴 옵션
  3. 분석하려는 APK를 선택합니다.

  4. 공유 객체(.so) 파일(있는 경우)을 호스팅하는 lib 폴더 내부를 살펴봅니다. 공유 객체 파일이 있으면 앱에서 네이티브 코드를 사용하는 것입니다. 정렬 열에는 정렬 문제가 있는 파일에 대한 경고 메시지가 표시됩니다. 공유 객체 파일이 없거나 lib 폴더가 없으면 앱에서 네이티브 코드를 사용하지 않는 것입니다.

    공유 객체 파일이 있음을 보여주는 APK Analyzer 뷰

자동화된 검사를 통해 정렬 문제 감지

사전 빌드된 라이브러리 또는 APK가 16KB를 준수하지 않으면 Android 스튜디오에서 사전에 경고를 표시합니다. APK 분석기 도구를 사용하여 업데이트해야 하는 라이브러리 또는 코드 변경이 필요한지 검토합니다.

프로젝트의 정렬 문제에 관한 스튜디오 경고 알림

Android 스튜디오의 린트는 16KB로 정렬되지 않은 네이티브 라이브러리도 강조 표시합니다.

정렬되지 않은 네이티브 라이브러리에 관한 스튜디오 린터 경고

공유 라이브러리의 ELF 세그먼트 정렬 확인

공유 라이브러리의 경우 16KB ELF 정렬을 사용하여 공유 라이브러리의 ELF 세그먼트가 올바르게 정렬되었는지 확인합니다.

APK Analyzer 사용(권장)

공유 라이브러리의 정렬을 확인하는 가장 쉬운 방법은 Android 스튜디오에서 APK Analyzer를 사용하는 것입니다. 공유 객체 파일이 16KB로 정렬되지 않은 경우 Alignment 열에 경고 메시지가 표시됩니다.

check_elf_alignment.sh 스크립트 사용(Linux 또는 macOS)

Linux 또는 macOS에서 개발 중이고 스크립트를 사용하려는 경우 check_elf_alignment.sh 스크립트를 사용할 수 있습니다.

  1. check_elf_alignment.sh 스크립트를 파일에 저장합니다.

  2. 앱의 APK 파일에서 스크립트를 실행합니다.

    check_elf_alignment.sh APK_NAME.apk
    

    스크립트는 모든 arm64-v8a 공유 라이브러리에 대해 ALIGNED 또는 UNALIGNED를 출력합니다.

  3. arm64-v8a 또는 x86_64 공유 라이브러리가 UNALIGNED인 경우 해당 라이브러리의 패키징을 업데이트한 다음 앱을 다시 컴파일하고 이 섹션의 단계에 따라 다시 테스트해야 합니다.

명령줄 도구 직접 사용

명령줄 도구를 직접 사용하여 ELF 세그먼트의 정렬을 확인하려면 다음 단계를 따르세요.

  1. Android 스튜디오의 SDK Manager 또는 sdkmanager 명령줄 도구를 사용하여 Android SDK Build-Tools 버전 35.0.0 이상과 Android NDK가 모두 설치되어 있는지 확인합니다.
  2. 앱의 APK 파일을 추출합니다.

    Linux 또는 macOS

    unzip APK_NAME.apk -d /tmp/my_apk_out
    

    Windows(PowerShell)

    Expand-Archive -Path .\APK_NAME.apk -DestinationPath ~\tmp\my_apk_out
    
  3. APK 파일을 추출한 임시 디렉터리에서 lib 디렉터리의 콘텐츠에 공유 객체(.so) 파일이 있는지 확인합니다. 이러한 파일은 APK Analyzer를 사용하여 네이티브 라이브러리를 식별하는 동안 표시되는 공유 객체 파일과 동일합니다. 각 공유 객체 파일에서 다음 명령어를 실행합니다.

    Linux 또는 macOS

    SDK_ROOT_LOCATION/Android/sdk/ndk/NDK_VERSION/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-objdump -p SHARED_OBJECT_FILE.so | grep LOAD
    

    Windows(PowerShell)

    SDK_ROOT_LOCATION\Android\sdk\ndk\NDK_VERSION\toolchains\llvm\prebuilt\windows-x86_64\bin\llvm-objdump.exe -p SHARED_OBJECT_FILE.so | Select-String -Pattern "LOAD"
    

    여기서 SDK_ROOT_LOCATION은 Android SDK를 설치한 디렉터리의 경로이고, SHARED_OBJECT_FILE은 확인 중인 공유 객체 파일의 이름이며, NDK_VERSION은 설치한 Android NDK의 버전입니다(예: 28.0.12433566). 출력은 확인하는 각 파일에 대해 다음과 같이 표시됩니다.

    LOAD off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**14
    LOAD off    0x0000000000042a90 vaddr 0x0000000000043a90 paddr 0x0000000000043a90 align 2**14
    LOAD off    0x0000000000046230 vaddr 0x0000000000048230 paddr 0x0000000000048230 align 2**14
    
  4. 출력 줄을 확인하여 로드 세그먼트에 2**14보다 작은 값이 없는지 확인합니다. 로드 세그먼트가 2**13, 2**12 또는 더 낮은 값이면 해당 라이브러리의 패키징을 업데이트한 다음 앱을 다시 컴파일하고 이 섹션의 단계에 따라 다시 테스트해야 합니다.

  5. 그런 다음 앱의 APK 파일에서 zipalign 명령줄 도구를 실행합니다.

    Linux 또는 macOS

    SDK_ROOT_LOCATION/Android/sdk/build-tools/35.0.0/zipalign -v -c -P 16 4 APK_NAME.apk
    

    Windows(PowerShell)

    SDK_ROOT_LOCATION\Android\sdk\build-tools\35.0.0\zipalign.exe -v -c -P 16 4 APK_NAME.apk
    

    여기서 SDK_ROOT_LOCATION는 Android SDK를 설치한 디렉터리의 경로이고 APK_NAME는 앱의 APK 파일 이름입니다. 모든 공유 라이브러리가 올바르게 정렬되면 출력의 마지막 줄에 'Verification successful'이 표시됩니다.

    확인에 실패하면 일부 공유 라이브러리를 재정렬해야 하므로 해당 라이브러리의 패키징을 업데이트한 다음 앱을 다시 컴파일하고 이 섹션의 단계에 따라 다시 테스트해야 합니다.

RELRO 보안 플래그 확인

보안 공격을 완화하기 위해 최신 링커는 Relocation Read-Only(RELRO) 플래그를 사용하여 동적 재배치가 완료된 후 공유 객체 파일의 재배치 타겟 섹션(예: .got 및 .data.rel.ro)을 읽기 전용으로 만듭니다. 빌드에서 RELRO 플래그를 사용 설정합니다.

GNU_RELRO가 쓰기 가능한 LOAD 세그먼트를 후속 쓰기 가능한 데이터(예: .data 또는 .bss)와 공유하는 경우 GNU_RELRO 세그먼트의 끝 주소(VirtAddr + MemSiz)는 16KB 정렬(0x4000)되어야 합니다. 그렇지 않으면 Android 링커가 GNU_RELRO의 끝을 포함하는 16KB 페이지를 읽기 전용으로 표시할 때 동일한 16KB 페이지의 나머지 부분에 있는 인접한 쓰기 가능한 데이터도 읽기 전용이 되어 해당 데이터가 작성될 때 세그먼트 오류(SIGSEGV)와 함께 런타임에 앱이 다운됩니다. 이는 일반적으로 .so 파일이 관련 플래그를 사용 설정하지 않고 NDK 도구 모음 r27 이하로 빌드된 경우에 발생합니다.

Android 스튜디오의 APK Analyzer를 사용하여 공유 객체 파일의 RELRO 정렬 상태를 직접 확인할 수 있습니다.

네이티브 라이브러리의 RELRO 정렬을 보여주는 Android 스튜디오의 APK Analyzer

명령줄 도구 사용

정렬을 수동으로 확인하거나 프로세스를 자동화해야 하는 경우 다음 단계를 따르세요.

1단계: 프로그램 헤더 검사

각 공유 객체 파일에서 다음 명령어를 실행합니다(Linux에서 darwin-x86_64를 linux-x86_64로 대체).

Linux 또는 macOS

SDK_ROOT_LOCATION/Android/sdk/ndk/NDK_VERSION/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-readelf -Wl SHARED_OBJECT_FILE.so | grep 'LOAD\|RELRO\|Type'

Windows(PowerShell)

SDK_ROOT_LOCATION\Android\sdk\ndk\NDK_VERSION\toolchains\llvm\prebuilt\windows-x86_64\bin\llvm-readelf.exe -Wl SHARED_OBJECT_FILE.so | Select-String -Pattern 'LOAD|RELRO|Type'

RELRO 세그먼트가 있는 경우 GNU_RELRO 문자열이 출력됩니다.

2단계: RELRO의 16KB 규정 준수 확인

공유 객체 파일은 다음 조건 중 하나라도 충족하는 경우 RELRO와 관련하여 16KB를 준수합니다.

  1. 프로그램 헤더에 GNU_RELRO 세그먼트가 없습니다.
  2. GNU_RELRO 끝 주소가 16KB로 정렬됨: 가상 주소(VirtAddr)와 메모리 크기(MemSiz)의 합계를 16KB(0x4000)로 나눈 나머지가 0입니다((VirtAddr + MemSiz) % 0x4000 == 0).
  3. 동일한 LOAD 세그먼트에서 GNU_RELRO 뒤에 쓰기 가능한 데이터가 없음: GNU_RELRO가 포함된 LOAD 세그먼트의 나머지 부분을 모두 차지하므로(LOAD.VirtAddr + LOAD.MemSiz == RELRO.VirtAddr + RELRO.MemSiz 의미) 쓰기 가능한 .data 또는 .bss 섹션이 후행 페이지를 공유하지 않습니다.

예

다음 예시에서는 다양한 GNU_RELRO 구성에 대해 llvm-readelf 출력을 평가하는 방법을 보여줍니다.

예시 1: 정렬되지 않은 .so 파일(규정 미준수)

이 예에서 GNU_RELRO는 후속 쓰기 가능 데이터와 RW LOAD 세그먼트를 공유하며 종료 주소는 0x4000의 배수가 아닙니다.

Type           Offset   VirtAddr           PhysAddr           FileSiz MemSiz  Flg Align
LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x0cfaf0 0x0cfaf0 R E 0x4000
LOAD           0x0cfaf0 0x00000000000dfaf0 0x00000000000dfaf0 0x02510 0x04510 RW  0x4000
GNU_RELRO      0x0cfaf0 0x00000000000dfaf0 0x00000000000dfaf0 0x01510 0x01510 R   0x1
  • 수식: (VirtAddr + MemSiz) % 0x4000 == 0
  • 결과: (0xdfaf0 + 0x01510) % 0x4000 = 0xE1000 % 0x4000 == 0x1000

0x1000가 0이 아니고 포함된 LOAD 세그먼트가 0xE1000를 지나 계속되므로(0xdfaf0 + 0x04510 = 0xE4000에서 종료) 이 .so 파일은 16KB를 준수하지 않습니다. 16KB 기기에서 링커는 RELRO 보호 범위를 16KB 페이지 경계로 반올림하여 0xDC000(이전 페이지 나누기)부터 0xE4000까지를 읽기 전용으로 표시합니다. 그러나 .so 파일의 쓰기 가능한 .data 및 .bss 섹션은 0xE1000에서 0xE4000까지의 하위 범위에 있으므로 런타임에 수정하면 세그먼테이션 오류가 발생합니다. 16KB ELF 정렬을 사용하여 앱 컴파일 섹션에 설명된 대로 이 .so 파일을 다시 빌드합니다.

예시 2: RELRO 세그먼트가 있는 정렬된 .so 파일(규정 준수)

이 예에서 GNU_RELRO 세그먼트의 끝은 16KB 페이지 경계에 있습니다.

Type           Offset   VirtAddr           PhysAddr           FileSiz MemSiz  Flg Align
LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x0cfaf0 0x0cfaf0 R E 0x4000
LOAD           0x0cfaf0 0x00000000000dfaf0 0x00000000000dfaf0 0x01510 0x03510 RW  0x4000
GNU_RELRO      0x0cfaf0 0x00000000000dfaf0 0x00000000000dfaf0 0x00510 0x00510 R   0x1
  • 수식: (VirtAddr + MemSiz) % 0x4000 == 0
  • 결과: (0xdfaf0 + 0x00510) % 0x4000 = 0xE0000 % 0x4000 == 0

나머지가 0이므로 RELRO 보호는 0xE0000 페이지 경계에서 깔끔하게 종료되어 0xE0000에서 시작하는 쓰기 가능한 데이터에 영향을 미치지 않습니다.

예 3: RELRO 세그먼트가 없는 .so 파일(규정 준수)
Type           Offset   VirtAddr           PhysAddr           FileSiz MemSiz  Flg Align
LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x000680 0x000680 R E 0x4000
LOAD           0x000680 0x0000000000004680 0x0000000000004680 0x0001d8 0x0001d8 RW  0x4000

이 .so 파일에는 GNU_RELRO 세그먼트가 없으므로 RELRO 페이지 경계 문제가 적용되지 않습니다(보안을 위해 RELRO를 사용 설정하는 것이 좋습니다).

예 4: 자체 LOAD 세그먼트의 GNU_RELRO(규정 준수)

이 예에서 GNU_RELRO는 16KB 경계에서 끝나지 않지만 동일한 LOAD 세그먼트에서 쓰기 가능한 데이터가 뒤따르지 않으므로 여전히 규정을 준수합니다.

Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x000680 0x000680 R E 0x4000
LOAD           0x000680 0x0000000000004680 0x0000000000004680 0x0001d8 0x000980 RW  0x4000
GNU_RELRO      0x000680 0x0000000000004680 0x0000000000004680 0x0001d8 0x000980 R   0x1
  • 수식: (VirtAddr + MemSiz) % 0x4000 == 0
  • 결과: (0x4680 + 0x0980) % 0x4000 = 0x5000 % 0x4000 == 0x1000

0x1000가 0이 아니지만 이 .so 파일은 여전히 16KB를 준수합니다. GNU_RELRO를 포함하는 LOAD 세그먼트(두 번째 LOAD 항목)와 비교하면 둘 다 VirtAddr 0x4680에서 시작하고 정확히 동일한 주소(0x4680 + 0x0980 = 0x5000)에서 끝납니다.

전체 LOAD 세그먼트에는 RELRO 데이터만 포함되므로 0x5000에서 0x8000까지의 하위 범위에는 쓰기 가능한 .data 또는 .bss 데이터가 없습니다(라이브러리에 RELRO가 아닌 쓰기 가능한 데이터가 없거나 후속 쓰기 가능한 LOAD 세그먼트가 0x8000 이상의 별도의 16KB 페이지 경계에서 시작됨). 링커의 읽기 전용 보호는 쓰기 가능한 데이터와 겹치지 않으므로 세그멘테이션 오류가 발생하지 않습니다.

16KB 기기 지원을 포함하여 앱 빌드

앱에서 네이티브 코드를 사용하는 경우 다음 섹션에 설명된 단계를 완료하여 앱이 16KB 기기를 지원하는지 확인하세요.

  1. 공유 라이브러리 패키징 업데이트
  2. 16KB ELF 정렬을 사용하여 앱 컴파일
  3. 코드 수정 및 런타임 문제 해결
  4. 맞춤 메모리 할당자 최적화(해당하는 경우)
  5. SDK의 16KB 지원 여부 확인

공유 라이브러리 패키징 업데이트

AGP 버전 8.5.1 이상으로 업그레이드하고 압축되지 않은 공유 라이브러리를 사용하세요.

bundletool를 사용하여 ZIP 정렬 확인

번들의 정렬을 확인하려면 다음을 사용하세요.

bundletool dump config --bundle=<my .aab>  | grep alignment

PAGE_ALIGNMENT_16K가 표시되면 번들이 16KB ZIP 정렬을 요청하는 것입니다. PAGE_ALIGNMENT_4K가 표시되면 이 AAB에서 빌드된 APK가 ZIP 파일에 4KB 정렬된 .so 파일을 포함하도록 지시합니다.

AGP 버전 8.5.1 이상

16KB 기기에서는 압축되지 않은 공유 라이브러리와 함께 제공되는 앱을 16KB zip 정렬 경계에 정렬해야 합니다. 이렇게 하려면 Android Gradle 플러그인(AGP) 버전 8.5.1 이상으로 업그레이드해야 합니다. 업그레이드 프로세스에 관한 자세한 내용은 Android Gradle 플러그인 업그레이드 어시스턴트 섹션을 참고하세요.

AGP 버전 8.5 이하

AGP를 버전 8.5.1 이상으로 업그레이드할 수 없는 경우 압축된 공유 라이브러리를 사용하도록 전환하는 방법이 있습니다. 정렬되지 않은 공유 라이브러리로 인한 앱 설치 문제를 방지하려면 앱을 패키징할 때 Gradle이 공유 라이브러리를 압축하도록 Gradle 구성을 업데이트하세요.

Groovy

build.gradle 파일에 다음 옵션을 추가합니다.

android {
  ...
  packagingOptions {
      jniLibs {
        useLegacyPackaging true
      }
  }
}

Kotlin

build.gradle.kts 파일에 다음 옵션을 추가합니다.

android {
  ...
  packagingOptions {
      jniLibs {
        useLegacyPackaging = true
      }
  }
}
AGP 버전 8.0 이하

AGP 버전이 8.0 이하인 경우 gradle.properties 파일에서 App Bundle의 압축되지 않은 네이티브 라이브러리 옵션도 사용 중지해야 합니다.

android.bundle.enableUncompressedNativeLibs=false

16KB ELF 정렬을 사용하여 앱 컴파일

16KB 기기에서는 앱을 실행하기 위해 16KB ELF 정렬을 사용하여 공유 라이브러리의 ELF 세그먼트를 올바르게 정렬해야 합니다.

게임 개발자의 경우 게임이 Unity 게임 엔진에서 실행된다면 Unity 가이드를 참고하세요. 게임이 Unreal 게임 엔진에서 실행되는 경우 Unreal 가이드를 참고하세요. 네이티브 게임 엔진의 경우 이 가이드를 계속 진행하세요.

16KB ELF 정렬을 사용하여 앱을 컴파일하려면 사용 중인 Android NDK 버전에 따라 다음 섹션 중 하나의 단계를 완료하세요.

Android NDK r28 이상

NDK 버전 r28 이상은 기본적으로 16KB 정렬로 컴파일됩니다.

Android NDK r27 이하

Android NDK 버전 r27 이하에서 16KB 정렬 공유 라이브러리 컴파일을 지원하려면 다음 링커 플래그를 사용하세요.

-Wl,-z,max-page-size=16384
-Wl,-z,common-page-size=16384

빌드 시스템 구성 파일을 업데이트하는 방법은 다음과 같습니다.

ndk-build

ndk-build를 사용하는 경우 Android.mk를 업데이트하여 16KB ELF 정렬을 사용 설정합니다.

LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384

CMake

CMake를 사용하는 경우 CMakeLists.txt를 업데이트하여 16KB ELF 정렬을 사용 설정합니다.

target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
    "-Wl,-z,max-page-size=16384"
    "-Wl,-z,common-page-size=16384"
)

코드 수정 및 런타임 문제 해결

앱이 16KB로 정렬되어 있더라도 코드에서 기기가 특정 페이지 크기를 사용한다고 가정하는 경우 앱에 오류가 발생할 수 있습니다. 이를 방지하려면 다음 단계를 완료하세요.

  1. PAGE_SIZE 상수를 참조하는 하드코딩된 종속 항목이나 기기의 페이지 크기가 4KB(4096)라고 가정하는 코드 로직의 인스턴스를 삭제합니다.

    대신 getpagesize() 또는 sysconf(_SC_PAGESIZE)를 사용하세요.

  2. mmap() 및 페이지 정렬 인수가 필요한 기타 API의 사용을 찾아 필요한 경우 대체 항목으로 바꿉니다.

앱에서 PAGE_SIZE를 기본 페이지 크기와 연결되지 않은 편리한 값으로 사용하는 경우 16KB 모드에서 사용해도 앱이 중단되지 않습니다. 그러나 이 값이 MAP_FIXED 없이 mmap를 통해 커널에 전달되면 커널은 여전히 전체 페이지를 사용하므로 일부 메모리가 낭비됩니다. 이러한 이유로 NDK r27 이상에서 16KB 모드가 사용 설정되면 PAGE_SIZE가 정의되지 않습니다.

앱이 이러한 방식으로 PAGE_SIZE를 사용하고 이 값을 커널에 직접 전달하지 않는 경우 PAGE_SIZE를 사용하는 대신 다른 목적으로 사용되며 실제 메모리 페이지를 반영하지 않는다는 것을 반영하도록 새 이름으로 새 변수를 만듭니다.

맞춤 메모리 할당자 최적화

16KB 시스템에서 운영체제가 할당하는 물리적 메모리의 최소 단위는 4KB 시스템보다 4배 더 큽니다. 커스텀 할당자가 4KB 가정을 기반으로 설계된 경우 작은 객체를 여러 16KB 페이지에 분산하고 빈 메모리를 불필요하게 유지할 수 있습니다. 이로 인해 실제 메모리 사용량(RSS)이 크게 증가하고 압축 스왑(ZRAM) 효율성이 저하될 수 있습니다.

코드가 자체 메모리 풀을 관리하는 경우 다음 권장사항을 따르세요.

1. 메모리 확보를 위해 하드 코딩된 바이트 기준점 사용하지 않기

많은 할당기는 고정된 바이트 한도를 사용하여 madvise(MADV_DONTNEED)를 통해 OS에 메모리를 다시 해제할 시점을 결정합니다(예: 활성 객체가 8KB 미만을 차지하는 경우에만 메모리 해제).

16KB 시스템에서는 단일 16바이트 라이브 객체도 전체 16KB 페이지(8KB보다 큼)를 고정합니다. 따라서 해제 기준점이 충족되지 않으며 할당기는 주변의 사용되지 않는 메모리를 커널에 반환하지 않습니다.

해야 할 일: 페이지 해제 휴리스틱에 하드코딩된 바이트 상수를 사용하지 마세요. sysconf(_SC_PAGESIZE)를 사용하여 실제 페이지 크기에 따라 런타임 시 출시 기준점을 동적으로 확장합니다.

2. 이미 사용된 페이지를 먼저 채우기(밀집 우선 할당)

할당자가 선입선출(FIFO) 또는 라운드 로빈 순서로 메모리를 할당하면 새 할당이 부분적으로 채워진 여러 16KB 페이지에 분산됩니다. 페이지의 단일 객체는 16KB를 모두 물리적 RAM에 상주하게 합니다.

해야 할 일: 비어 있거나 가볍게 사용된 페이지를 터치하기 전에 항상 가장 가득 찬(밀도가 높은) 페이지 또는 슬래브에서 새 객체를 할당합니다. 이미 더티 페이지에 새 할당을 집중하면 가볍게 사용되는 페이지가 자연스럽게 활성 객체 0개로 소모되므로 전체 16KB 페이지를 OS에 해제할 수 있습니다.

3. 메모리 풀을 16KB로 정렬하고 풀 크기를 적당하게 유지

4KB 시스템용으로 설계된 다중 페이지 스팬은 16KB 커널에서 해제되지 않은 과도한 메모리 여유 공간을 보유할 수 있습니다. 또한 16KB로 균등하게 나누어지지 않는 크기 클래스는 각 페이지 끝에서 단편화를 일으킵니다.

해야 할 일:

  • 모든 메모리 풀, 슬래브 경계, 버퍼 정렬이 런타임 페이지 크기의 정확한 배수인지 확인합니다.
  • 유휴 메모리를 가두는 지나치게 큰 슬랩을 할당하지 않도록 작은 객체 클래스의 다중 페이지 스팬 크기를 다시 평가합니다.

4. 캐시된 대형 버퍼의 실제 메모리를 즉시 해제

커스텀 할당자는 mmap 또는 munmap 시스템 호출의 오버헤드를 지불하지 않고 재사용할 수 있도록 대형 버퍼(>64KB)를 인메모리 풀에 캐시하는 경우가 많습니다. 하지만 이러한 더티 버퍼를 메모리에 보관하면 제거 타이머를 기다리는 동안 수 메가바이트의 실제 RAM이 낭비됩니다.

해야 할 일: 빠른 재사용을 위해 가상 메모리 주소 범위를 예약된 상태로 유지하되, 버퍼를 캐시에 반환할 때 즉시 madvise(..., MADV_DONTNEED) 또는 madvise(..., MADV_FREE)를 호출합니다. 운영체제는 실제 RAM을 즉시 회수하지만 앱은 재할당 없이 가상 주소를 즉시 재사용할 수 있습니다.

5. ZRAM 압축을 위해 할당 시가 아닌 해제 시 메모리를 0으로 설정

Android는 압축 스왑(ZRAM)을 사용하여 백그라운드 앱을 메모리에 유지합니다. 16KB 기기에서는 페이지에 라이브 객체가 하나라도 있으면 전체 16KB 페이지가 상주하거나 ZRAM으로 스왑됩니다. 이전에 해제된 페이지 부분에 남아 있는 가비지 데이터(예: 비활성 포인터 및 문자열)는 압축이 잘 되지 않습니다.

할당자가 메모리를 0으로 설정하는 경우(예: 보안 또는 0으로 초기화된 할당) 할당 시가 아닌 할당 해제(free()) 시 0으로 설정하는 것이 좋습니다.

  • 고정된 페이지의 비용이 더 적음: 부분적으로 채워진 16KB 페이지의 유휴 메모리는 ZRAM에서 거의 아무것도 아닌 상태로 압축되므로 고정된 페이지는 실제 스왑 공간을 낭비하지 않습니다.
  • 최소 캐시 오버헤드: free()가 호출될 때 메모리가 이미 CPU 캐시에서 핫 상태이므로 나중에 추가 캐시 부적중이 발생하지 않습니다.

추천 요약

영역 추천 예상 영향
삭제 기준점 sysconf(_SC_PAGESIZE)를 사용하여 출시 기준점을 동적으로 확장합니다. 출시 로직이 영구적으로 교착 상태에 빠지는 것을 방지합니다.
할당 순서 가장 밀도가 높은(거의 가득 찬) 페이지 또는 슬래브에서 먼저 할당합니다. 활성 객체를 함께 패킹하여 상주 메모리(RSS)를 줄입니다.
스팬 크기 조정 크기 클래스를 16KB 배수로 정렬하고 크기가 너무 큰 슬래브를 피합니다. 테일 페이지 단편화를 제거하고 메모리 여유 공간을 줄입니다.
버퍼 캐시 큰 버퍼를 캐시할 때는 즉시 madvise(MADV_DONTNEED)를 호출합니다. 빠른 가상 재사용을 유지하면서 유휴 RAM 비대화를 제거합니다.
스왑/ZRAM 할당 시가 아닌 free()에서 메모리를 0으로 설정합니다. ZRAM 압축률을 개선하여 고정된 페이지의 비용을 줄입니다.

SDK의 16KB 지원 여부 확인

많은 SDK가 16KB 페이지 크기와 호환되며, 특히 직접 빌드하거나 최신 사전 빌드를 사용하는 경우 더욱 그렇습니다. 그러나 일부 SDK 사전 빌드 또는 SDK 버전은 16KB와 호환되지 않으므로 각 SDK 제공업체의 웹사이트를 확인하여 16KB와 함께 사용할 버전을 결정해야 합니다.

16KB 환경에서 앱 테스트

16KB 기기를 지원하도록 앱을 빌드한 후에는 16KB 환경에서 앱을 테스트하여 앱에 회귀가 발생하는지 확인해야 합니다. 이를 위해서는 다음 단계를 따르세요.

  1. Android 15 SDK 이상을 설정합니다.

  2. 다음 테스트 환경 중 하나를 설정합니다.

  3. 테스트 기기를 시작한 후 다음 명령어를 실행하여 16KB 환경을 사용하고 있는지 확인합니다.

    adb shell getconf PAGE_SIZE
    

    이 명령어는 16384 값을 반환해야 합니다.

  4. 다음 zipalign 명령어를 실행하여 앱이 16KB 정렬되었는지 확인합니다. 여기서 APK_NAME는 앱의 APK 파일 이름입니다.

    zipalign -c -P 16 -v 4 APK_NAME.apk
    
  5. 특정 페이지 크기를 참조하는 코드 인스턴스를 변경하여 영향을 받을 수 있는 영역에 중점을 두고 앱을 철저히 테스트합니다.

16KB 기반 시스템 이미지로 Android Emulator 설정

Android Emulator를 사용하여 16KB 환경을 설정하려면 다음 단계를 따르세요.

  1. Android 스튜디오에서 Tools > SDK Manager를 클릭합니다.
  2. SDK Platforms 탭에서 Show Package Details를 선택한 다음 Android VanillaIceCream 이상 섹션을 펼치고 만들려는 가상 기기에 따라 다음 에뮬레이터 시스템 이미지 중 하나 또는 둘 다를 선택합니다.

    • Google API 실험용 16KB 페이지 크기 ARM 64 v8a 시스템 이미지
    • Google API 실험용 16KB 페이지 크기 Intel x86_64 Atom 시스템 이미지
    Android 스튜디오의 SDK Manager를 사용하여 16KB 에뮬레이터 시스템 이미지 다운로드
  3. Apply > OK를 클릭하여 선택한 시스템 이미지를 다운로드합니다.

  4. Android 15용 가상 기기 설정 단계를 따르고 시스템 이미지를 선택하라는 메시지가 표시되면 다운로드한 16KB 시스템 이미지를 선택합니다. 자동으로 추천되지 않는 경우 Other Images 탭에서 16KB 시스템 이미지를 찾을 수 있습니다.

    Other Images 탭에서 16KB 에뮬레이터 이미지 찾기

에뮬레이터 실행

Android Emulator와 가상 기기 설정을 완료한 후 대상 기기 메뉴 또는 명령줄에서 에뮬레이터를 실행합니다.

개발자 옵션을 사용하여 기기에서 16KB 모드 사용 설정

16KB 페이지 크기로 부팅 개발자 옵션을 전환하여 16KB 모드로 기기를 부팅합니다.

Android 15의 QPR 버전에서는 특정 기기에서 사용할 수 있는 개발자 옵션을 사용하여 기기를 16KB 모드로 부팅하고 기기 내 테스트를 실행할 수 있습니다. 개발자 옵션을 사용하기 전에 설정 > 시스템 > 소프트웨어 업데이트로 이동하여 사용 가능한 업데이트를 적용합니다.

이 개발자 옵션은 다음 기기에서 사용할 수 있습니다.

  • Pixel 8 및 8 Pro (Android 15 QPR1 이상)

  • Pixel 8a (Android 15 QPR1 이상)

  • Pixel 9, 9 Pro, 9 Pro XL (Android 15 QPR2 이상)

  • Pixel 9a (Android 16 이상)

16KB 하위 호환 모드

페이지 크기 호환 모드의 경고

페이지 크기 호환 모드의 경고

16KB 하위 호환성 옵션은 기기가 16KB 커널로 실행되는 경우에 사용할 수 있습니다. 패키지 관리자는 다음 조건이 충족되면 16KB 하위 호환성 모드로 앱을 실행합니다.

  • 앱에 LOAD 세그먼트 정렬이 4KB인 ELF 파일(확장자 .so)이 있는 경우
  • 압축된 APK에 4KB ZIP 정렬된 압축되지 않은 ELF 파일이 있는 경우

패키지 관리자가 앱에 16KB 하위 호환성 모드를 사용 설정한 경우 앱이 처음 실행될 때 16KB 하위 호환성 모드에서 실행 중이라는 경고가 표시됩니다.

16KB 하위 호환성 모드를 사용하면 일부 앱이 작동할 수 있지만, 안정성과 신뢰성을 극대화하려면 앱을 16KB로 정렬해야 합니다.

앱 정보 페이지의 고급에서 페이지 크기 호환 모드로 앱 실행 설정을 전환하여 특정 앱의 16KB 하위 호환성 모드를 사용 설정하거나 사용 중지합니다. 이 설정은 기기가 16KB 페이지 크기로 실행되는 경우에만 표시됩니다.

페이지 크기 호환 모드 설정

페이지 크기 호환 모드 설정

기기의 모든 앱에 16KB 하위 호환성을 강제로 사용 설정하려면 다음 명령어를 실행합니다.

adb shell setprop bionic.linker.16kb.app_compat.enabled true
adb shell setprop pm.16kb.app_compat.disabled false

기기의 모든 앱에 대해 16KB 하위 호환성을 강제로 사용 중지하려면 다음 단계를 따르세요.

adb shell setprop bionic.linker.16kb.app_compat.enabled false
adb shell setprop pm.16kb.app_compat.disabled true

Android 17에서는 모든 앱에 대해 16KB 하위 호환성을 강제로 사용 중지하고 호환되지 않는 바이너리를 즉시 중단할 수도 있습니다.

    adb shell setprop bionic.linker.16kb.app_compat.enabled fatal
    adb shell setprop pm.16kb.app_compat.disabled true

android:pageSizeCompat 속성을 사용 설정 또는 사용 중지로 설정하여 AndroidManifest.xml에서 특정 앱의 이전 버전 호환 모드를 사용 설정하거나 사용 중지합니다. 이 속성이 설정되면 앱이 실행될 때 이전 버전과의 호환성 모드 경고가 표시되지 않습니다.