호환성 런타임
Mac.apk는 Apple Silicon Mac에서 Android 앱을 일반 macOS 프로세스로 실행하도록 만드는 호환성 계층입니다. APK 속 DEX 코드는 JVM 바이트코드로 바꾸고, Android의 android.* API는 Java로 다시 구현하며, 네이티브 코드는 아키텍처에 맞는 로더·변환기를 거칩니다. 화면과 소리는 Cocoa·Metal·macOS 미디어 기능으로 연결합니다.
같은 CPU 명령어를 쓴다는 사실만으로는 부족하고, 앱이 기대하는 운영체제의 약속까지 맞춰야 실행됩니다. Android와 macOS는 실행 파일 형식, 시스템 라이브러리, 파일 경로, 스레드 규칙이 다릅니다. 이 차이를 메우는 코드가 이 저장소의 주인공입니다.
앱은 손님이고 Android API는 메뉴판입니다. “창을 열어 주세요”, “파일을 읽어 주세요”, “이 도형을 그려 주세요”라는 주문을 Mac의 주방이 받아 처리합니다. 손님이 쓰는 메뉴판은 그대로지만, 뒤에서 움직이는 조리 도구는 달라집니다.
문제는 메뉴 이름만 번역하면 끝나지 않는다는 데 있습니다. “차갑게”, “먼저 내어 주세요” 같은 조건도 맞춰야 합니다. 소스의 수많은 호환성 분기와 스레드 처리는 바로 이런 세부 약속을 맞추는 작업입니다.
| APK 구성 | 담긴 내용 | Mac.apk의 처리 |
|---|---|---|
| DEX | Java·Kotlin 등에서 컴파일된 Android 바이트코드 | com.macapk.dex가 JVM용 클래스 바이트를 생성 |
| 네이티브 코드 | lib/arm64-v8a/*.so, lib/armeabi-v7a/*.so 등 | ARM64는 ELF 로딩·연결, ARM32는 ARM64 코드로 재컴파일 |
| 리소스 | 바이너리 Manifest, 문자열·레이아웃·그림, resources.arsc | Manifest·리소스 파서가 읽고 프레임워크 구현에 전달 |
구현의 시작점은 runtime/src/main.cpp::main()입니다. APK 준비와 JVM 생성을 거쳐 com.macapk.loader.Boot.run()을 호출합니다. Java 쪽에서는 앱 클래스 로더와 실행 환경을 만들고 Activity 실행으로 이어집니다. 끝에는 Mac의 이벤트 루프가 창과 입력을 처리합니다.
“Java 프로젝트”라는 언어 분류 너머를 보게 됩니다. 컴파일러가 코드를 바꾸는 경계, JNI가 언어를 연결하는 경계, 로더가 파일을 메모리에 올리는 경계, 설치기가 사용자 데이터를 보존하는 경계를 실제 파일에서 찾는 것이 목표입니다.
README는 프로젝트를 Alpha로 명시합니다. “모든 Android 앱을 실행한다”는 제품으로 이해하면 곤란합니다. 어떤 기능을 구현했고 어떤 기능은 비어 있는지 읽어 가며, 완전한 플랫폼 호환성이 얼마나 넓은 문제인지 배우기에 적합합니다.
TrendShift Weekly #20의 선정 대상이며, 2026-10-23 GitHub 기준 스타는 2,537개, 포크는 80개입니다. 2026-05-19 생성된 저장소에 이 정도 관심이 모였다는 점은 눈에 띕니다. 다음은 소스 구조와 공개 기능을 바탕으로 해석한 관심 요인이며, 특정 사건이 인기 상승의 원인이라는 뜻은 아닙니다.
.app 번들, Cocoa 창, 호스트 파일 선택·미디어 제어를 한 제품으로 연결합니다.| 접근 | 앱이 실행되는 환경 | 장점과 선택 기준 |
|---|---|---|
| Mac.apk | macOS 프로세스 + 재구현한 Android API + JVM·네이티브 브리지 | Android 시스템 부팅 없이 Mac 창과 통합. 대신 API 누락·행동 차이를 프로젝트가 직접 해결해야 함 |
| Android Emulator | AVD로 구성하는 가상 Android 기기와 시스템 이미지 | 기기·API 수준을 바꾸며 앱을 개발·시험하는 목적. Mac.apk와 호환성·성능이 같다고 볼 수 없음 |
| Waydroid | GNU/Linux에서 컨테이너로 구동하는 전체 Android 시스템 | Linux의 namespaces와 Android 환경을 활용. macOS 위의 API 재구현과는 전제가 다름 |
비교 근거는 Android Emulator 공식 안내와 Waydroid 공식 문서입니다. 여기서 Mac.apk의 장점은 시스템 부팅을 생략하고 호스트 기능을 연결하는 설계입니다. 동일 앱·동일 하드웨어로 측정한 자료 없이 “항상 더 빠르다”, “메모리가 몇 배 적다”는 결론은 내릴 수 없습니다.
Mac.apk의 “네이티브 실행”은 모든 코드를 변환 없이 실행한다는 뜻이 아니라, Android 게스트 운영체제 없이 호스트에서 실행한다는 뜻으로 읽어야 합니다. ARM64 라이브러리는 원래 기계어를 주로 실행하지만 로딩·재배치·시스템 호출 처리가 필요합니다. ARM32는 ARM64로 바뀌며, DEX도 JVM용 코드로 변환됩니다.
| README 표현 | 소스에서 확인되는 범위 | 배울 점 |
|---|---|---|
| 에뮬레이터·VM 없음 | jvm_loader.cpp는 HotSpot JVM을 프로세스 안에 생성 | 게스트 OS 가상머신과 언어 실행용 JVM을 구분해야 함 |
| CPU 네이티브 속도 | ARM64 직접 실행 경로 외에 DEX 변환·ARM32 재컴파일·브리지 작업이 존재 | 실행 방식과 최종 성능 수치는 별개의 문제 |
| Mac GPU로 그림 | GLES·Vulkan·AGSL의 GPU 경로가 있으나 Canvas는 Java2D 경로도 사용 | GPU 사용 여부와 화면 전체의 GPU 렌더링을 구분 |
COMPATIBILITY.md는 2026-10-20 갱신 기준으로 71개 앱: 동작 58개, 부분 동작 10개, 미동작 3개라고 요약합니다. 이는 프로젝트의 수동 시험 집계입니다. 같은 앱의 여러 버전이 별도 행으로 나오므로 표의 행 수를 앱 수로 세면 안 됩니다.
또한 “동작” 표시 옆에도 세부 결함이 적혀 있습니다. AntennaPod는 재생 화면 문제, Mindustry는 온라인 멀티플레이 문제를 기록합니다. 숫자를 Android 생태계 전체의 성공률로 일반화하지 말고, 어떤 버전에서 어떤 동작을 시험했는지를 함께 읽어야 합니다.
“해외 메뉴 58개를 조리해 봤다”는 기록과 “그 나라의 모든 요리를 완벽하게 만든다”는 보장은 다릅니다. 호환성 표는 시험한 메뉴와 남은 문제를 적어 놓은 작업 일지에 가깝습니다.
이 저장소에는 package.json, Cargo.toml, pyproject.toml, go.mod, Package.swift, Dockerfile이 없습니다. Node 서버나 Rust 데스크톱 셸을 가정하고 읽으면 구조를 놓칩니다. 빌드 설정과 의존성 출처는 CMake 파일과 셸 스크립트, 사전 빌드 산출물의 PROVENANCE.txt에 나뉘어 기록되어 있습니다.
| 층 | 기술·실제 설정 | 역할과 확인 파일 |
|---|---|---|
| Android API | Java, 컴파일 기준 --release 21 | runtime/java/android/ 등의 재구현. 설정은 runtime/scripts/build.sh |
| DEX 변환 | 자체 Java 변환기 | DexFile·TypeInference·Translator·ClassFileWriter. ASM 의존을 가정하지 말 것 |
| JVM 호스트 | OpenJDK/HotSpot, 기본 JDK 21 | jvm_loader.cpp의 JNI_CreateJavaVM(). 선택 기본값은 debug/jdk25_select.sh |
| 네이티브 런타임 | C++ 17, Objective-C++ 17 | runtime/CMakeLists.txt. APK 준비, Cocoa·미디어·JNI 연결 |
| 번역·호환 계층 | C++ 20, Capstone, Bionic shim | runtime/translator/CMakeLists.txt. ARM 코드 처리·심볼 연결 |
| 암호 제공자 | Bouncy Castle 1.79 | runtime/thirdparty/bcprov-jdk18on-1.79.jar를 빌드 스크립트가 복사 |
JDK 25 선택 경로도 있지만 기본값은 MACAPK_JDK_DEFAULT=21입니다. JDK의 실제 버전은 디렉토리 이름이 아니라 release 파일의 JAVA_VERSION으로 검사합니다. 따라서 “JDK 25 전면 전환”으로 쓰면 부정확합니다. Java API 컴파일 기준과 실행 JVM의 선택도 서로 다른 설정입니다.
CMAKE_CXX_STANDARD 20은 사용할 C++ 문법·표준의 기준이지 컴파일러 제품의 버전이 아닙니다. 반면 bcprov...1.79.jar의 1.79는 외부 라이브러리 버전입니다. 둘을 같은 종류의 숫자로 나열하지 않는 습관이 중요합니다.| 부품 | 기술 | 실제 연결 지점 |
|---|---|---|
| 관리 화면 | SwiftUI·AppKit·Combine | launcher/MacApkLauncherApp.swift; 설치 앱 목록과 설정 |
| 앱 포장 | Swift, Finder·Dock·LaunchServices | BundleGenerator.generate()로 앱별 .app 생성 |
| 창·입력 | Cocoa·Metal·JNI | cocoa_bridge.mm, NativeCocoa.java; Android 앱 UI의 호스트 창 |
| 웹 콘텐츠 | WKWebView | runtime/java/android/webkit/WebView.java → MacApkWebViewBridge → webview_bridge.mm |
| 소리·영상 | AudioToolbox·AVFoundation·VideoToolbox, 선택적 libvpx | audio_backend.mm, video_toolbox.mm, video_vp8_decode.mm 등 |
SwiftUI는 Mac.apk의 관리 화면에 쓰입니다. 모든 Android View를 SwiftUI 컴포넌트로 바꾼다는 뜻은 아닙니다. Android 앱 자체의 화면은 프레임워크·Canvas·Surface·그래픽 브리지를 통해 그려집니다. 이 구분을 해야 UI 오류가 생겼을 때 올바른 파일부터 찾을 수 있습니다.
| 입력 | 호스트 경로 | 의존성 근거 |
|---|---|---|
| OpenGL ES | GLES 브리지 → ANGLE → Metal 백엔드 등 | thirdparty/angle-prebuilt/PROVENANCE.txt: ANGLE 커밋 e44b533 |
| Vulkan | Bionic Vulkan 호환 코드 → MoltenVK 직접 연결 → Metal | 같은 출처 파일: MoltenVK 커밋 1c96d3d |
| AGSL·효과 | Skia Ganesh Metal → 결과 읽기 → Java2D Canvas 합성 | thirdparty/skia-prebuilt/PROVENANCE.txt: Skiko 0.150.2, Skia m150-a3f509bf52 |
| 일반 Canvas | Java2D를 사용하는 구현 | runtime/java/android/graphics/Canvas.java와 BUILDING.md |
ANGLE은 그래픽 API 번역 계층이며, MoltenVK는 Vulkan의 일부 기능을 Metal 위에 제공합니다. 각각의 상류 프로젝트가 지원하는 범위와 Mac.apk가 실제 연결한 범위는 같지 않을 수 있습니다. API 이름이 보인다고 모든 확장 기능의 지원까지 결론 내리지 마세요.
Capstone·libpng·libvpx는 Homebrew 경로에서 찾지만 CMake에 정확한 패치 버전 고정은 없습니다. Swift 컴파일러 버전도 고정 매니페스트로 선언하지 않습니다. 따라서 표에 임의의 최신 버전을 채우지 않습니다. 고정된 것과 환경에 맡긴 것을 나눠 기록하는 것도 의존성 분석입니다.
CMakeLists.txt가 선언합니다. 런타임과 translator는 별도 대상으로 빌드됩니다.scripts/build_launcher.sh가 swiftc -O -parse-as-library를 호출합니다. 대상은 arm64-apple-macos26.0입니다.scripts/stage_jre.sh가 jlink로 모듈을 묶습니다. 최종 사용자에게 개발용 JDK 전체가 필요한 구조는 아닙니다.android-16.0.0_r2 태그를 사용합니다. scripts/build_framework_res.sh의 인자 없는 기본값은 API 30이므로 구분해야 합니다..github/workflows/cla.yml은 CLA 서명 처리이며 contributor-assistant/github-action@v2.6.1을 사용합니다. 여기에 빌드·호환성 CI가 있다고 읽으면 안 됩니다.debug/AgslRenderingSmoke.java와 debug/LayerColorFilterTest.java는 픽셀 결과를 검사합니다. 전체 회귀 시험과 APK 모음은 BUILDING.md 기준 비공개입니다.의존성 목록만 외우기보다, 각 기술이 어느 경계를 해결하고 무엇을 아직 보장하지 않는지 연결해 읽는 것이 핵심입니다.
구조도는 책임을 구분한 그림입니다. 모든 코드가 위에서 아래로 한 번씩만 실행되는 것은 아닙니다. 앱의 Java 코드가 JNI로 네이티브 함수를 부르고, 그 네이티브 코드가 다시 Java 메서드를 호출하기도 합니다. 리소스는 화면 생성 중에도 읽힙니다.
runtime/java/com/macapk/dex/DexConverter.java는 변환기의 진입점입니다. open(byte[]... dexes)로 세션을 만들고, Session.classBytes()로 한 클래스를, convertAll()로 전체를 변환합니다. 세션은 여러 DEX의 클래스를 모아 부모 타입과 중첩 클래스 관계를 함께 판단합니다.
왜 이름만 바꿔서는 안 될까요? DEX는 레지스터 기반이고, JVM 명령은 피연산자 스택을 사용하기 때문입니다. 같은 덧셈도 값을 어디에서 꺼내고 어디에 남길지 다시 표현해야 합니다. 아래는 개념 설명용 대응이며 실제 변환 출력의 복사본은 아닙니다.
# DEX 개념: v2에 v0 + v1을 저장
add-int v2, v0, v1
# JVM 개념: 두 값을 스택에 올려 더한 뒤 로컬 슬롯에 저장
iload_0
iload_1
iadd
istore_2
Session은 BATCH=512, MAX_THREADS=8로 전체 변환의 배치와 병렬도를 제한합니다. 출력 클래스를 전부 쥐고 있기보다 forEach()의 소비자에게 넘기는 통로도 둡니다. 컴파일러를 공부하면서 메모리 사용과 동시성까지 함께 볼 수 있는 부분입니다.
실패 처리도 읽을 만합니다. Translator.emitUnsupportedBody()는 처리하지 못하는 메서드에 UnsupportedOperationException을 던지는 몸체를 만들 수 있습니다. “클래스를 로드했다”와 “그 안의 모든 메서드가 올바르게 작동한다”는 서로 다른 성공 조건입니다.
| 경로 | 핵심 파일·함수 | 해결하는 문제 |
|---|---|---|
| ARM64 | arm64_pack.cpp, arm64_macho_emit.cpp, arm64_native.cpp::__macapk_arm64_native_load_from_path() | 작은 Mach-O wrapper로 진입하고 ELF를 메모리에 올려 재배치·심볼 연결. 호스트와 다른 동작은 shim·패치로 처리 |
| ARM32 | runtime/translator/src/main.cpp의 함수 발견 → arm32rc::build_module() → macho_link.cpp | ARM·Thumb 명령을 ARM64 코드로 생성. 게스트 레지스터·포인터 폭·호출 규약 차이도 연결 |
| 공통 경계 | bionic_shim.cpp와 관련 분리 파일 | Android Bionic의 libc·pthread·JNI·시스템 기능을 호스트와 연결 |
주의해서 읽을 오래된 설명: runtime/translator/CMakeLists.txt의 첫 주석에는 LLVM IR과 외부 clang 경로가 남아 있습니다. 그러나 실제 runtime/translator/src/main.cpp의 ARM32 분기는 arm32rc가 유일한 코드 생성기라고 명시하고, runtime/translator/README.md도 직접 Mach-O 생성을 설명합니다. 빌드 파일의 오래된 주석보다 현재 분기와 호출을 우선해야 합니다.
runtime/java/android/webkit/WebView.java는 Android 앱에 익숙한 인터페이스를 제공합니다. 내부 MacApkWebViewBridge.nativeCreate()가 JNI 경계를 넘어가면 runtime/src/webview_bridge.mm::NativeCreate()가 WKWebViewConfiguration을 만들고 호스트 WebKit 뷰를 준비합니다.
// MacApkWebViewBridge.java의 실제 선언 일부
static native void nativeCreate(int viewId, String pkg,
String userAgent, boolean jsEnabled);
static native void nativeDestroy(int viewId);
네이티브 구현은 dispatch_async(dispatch_get_main_queue(), ...)로 AppKit 주 스레드에 작업을 보냅니다. 반대쪽 이벤트는 Java의 Handler로 WebView의 UI 스레드에 전달합니다. API 결과만 맞추는 어댑터가 아니라 실행 스레드의 규칙까지 맞추는 어댑터인 셈입니다.
통역사가 말의 뜻뿐 아니라 발언 순서도 관리한다고 생각하면 됩니다. “페이지를 열었다”는 통지를 엉뚱한 스레드에서 전달하면, 번역한 문장이 맞아도 UI 상태가 동시에 바뀌어 문제가 생길 수 있습니다.
runtime/src/apk_unpacker.cpp::CacheIsFresh()는 .source-mtime에 적힌 APK 수정 시각, ABI 선택, JAVA_ID를 확인합니다. classes.jar의 존재만 보지 않고, DEX가 없는 앱이나 지연 변환 경로도 따로 처리합니다. 불완전한 결과를 정상 캐시로 오인하지 않으려는 설계입니다. 다만 현재 JAVA_ID를 구하지 못하면 경고를 남기고 식별값 비교를 생략합니다.
runtime/scripts/build_ids.py는 변환기와 프레임워크의 빌드 식별값을 분리합니다. 프레임워크 메서드의 내부 구현을 고친 것과 부모 타입·인터페이스 구조를 고친 것은 변환 결과에 미치는 영향이 다릅니다. JAVA_ID는 변환기, 클래스 형태, JDK 버전에 기반하고, NATIVE_ID는 translator·Bionic shim 산출물의 캐시를 구분합니다.
# 소스의 책임을 요약한 개념식: 실제 해시 문자열은 생략
CONVERTER_ID = 변환기 바이트코드 + 변환 호출 방식
FRAMEWORK_ID = 클래스 이름·접근 플래그·부모·인터페이스 + JDK 버전
JAVA_ID = f(CONVERTER_ID, FRAMEWORK_ID)
NATIVE_ID = 네이티브 변환기 + shim 내용
# “파일이 있다”만으로 재사용하지 않는다
DEX 캐시 재사용(현재 식별값을 알 때)
= APK 조건 일치 + ABI 조건 일치 + 변환 식별값 일치
선택 기능도 놓치지 마세요. LazyDexEnabled()에서 MACAPK_LAZY_DEX가 없으면 false입니다. 지연 변환을 기본이라고 설명하면 틀립니다. 다만 전체 변환이 실패해 lazydex 상태로 대체되는 예외 경로도 있습니다. runtime/java/dalvik/system/LazyDexSource.java는 필요해진 클래스만 변환하고 없는 클래스의 조회 결과도 캐시합니다.
launcher/AppStubMain.swift::main()은 앱별 작은 실행 파일에서 공용 런타임으로 execv()합니다. 실행 파일을 다른 프로그램으로 바꾸는 방식이므로, 앱마다 JVM·그래픽 라이브러리 전체를 복제할 필요가 줄어듭니다. BundleGenerator.generate()는 작은 stub과 앱 정보로 .app을 구성합니다.
각 이동은 같은 디렉토리의 이름 변경을 활용하지만, 설치 전체가 단일 원자 연산은 아닙니다. 여러 이동 사이에 중단될 수 있어서 recoverInterruptedSwap()가 필요합니다. 이전 데이터를 인계하지 못했으면 .previous를 삭제하지 않고 다음 시도를 위해 보존합니다. 현재 앱 데이터는 번들 밖의 ~/Library/Application Support/MacApk/에 저장됩니다. 교체 과정은 이를 가리키는 Contents/android 링크나 이전 버전의 내부 데이터 폴더도 보존합니다.
이 아키텍처에서 재사용할 만한 생각은 “무엇을 공유하고, 어떤 조건에서 다시 만들며, 실패하면 무엇을 남길까”를 명시하는 습관입니다.
입문자는 먼저 WebView 어댑터와 설치 상태 전이를 읽고, 그다음 DEX 변환기로 들어가세요. 처음부터 ARM64 로더 전체를 따라가면 포인터·메모리 매핑·재배치·신호 처리 문제가 한꺼번에 등장합니다.
| 관찰한 문제 | 먼저 읽을 곳 | 추적할 질문 |
|---|---|---|
| 설치·업데이트 오류 | launcher/InstallCore.swift, BundleGenerator.swift | 어느 상태까지 진행했나? 기존 앱과 데이터가 어디에 남았나? |
| 클래스 로딩 오류 | AppClassLoader.java, runtime/java/com/macapk/dex/ | 클래스가 없나, 변환이 실패했나, 부모 클래스와 충돌했나? |
| 문자열·레이아웃 오류 | ResourceIndex.java, ArscReader.java | 리소스 ID와 화면·언어·테마 조건을 어떻게 골랐나? |
| 웹 화면 오류 | runtime/java/android/webkit/, webview_bridge.mm | Java 이벤트와 WKWebView 이벤트 사이에서 무엇이 달라졌나? |
| 네이티브 로딩 오류 | AppClassLoader.findLibrary(), runtime/translator/ | ABI 선택, 캐시 정체성, 필요한 심볼 중 무엇이 문제인가? |
ResourceIndex는 설치된 APK와 split APK의 리소스 테이블·ZIP 항목을 다룹니다. 현재 경로를 “apktool로 모든 XML을 풀어 놓고 폴더를 읽는다”고만 설명하면 오래된 구조를 옮기게 됩니다. ArscReader·BinaryXmlParser와 실제 리소스 선택 코드를 함께 읽어야 합니다.
커밋 5a503e1의 확장자별 물리 줄 수이며, 파일 끝에 개행이 없는 마지막 줄도 1줄로 셉니다. 다음 표는 경로 구성요소가 thirdparty인 폴더를 모두 제외한 값입니다. 빈 줄·주석·생성 코드·API 스텁·JDK 패치도 포함하므로, 순수한 자체 구현량이나 완성도를 뜻하지 않습니다.
| 구분 | 파일 수 | 물리 줄 수 |
|---|---|---|
| Java | 3,060 | 583,719줄 · 약 58.4만 |
| C++ | 49 | 159,866줄 · 약 16.0만 |
| Objective-C++ | 22 | 30,222줄 |
| Swift | 7 | 18,551줄 |
| 헤더 | 38 | 14,096줄 · .h·.hpp |
| Shell | 30 | 7,432줄 |
| Python | 7 | 1,885줄 |
| C | 2 | 55줄 |
| 총계 | 3,215 | 815,826줄 · 약 81.6만 |
thirdparty 경로까지 포함하면 같은 확장자 범위에서 3,246개 파일, 840,519줄입니다. CMake·JSON·문서·바이너리는 이 집계에서 제외했습니다. 특히 사전 빌드 라이브러리의 원본 구현량은 바이너리 크기만으로 환산할 수 없습니다.
가장 넓은 Java 코드 영역은 runtime/java/의 578,305줄입니다. 하지만 처음부터 모두 읽을 필요는 없습니다. 증상 하나 → 진입 함수 하나 → 관련 인터페이스 하나로 좁히는 것이 이 규모의 저장소를 읽는 현실적인 방법입니다.
먼저 runtime/java/com/google/firebase/analytics/FirebaseAnalytics.java를 보세요. logEvent()는 공개 메서드로 존재하지만 이벤트를 서버로 전송하지 않습니다. 일부 SDK를 멈추지 않게 하려고 호출 표면만 유지하는 스텁을 제공하는 사례입니다.
// FirebaseAnalytics.java의 실제 구현 일부
public void setUserProperty(String name, String value) {}
public void setUserId(String id) {}
public void setAnalyticsCollectionEnabled(boolean enabled) {}
앱이 이 메서드를 찾을 수 있다는 사실과 Firebase 분석 서비스가 실제 작동한다는 사실은 다릅니다. 같은 기준으로 Android API 구현도 읽어 보세요. 빈 몸체, 고정 반환값, 호스트 브리지 호출, 완전한 상태 관리 중 어느 유형인지 구분하면 호환성의 깊이가 보입니다.
각 메서드에 대해 인자와 반환형, 바뀌는 메모리 상태, 파일·네트워크·UI 부작용을 적습니다. 빈 메서드와 WKWebView로 연결되는 메서드를 비교하면 API가 존재한다는 말의 한계를 설명할 수 있습니다.
AppClassLoader.findClass()는 클래스 이름의 슬래시를 점으로 정규화하고, 필요한 재작성과 정의를 거칩니다. 지연 DEX 경로에서도 ClassFileRewriter.maybeRewrite()를 통과합니다. 변환 방식이 달라져도 공통으로 코드를 보정하는 지점을 유지하는 설계를 볼 수 있습니다.
findLibrary()는 System.loadLibrary()에 대응하는 네이티브 라이브러리 위치를 찾습니다. 따라서 ClassNotFoundException과 네이티브 라이브러리 로딩 오류는 서로 다른 질문으로 출발해야 합니다. 클래스 파일과 .so를 같은 탐색 경로라고 생각하지 마세요.
findClass()에서 JAR·지연 DEX·상위 구현으로 갈라지는 분기를 종이에 그립니다.runtime/java/com/macapk/dex/TypeInference.java는 각 명령 진입 시점의 레지스터 타입을 추론합니다. 한 명령의 결과를 다음 명령으로 전달하고, 여러 경로가 합쳐지는 곳에서는 상태를 병합합니다. 더 이상 상태가 바뀌지 않을 때까지 반복하는 고정점 계산입니다.
특히 예외 경로를 보세요. 명령 실행 도중 실패할 수 있으므로 예외 처리기로 전달할 상태는 그 명령이 완성된 뒤의 결과가 아닙니다. 소스 주석과 상태 전파 코드는 명령 실행 전 상태를 강조합니다. 이것이 단순 opcode 치환보다 변환기가 훨씬 복잡한 이유입니다.
runtime/translator/src/elf64_reader.cpp와 arm64_native.cpp를 연결해 읽으면 파일의 바이트가 메모리의 실행 가능한 영역으로 바뀌는 과정을 볼 수 있습니다. 로더는 “파일을 연다”에서 끝나지 않습니다. 영역을 배치하고, 주소를 고치고, 외부 심볼을 찾고, 초기화 함수를 실행해야 합니다.
ARM32 경로는 포인터 폭이 추가 난점입니다. 32비트 게스트가 생각하는 주소와 64비트 호스트의 실제 포인터를 연결해야 합니다. runtime/translator/src/arm32rc/regmap.cpp는 게스트 레지스터와 호스트 레지스터의 대응을 다룹니다. 처음에는 코드 생성 세부보다 입력 상태·출력 상태·보존할 레지스터만 표로 정리해도 충분합니다.
ELF 로딩은 압축 상자에서 물건을 꺼내는 것보다 이사에 가깝습니다. 새 방에 가구를 놓은 뒤, 예전 방 번호를 가리키던 주소록까지 새 주소에 맞춰 고쳐야 합니다. 그 주소 보정이 재배치입니다.
실습 아이디어: 동적 라이브러리를 설명하는 카드에 “CPU 아키텍처, 파일 형식, 외부 심볼, 초기화 함수” 네 항목을 적고 ELF와 Mach-O의 차이를 정리합니다. 실제 바이너리를 실행하지 않아도 로더의 책임을 공부할 수 있습니다.
MacApkSkia.java의 GpuEngine은 Metal 장치·큐와 Skia 컨텍스트를 만듭니다. 렌더 타깃 풀은 TARGET_CACHE_COUNT=8, TARGET_CACHE_BYTES=64L * 1024 * 1024로 제한합니다. target()에서 오래된 타깃을 내보내고, finish()에서 너무 큰 일회성 타깃의 보유를 정리합니다.
BUILDING.md는 셰이더·효과의 GPU 결과를 Java2D Canvas용으로 다시 읽는다고 설명합니다. 빠른 GPU 연산 뒤에도 결과를 CPU 쪽으로 가져오는 readback과 동기화 비용이 남습니다. 일반 Canvas 전체를 Metal로 옮겼다는 설명과 구분해야 합니다.
AgslRenderingSmoke.transparentTiles()에서 투명한 결과가 이전 프레임의 불투명 픽셀을 남기지 않는지 어떻게 검사하는지 읽습니다.BundleGenerator.generate()의 정상 경로와 recoverInterruptedSwap()의 복구 경로를 나란히 읽으세요. 새 번들을 준비하는 책임과 기존 데이터를 인계하는 책임을 나눕니다. InstallPipeline.Hooks에 클로저로 번들 동작을 연결하는 부분에서는 UI와 공통 설치 로직을 분리하는 방법도 보입니다.
학습할 핵심은 Swift 문법보다 실패했을 때 지우면 안 되는 데이터입니다. “오류를 잡았다”는 것만으로 복구가 되지는 않습니다. 사용자의 유일한 데이터가 있는 폴더를 보존하고, 다음 실행에서 그 상태를 알아볼 수 있어야 합니다.
기능이 한 번 성공하는 코드를 넘어, 실패해도 진단하고 다시 시도할 수 있는 코드를 읽는 것이 이 저장소의 가장 실용적인 공부입니다.
| 항목 | 요구사항 | 근거와 해석 |
|---|---|---|
| CPU | Apple Silicon Mac | README 기준. Intel Mac 지원 없음 |
| 운영체제 | macOS 26.4 이상 | README·BUILDING.md·launcher/Info.plist가 일치. macOS 27은 README 기준 v1.0.1702부터 지원 |
| 설치 공간 | Mac.apk 약 280 MB | README의 대략값. 배포 파일·런타임 구성에 따라 달라질 수 있음 |
| 앱별 공간 | APK 크기의 약 1.5~3배 | README 기준 추정. 앱 데이터와 장기 캐시 증가는 별도 고려 |
| 메모리 | 공식 최소 RAM 수치 미기재 | 앱과 변환 작업에 따라 다름. 코드의 힙 설정을 제품 최소 RAM으로 읽으면 안 됨 |
| 최초 준비 | 앱 크기에 따라 지연 가능 | README는 큰 앱에서 최초 실행 준비가 최대 약 1분이라고 안내. 보편적 시간 보장은 아님 |
runtime/src/jvm_loader.cpp에는 -Xmx1g 설정이 있습니다. 이것은 Java 힙의 상한 설정이며, 프로세스 전체 메모리나 Mac의 최소 RAM이 1 GB라는 뜻이 아닙니다. 네이티브 코드·그래픽 메모리·JIT·공유 라이브러리 등은 별도 영역을 사용합니다.
BUILDING.md는 ARM64 경로에 com.apple.security.custom-x18-abi-toggle entitlement가 필요하고, 이 기능 때문에 26.4를 최소로 둔다고 설명합니다. runtime/scripts/jit-entitlements.plist에 해당 키가 있으며, arm64_native.cpp::macapk_x18_capability_ok()는 실제 보존 능력을 검사하고 조건이 맞지 않으면 로딩을 거부합니다.
CMake의 CMAKE_OSX_DEPLOYMENT_TARGET과 Swift 컴파일 대상에는 26.0이 보입니다. 이 값만 보고 제품이 26.0부터 동작한다고 결론 내리면 안 됩니다. 번들의 최소 OS 선언과 실행 중 기능 검사가 더 높은 조건을 요구합니다.
BUILDING.md입니다.aapt2가 필요합니다. 문서는 Android SDK build-tools 36.0.0을 사용한 API 36 절차를 제시합니다.BUILDING.md는 빌드만으로 공유 런타임이 갱신되지 않는다고 명시합니다.코드를 읽는 데 필요한 환경과 APK를 실제로 실행하는 데 필요한 환경은 다르므로, 입문 학습은 읽기·상태도 작성만으로도 시작할 수 있습니다.
README는 Android 앱이 macOS 사용자 계정 권한의 일반 네이티브 코드로 실행되며, 휴대전화식 앱 샌드박스가 없다고 명시합니다. 파일 선택·카메라 등에 macOS 권한 처리가 있더라도, 그것이 APK 전체를 Android 권한 체계로 격리한다는 뜻은 아닙니다.
알 수 없는 APK를 시험 실행하는 일과 소스를 읽는 일은 위험 수준이 다릅니다. 아래 실습은 설치나 APK 실행 없이도 학습 결과를 만들 수 있도록 구성했습니다.
다음 과제는 커밋 5a503e1의 파일을 읽고 표·그림·설계안을 만드는 방식입니다. 명령 예시는 저장소 루트 기준의 읽기 전용 탐색입니다. 빌드·설치·APK 실행은 요구하지 않습니다. 각 과제의 결과에는 근거 파일과 함수 이름을 함께 남기세요.
목표: DEX, ARM64·ARM32 라이브러리, 리소스가 각각 어떤 경로로 처리되는지 구분합니다.
읽을 파일: runtime/src/main.cpp, apk_unpacker.cpp, runtime/java/com/macapk/loader/Boot.java, DexConverter.java.
진행: APK 입력부터 Boot.run()까지 함수·파일 이름으로 화살표를 그리고, DEX·네이티브·리소스에 각각 색 대신 번호를 붙입니다.
완료 기준: “Manifest를 읽는 일”, “클래스를 변환하는 일”, “네이티브 라이브러리를 로딩하는 일”이 왜 서로 다른지 한 문장씩 설명한 A4 한 장을 만듭니다.
# 진입점과 Java 쪽 인계 지점 찾기
rg -n 'int main|Boot.run|CallStaticVoidMethod' runtime/src/main.cpp
rg -n 'public static void run' runtime/java/com/macapk/loader/Boot.java
rg -n 'classBytes|convertAll' runtime/java/com/macapk/dex/DexConverter.java
목표: 메서드 존재와 기능 구현을 구분합니다.
읽을 파일: FirebaseAnalytics.java, runtime/java/android/webkit/WebView.java, MacApkWebViewBridge.java, webview_bridge.mm.
진행: 빈 메서드 하나와 호스트로 연결되는 메서드 하나를 고릅니다. “입력·상태 변화·외부 동작·콜백 스레드·실패 처리” 다섯 열의 비교표를 만듭니다.
완료 기준: “클래스가 있으므로 서비스 전체가 지원된다”는 주장을 반박할 수 있는 코드 근거 두 개와, 지원 여부를 추가 확인해야 할 질문 두 개를 적습니다.
목표: “언제 재사용하고 언제 다시 만드는가”를 결정 규칙으로 바꿉니다.
읽을 파일: apk_unpacker.cpp::CacheIsFresh(), runtime/scripts/build_ids.py, AppClassLoader.java의 네이티브 정체성 검사.
진행: APK 변경, ABI 강제 선택 변경, 변환기 변경, 프레임워크 부모 타입 변경, 프레임워크 메서드 몸체 변경을 각각 한 행으로 만듭니다. DEX 캐시와 네이티브 캐시를 별도 열로 판단합니다.
완료 기준: 각 셀에 “재생성·재사용·추가 조건 확인”과 근거를 적습니다. 수정한 파일이 있다는 이유만으로 모든 캐시를 지우는 답은 피하세요.
# 실행이 아니라 판단 근거가 있는 줄을 찾는 명령
rg -n 'CacheIsFresh|EffectiveAbiSelector|GetBuildIdentity' runtime/src/apk_unpacker.cpp
rg -n 'CONVERTER_ID|FRAMEWORK_ID|JAVA_ID|NATIVE_ID' runtime/scripts/build_ids.py
rg -n 'nativeIdMatches|findLibrary' runtime/java/com/macapk/loader/AppClassLoader.java
목표: 파일 교체와 데이터 보존을 상태 머신으로 설명합니다.
읽을 함수: BundleGenerator.generate(), recoverInterruptedSwap(), adoptAndroidDir().
진행: final·staging·previous의 존재 여부를 상태로 적습니다. 이전 번들 이동 직후, 새 번들 승격 직후, 데이터 인계 실패 직후에 중단됐다고 각각 가정합니다.
완료 기준: 세 상황마다 다음 실행이 무엇을 복구하는지, 어느 폴더를 삭제하면 안 되는지 적습니다. 실제 파일을 이동하거나 삭제하지 말고 상태표로 검토합니다.
| 중단 시점 | 남은 상태 | 코드에서 찾아야 할 답 |
|---|---|---|
| 이전 번들 이동 뒤 | previous 존재, final 없음 | 이전 앱을 원래 위치로 돌리는가? |
| 새 번들 승격 뒤 | previous와 final 존재 | 이전 데이터가 어디로 인계되는가? |
| 데이터 인계 실패 | 이전 데이터가 아직 previous에 있음 | 삭제를 막고 다음 시도를 남기는가? |
목표: 화면이 대충 비슷하다는 평가를 구체적인 검사 조건으로 바꿉니다.
읽을 파일: debug/LayerColorFilterTest.java, debug/AgslRenderingSmoke.java, runtime/java/android/graphics/Canvas.java.
진행: transparentTiles()와 레이어 색 필터 검사 중 하나를 고릅니다. 입력 크기·색·변환 행렬, 검사 픽셀 좌표, 기대 ARGB 값 또는 허용 오차를 표로 옮깁니다.
완료 기준: 알파가 0인 입력 재사용, 겹친 레이어의 필터 순서, 클립 경계 중 하나에 대해 새 회귀 검사 설계안을 작성합니다. 테스트 코드를 실행하지 않아도 조건과 기대값을 검토할 수 있어야 합니다.
각 과제 끝에 파일 경로, 함수 이름, 확인한 조건, 아직 모르는 질문을 기록하세요. 막연한 기능 소개보다 다음에 같은 문제를 만났을 때 다시 쓸 수 있는 자료가 됩니다.
Java의 클래스·인터페이스·예외와 파일 읽기를 알고 있다면 다음 순서로 접근할 수 있습니다. 주당 3~5시간을 가정한 학습 제안이며, 저장소 전체를 여섯 주 만에 이해한다는 일정은 아닙니다. 매주 하나의 개념과 하나의 결과물만 가져가는 편이 좋습니다.
| 주차 | 핵심 개념과 읽을 코드 | 주간 결과물 |
|---|---|---|
| 1주차 | APK·DEX·Manifest·리소스. main.cpp의 Boot.run() 호출과 Manifest.java의 역할 구분 | APK 구성과 실행 경로를 잇는 한 장의 지도 |
| 2주차 | Java 클래스 로더·스텁·어댑터. AppClassLoader, FirebaseAnalytics, WebView | API 세 개의 입력·상태·부작용·실패 처리 비교표 |
| 3주차 | JNI·UI 스레드·이벤트 루프. MacApkWebViewBridge → NativeCreate() → Java 이벤트 | 호출 방향과 실행 스레드를 표시한 왕복 시퀀스도 |
| 4주차 | 바이트코드·제어 흐름·타입 추론. DexConverter, TypeInference, Translator | 작은 분기·예외 코드의 레지스터 상태표와 JVM 대응 |
| 5주차 | ELF·Mach-O·ABI·재배치. arm64_pack, arm64_native, arm32rc_driver | ARM64 직접 로딩과 ARM32 재컴파일의 책임 비교표 |
| 6주차 | 캐시 정체성·설치 복구·픽셀 회귀. build_ids.py, BundleGenerator, LayerColorFilterTest | 무효화 결정표 + 설치 중단 상태표 + 회귀 검사 설계 |
보통은 Java에서 네이티브로 들어가는 화살표만 그립니다. 그러나 UI는 “페이지 열기 요청 → 호스트 처리 → 페이지 완료 이벤트 → 앱 콜백”이라는 왕복입니다. MacApkWebViewBridge의 이벤트 코드와 Handler 전달까지 포함해야 비동기 API의 약속을 설명할 수 있습니다.
위 그림은 스레드 경계를 익히기 위한 개념도입니다. 실제 이벤트별 전달 규칙은 MacApkWebViewBridge.java에 따로 있습니다. JavaScript 인터페이스 호출은 전용 백그라운드 큐를 사용하므로 모든 콜백을 같은 UI 스레드라고 묶으면 안 됩니다.
처음에는 정수 덧셈·조건 분기·반환만 대상으로 삼으세요. 그다음 객체 생성과 예외를 더합니다. ARM 네이티브 쪽도 정수 계산과 함수 호출부터 시작하고, 신호·스레드·SIMD는 뒤로 미룹니다. 읽는 범위를 좁히면 변환 전후 상태를 끝까지 확인할 수 있습니다.
한 함수를 입력부터 출력까지 정확히 설명하는 경험이, 수십만 줄을 훑고 이름만 기억하는 것보다 다음 단계로 이어집니다.
“이 함수는 무엇을 받아 무엇을 돌려주는가”, “누가 소유하고 언제 해제하는가”, “어느 스레드에서 실행되는가”, “실패하면 무엇이 남는가”를 적어 보세요. 세부 구현을 몰라도 다음에 찾아볼 문서와 코드가 선명해집니다.
| 용어 | 쉬운 설명 |
|---|---|
| APK | Android 앱의 코드·리소스·설치 정보를 담은 배포 묶음. ZIP 구조를 사용하며, Mac.apk는 안의 서로 다른 짐을 별도 경로로 처리합니다. |
| Split APK | 기본 앱과 CPU·언어·화면별 구성을 여러 APK로 나눈 것. 기본 파일 하나만 읽어서는 필요한 코드·리소스를 놓칠 수 있습니다. |
| DEX | Android용 클래스·메서드·명령을 담은 바이트코드 형식. Java 소스나 JVM .class와 같은 파일은 아닙니다. |
| JVM·HotSpot | JVM은 Java 바이트코드 실행 규칙을 제공하는 가상 머신, HotSpot은 그 구현 중 하나입니다. Mac.apk는 이를 프로세스 안에서 사용합니다. |
| JIT | 프로그램 실행 중 자주 쓰는 코드를 기계어로 컴파일하는 방식. 게스트 운영체제를 부팅하는 가상화와는 다른 층의 기술입니다. |
| 호환성 계층 | 앱이 기대하는 다른 플랫폼의 규칙을 현재 플랫폼에서 재현하는 코드. 함수 표면뿐 아니라 상태·오류·순서도 중요합니다. |
| 스텁·no-op | 연결에 필요한 모양만 제공하거나 아무 동작도 하지 않는 구현. 호출 성공만으로 실제 서비스가 구현됐다고 볼 수 없습니다. |
| JNI | Java 코드와 C·C++ 같은 네이티브 코드를 연결하는 인터페이스. 이름·인자·반환형·스레드 규칙이 맞아야 합니다. |
| ISA | CPU가 이해하는 명령어 집합. ARM32와 ARM64는 명령과 레지스터 체계가 다릅니다. |
| ABI | 이미 컴파일된 코드끼리 연결될 때 지키는 규칙. 인자 전달, 데이터 배치, 보존 레지스터 등이 포함됩니다. |
| ELF·Mach-O | 실행 파일·공유 라이브러리의 형식. Android 네이티브 라이브러리는 ELF를, macOS는 Mach-O를 사용합니다. |
| 동적 로더 | 라이브러리를 메모리에 올리고 필요한 주소·함수를 연결하는 부품. 파일 읽기보다 훨씬 많은 책임을 가집니다. |
| 심볼·재배치 | 심볼은 함수·데이터를 찾는 이름표입니다. 재배치는 실제 배치 위치에 맞게 코드·데이터의 주소 참조를 고치는 작업입니다. |
| Bionic shim | Android C 라이브러리 Bionic을 기대하는 호출을 호스트 쪽 기능과 연결하는 호환 코드입니다. |
| 트램펄린 | 실제 함수로 실행을 넘기는 작은 코드 조각. 호출 규칙이나 주소 경계를 연결하는 데 쓰입니다. |
| ARM·Thumb | ARM32 코드에서 사용하는 명령 인코딩 방식. 같은 바이트도 올바른 모드와 시작 위치로 해석해야 합니다. |
| TLS | 여기서는 스레드별 저장 공간인 Thread-Local Storage. HTTPS의 암호 통신 규약 TLS와 문맥이 다릅니다. |
| Handler·Looper | Android의 메시지를 특정 스레드 큐에 넣고 처리하는 도구. UI 콜백의 순서를 맞출 때 중요합니다. |
| ANGLE | OpenGL ES 호출을 호스트가 지원하는 그래픽 API로 옮기는 계층. Mac.apk의 GLES 경로에서 사용됩니다. |
| MoltenVK | Vulkan의 일부 기능을 Apple Metal 위에서 제공하는 구현. Vulkan 전체 기능과 무조건 같다고 가정하면 안 됩니다. |
| Skia·AGSL | Skia는 그래픽 엔진, AGSL은 Android의 런타임 셰이더 언어입니다. 이 저장소는 관련 효과를 Metal GPU 경로로 연결합니다. |
| Readback | GPU의 렌더 결과를 CPU 쪽 메모리로 가져오는 작업. 전송량과 완료 대기로 비용이 발생할 수 있습니다. |
| LRU | 오랫동안 사용하지 않은 항목부터 내보내는 캐시 정책. 렌더 타깃처럼 크기가 큰 자원은 항목 수와 바이트 용량을 함께 제한합니다. |
| 캐시 무효화 | 예전 결과를 더는 재사용하면 안 된다고 판단하는 일. APK뿐 아니라 변환기·ABI·프레임워크 조건도 원인이 됩니다. |
| 픽셀 계약 | 특정 입력을 그렸을 때 어느 픽셀이 어떤 값이어야 하는지 정한 검사 조건. 막연한 육안 평가를 구체화합니다. |
| Entitlement·서명 | 프로그램의 특정 시스템 기능 사용 선언과 코드의 서명 정보. 서명되어 있다는 사실이 제3자 APK의 안전성 보증은 아닙니다. |
| CLA·AGPL | CLA는 기여 시 체결하는 별도 계약이고, AGPL은 이 저장소 코드의 공개 라이선스입니다. 적용 대상과 역할이 다릅니다. |
키워드를 암기할 때는 정의 옆에 근거 파일 하나를 붙이세요. 예를 들어 “JNI → jvm_loader.cpp와 MacApkWebViewBridge.java”, “캐시 무효화 → CacheIsFresh()”처럼 연결하면 실제 문제를 만났을 때 찾아가기 쉽습니다.
| 항목 | 상태 |
|---|---|
| 저장소 | kksimp/Mac.apk · Android 앱의 Apple Silicon Mac 실행 환경 |
| 선정 | TrendShift Weekly #20 · 2026-10-23 분석 |
| 스타 / 포크 | 2,537 / 80 · 2026-10-23 GitHub 기준 |
| 생성일 | 2026-05-19 |
| 주 언어 | Java · 네이티브 실행 엔진은 C++·Objective-C++, 런처는 Swift |
| 최신 릴리스 | v1.0.2303 · 2026-10-21 · VERSION도 1.0.2303 |
| 최근 push | 2026-10-23 · GitHub 기준 |
| 최근 커밋 | 5a503e1 · 2026-10-23 · 커밋 메시지: PR #87의 CLA 서명 기록 |
| 분석 코드 기준 | 5a503e1dba842ee670fdd343c3a3f249079a0e35 |
| 코드 규모 | 선택 확장자·thirdparty 경로 제외 3,215파일 / 815,826줄. 같은 확장자 전체 3,246파일 / 840,519줄. 집계 범위는 5절 참조 |
| 라이선스 | AGPL-3.0 · 별도 상용 라이선스 안내 존재. 제3자 구성요소는 각각의 고지 확인 |
| 성숙도 | README 기준 Alpha · 최소 macOS 26.4, Apple Silicon 전용 |
| 호환성 집계 | 문서 기준 71개 앱: 동작 58·부분 10·미동작 3. 갱신일 2026-10-20, 버전별 중복 행 존재 |
| 공개 검증 범위 | 일부 픽셀 회귀 검사·개발 도구 공개. 전체 회귀 시험과 APK 시험 모음은 BUILDING.md 기준 비공개 |
호환성: 동작 표시는 특정 앱·버전의 프로젝트 시험 기록입니다. 로그인·카메라·파일 권한·온라인 기능·그래픽의 완전한 호환성을 보장하지 않습니다. 빈 API·스텁도 있습니다. README는 Mac.apk 자체의 광고 제공 없음과 인앱 결제 미지원을 밝힙니다. 호환성 표에는 앱 내부 광고가 재생되는 사례도 있으므로, 모든 APK의 광고가 차단된다는 뜻으로 확대하면 안 됩니다.
성능과 안전: 게스트 OS가 없다는 사실만으로 모든 앱이 원래 기기와 같은 속도라는 결론은 나오지 않습니다. DEX 변환·ARM32 재컴파일·JNI·GPU readback 비용이 있습니다. APK는 Android식 격리 없이 사용자 권한으로 실행됩니다.
재현 범위: BUILDING.md는 전체 회귀 시험, APK 모음, 일부 pairip 호환 데이터가 공개 저장소에 없다고 설명합니다. 소스 빌드와 공식 배포본이 모든 앱에서 같은 결과를 낸다고 단정할 수 없습니다.
시점: 구현 해설은 커밋 5a503e1, GitHub 수치는 2026-10-23, 호환성 요약은 2026-10-20 문서 기준입니다. 구현 구조와 호환성 목록만으로 개별 APK의 성능·안전성까지 보장되지는 않습니다. 이후 버전은 해당 버전의 변경 사항과 문서를 함께 확인하세요.