본문으로 건너뛰기
5분 AI.
TRENDSHIFT WEEKLY #20 딥다이브 · 2026-10-23 분석

Mac.apk 딥다이브
— Android의 약속을 macOS 위에서 다시 구현하다

호환성 런타임

빠른 판단

무엇
DEX 변환·Android API 재구현·네이티브 로더로 APK를 macOS 프로세스에서 실행하는 호환 계층
이럴 때
Apple Silicon Mac에서 지원 Android 앱을 실행할 때
입력
Android APK의 DEX·리소스·네이티브 코드
결과
Mac의 화면·입력·오디오 기능에 연결한 앱 실행
APK를 Mac의 창과 Dock으로 가져오는 오픈소스 호환성 계층입니다. Android 운영체제를 통째로 띄우는 대신, Java API·바이트코드·네이티브 라이브러리·그래픽 호출의 경계를 하나씩 연결합니다. 이 글에서는 “둘 다 ARM인데 왜 바로 실행되지 않을까?”에서 출발해 컴파일러, JNI, 캐시, 안전한 설치 설계를 배웁니다. 저장소 kksimp/Mac.apk · ⭐2,537 · 포크 80 · 주 언어 Java · AGPL-3.0 · 2026-10-23 GitHub 기준.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

Android 앱이 기대하는 환경을 Mac의 부품으로 채우는 프로젝트

Mac.apk는 Apple Silicon Mac에서 Android 앱을 일반 macOS 프로세스로 실행하도록 만드는 호환성 계층입니다. APK 속 DEX 코드는 JVM 바이트코드로 바꾸고, Android의 android.* API는 Java로 다시 구현하며, 네이티브 코드는 아키텍처에 맞는 로더·변환기를 거칩니다. 화면과 소리는 Cocoa·Metal·macOS 미디어 기능으로 연결합니다.

같은 CPU 명령어를 쓴다는 사실만으로는 부족하고, 앱이 기대하는 운영체제의 약속까지 맞춰야 실행됩니다. Android와 macOS는 실행 파일 형식, 시스템 라이브러리, 파일 경로, 스레드 규칙이 다릅니다. 이 차이를 메우는 코드가 이 저장소의 주인공입니다.

용어 먼저
호환성 계층 (compatibility layer)
앱이 “이 기능을 호출하면 이런 결과가 온다”고 기대하는 규칙을 다른 환경에서 재현하는 중간층입니다. 함수 이름만 같게 만드는 것으로 끝나지 않습니다. 반환값, 오류, 호출 순서, 실행 스레드까지 앱의 기대와 맞아야 합니다.
한 문장 비유

외국 식당을 통째로 옮기는 대신, 그 식당의 주문을 알아듣는 주방을 Mac에 만듭니다

앱은 손님이고 Android API는 메뉴판입니다. “창을 열어 주세요”, “파일을 읽어 주세요”, “이 도형을 그려 주세요”라는 주문을 Mac의 주방이 받아 처리합니다. 손님이 쓰는 메뉴판은 그대로지만, 뒤에서 움직이는 조리 도구는 달라집니다.

문제는 메뉴 이름만 번역하면 끝나지 않는다는 데 있습니다. “차갑게”, “먼저 내어 주세요” 같은 조건도 맞춰야 합니다. 소스의 수많은 호환성 분기와 스레드 처리는 바로 이런 세부 약속을 맞추는 작업입니다.

APK 안의 세 가지 짐을 분리해서 보자

APK 구성담긴 내용Mac.apk의 처리
DEXJava·Kotlin 등에서 컴파일된 Android 바이트코드com.macapk.dex가 JVM용 클래스 바이트를 생성
네이티브 코드lib/arm64-v8a/*.so, lib/armeabi-v7a/*.so 등ARM64는 ELF 로딩·연결, ARM32는 ARM64 코드로 재컴파일
리소스바이너리 Manifest, 문자열·레이아웃·그림, resources.arscManifest·리소스 파서가 읽고 프레임워크 구현에 전달

구현의 시작점은 runtime/src/main.cpp::main()입니다. APK 준비와 JVM 생성을 거쳐 com.macapk.loader.Boot.run()을 호출합니다. Java 쪽에서는 앱 클래스 로더와 실행 환경을 만들고 Activity 실행으로 이어집니다. 끝에는 Mac의 이벤트 루프가 창과 입력을 처리합니다.

이 글에서 얻을 것
앱 하나를 실행하기 위해 필요한 경계를 읽는 눈

“Java 프로젝트”라는 언어 분류 너머를 보게 됩니다. 컴파일러가 코드를 바꾸는 경계, JNI가 언어를 연결하는 경계, 로더가 파일을 메모리에 올리는 경계, 설치기가 사용자 데이터를 보존하는 경계를 실제 파일에서 찾는 것이 목표입니다.

README는 프로젝트를 Alpha로 명시합니다. “모든 Android 앱을 실행한다”는 제품으로 이해하면 곤란합니다. 어떤 기능을 구현했고 어떤 기능은 비어 있는지 읽어 가며, 완전한 플랫폼 호환성이 얼마나 넓은 문제인지 배우기에 적합합니다.

2왜 주목받는가

관심을 끄는 구조적 이유와 실제로 감수하는 비용

2-1. “Android를 부팅하지 않고 앱을 띄운다”는 접근

TrendShift Weekly #20의 선정 대상이며, 2026-10-23 GitHub 기준 스타는 2,537개, 포크는 80개입니다. 2026-05-19 생성된 저장소에 이 정도 관심이 모였다는 점은 눈에 띕니다. 다음은 소스 구조와 공개 기능을 바탕으로 해석한 관심 요인이며, 특정 사건이 인기 상승의 원인이라는 뜻은 아닙니다.

2-2. 비슷한 목표, 다른 실행 환경

접근앱이 실행되는 환경장점과 선택 기준
Mac.apkmacOS 프로세스 + 재구현한 Android API + JVM·네이티브 브리지Android 시스템 부팅 없이 Mac 창과 통합. 대신 API 누락·행동 차이를 프로젝트가 직접 해결해야 함
Android EmulatorAVD로 구성하는 가상 Android 기기와 시스템 이미지기기·API 수준을 바꾸며 앱을 개발·시험하는 목적. Mac.apk와 호환성·성능이 같다고 볼 수 없음
WaydroidGNU/Linux에서 컨테이너로 구동하는 전체 Android 시스템Linux의 namespaces와 Android 환경을 활용. macOS 위의 API 재구현과는 전제가 다름

비교 근거는 Android Emulator 공식 안내와 Waydroid 공식 문서입니다. 여기서 Mac.apk의 장점은 시스템 부팅을 생략하고 호스트 기능을 연결하는 설계입니다. 동일 앱·동일 하드웨어로 측정한 자료 없이 “항상 더 빠르다”, “메모리가 몇 배 적다”는 결론은 내릴 수 없습니다.

2-3. “네이티브”를 세 가지 뜻으로 나눠 읽기

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 렌더링을 구분

2-4. 호환성 숫자는 시험 범위를 읽어야 의미가 있다

COMPATIBILITY.md는 2026-10-20 갱신 기준으로 71개 앱: 동작 58개, 부분 동작 10개, 미동작 3개라고 요약합니다. 이는 프로젝트의 수동 시험 집계입니다. 같은 앱의 여러 버전이 별도 행으로 나오므로 표의 행 수를 앱 수로 세면 안 됩니다.

또한 “동작” 표시 옆에도 세부 결함이 적혀 있습니다. AntennaPod는 재생 화면 문제, Mindustry는 온라인 멀티플레이 문제를 기록합니다. 숫자를 Android 생태계 전체의 성공률로 일반화하지 말고, 어떤 버전에서 어떤 동작을 시험했는지를 함께 읽어야 합니다.

비유

“해외 메뉴 58개를 조리해 봤다”는 기록과 “그 나라의 모든 요리를 완벽하게 만든다”는 보장은 다릅니다. 호환성 표는 시험한 메뉴와 남은 문제를 적어 놓은 작업 일지에 가깝습니다.

3기술 스택 전체 지도

웹 서비스의 앞·뒤가 아니라, 관리 UI와 실행 엔진의 분업

이 저장소에는 package.json, Cargo.toml, pyproject.toml, go.mod, Package.swift, Dockerfile이 없습니다. Node 서버나 Rust 데스크톱 셸을 가정하고 읽으면 구조를 놓칩니다. 빌드 설정과 의존성 출처는 CMake 파일과 셸 스크립트, 사전 빌드 산출물의 PROVENANCE.txt에 나뉘어 기록되어 있습니다.

3-1. 백엔드에 해당하는 실행 엔진

층기술·실제 설정역할과 확인 파일
Android APIJava, 컴파일 기준 --release 21runtime/java/android/ 등의 재구현. 설정은 runtime/scripts/build.sh
DEX 변환자체 Java 변환기DexFile·TypeInference·Translator·ClassFileWriter. ASM 의존을 가정하지 말 것
JVM 호스트OpenJDK/HotSpot, 기본 JDK 21jvm_loader.cpp의 JNI_CreateJavaVM(). 선택 기본값은 debug/jdk25_select.sh
네이티브 런타임C++ 17, Objective-C++ 17runtime/CMakeLists.txt. APK 준비, Cocoa·미디어·JNI 연결
번역·호환 계층C++ 20, Capstone, Bionic shimruntime/translator/CMakeLists.txt. ARM 코드 처리·심볼 연결
암호 제공자Bouncy Castle 1.79runtime/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는 외부 라이브러리 버전입니다. 둘을 같은 종류의 숫자로 나열하지 않는 습관이 중요합니다.

3-2. 프론트엔드와 호스트 통합

부품기술실제 연결 지점
관리 화면SwiftUI·AppKit·Combinelauncher/MacApkLauncherApp.swift; 설치 앱 목록과 설정
앱 포장Swift, Finder·Dock·LaunchServicesBundleGenerator.generate()로 앱별 .app 생성
창·입력Cocoa·Metal·JNIcocoa_bridge.mm, NativeCocoa.java; Android 앱 UI의 호스트 창
웹 콘텐츠WKWebViewruntime/java/android/webkit/WebView.java → MacApkWebViewBridge → webview_bridge.mm
소리·영상AudioToolbox·AVFoundation·VideoToolbox, 선택적 libvpxaudio_backend.mm, video_toolbox.mm, video_vp8_decode.mm 등

SwiftUI는 Mac.apk의 관리 화면에 쓰입니다. 모든 Android View를 SwiftUI 컴포넌트로 바꾼다는 뜻은 아닙니다. Android 앱 자체의 화면은 프레임워크·Canvas·Surface·그래픽 브리지를 통해 그려집니다. 이 구분을 해야 UI 오류가 생겼을 때 올바른 파일부터 찾을 수 있습니다.

3-3. 그래픽에는 여러 통로가 있다

입력호스트 경로의존성 근거
OpenGL ESGLES 브리지 → ANGLE → Metal 백엔드 등thirdparty/angle-prebuilt/PROVENANCE.txt: ANGLE 커밋 e44b533
VulkanBionic Vulkan 호환 코드 → MoltenVK 직접 연결 → Metal같은 출처 파일: MoltenVK 커밋 1c96d3d
AGSL·효과Skia Ganesh Metal → 결과 읽기 → Java2D Canvas 합성thirdparty/skia-prebuilt/PROVENANCE.txt: Skiko 0.150.2, Skia m150-a3f509bf52
일반 CanvasJava2D를 사용하는 구현runtime/java/android/graphics/Canvas.java와 BUILDING.md

ANGLE은 그래픽 API 번역 계층이며, MoltenVK는 Vulkan의 일부 기능을 Metal 위에 제공합니다. 각각의 상류 프로젝트가 지원하는 범위와 Mac.apk가 실제 연결한 범위는 같지 않을 수 있습니다. API 이름이 보인다고 모든 확장 기능의 지원까지 결론 내리지 마세요.

Capstone·libpng·libvpx는 Homebrew 경로에서 찾지만 CMake에 정확한 패치 버전 고정은 없습니다. Swift 컴파일러 버전도 고정 매니페스트로 선언하지 않습니다. 따라서 표에 임의의 최신 버전을 채우지 않습니다. 고정된 것과 환경에 맡긴 것을 나눠 기록하는 것도 의존성 분석입니다.

3-4. 인프라·빌드·검증

의존성 목록만 외우기보다, 각 기술이 어느 경계를 해결하고 무엇을 아직 보장하지 않는지 연결해 읽는 것이 핵심입니다.

4아키텍처 심화 분석

앱을 열 때의 흐름과, 그 흐름을 지탱하는 네 가지 설계

4-1. 시스템 전체 구조도

사용자: APK 설치 / 생성된 .app 열기 | SwiftUI 런처 / 설치 helper InstallCore + BundleGenerator | 앱별 .app [작은 실행 stub] | execv() v 공용 macapk-runtime [C++ 호스트] APK 준비 + JVM 생성 + JNI 등록 | +--------+------------------------+ | | v v DEX / resources 네이티브 .so | | | DexConverter ARM64 ELF ARM32 ELF | | | JVM .class / classes.jar wrapper + arm32rc | ELF 로더 재컴파일 Boot.run → AppClassLoader | | | +-----+----+ ActivityHost | | Bionic shim android.* 재구현 | | | +---- JNI / 호스트 브리지 ------+ | +------------+------------+----------------+ | | | | Cocoa 창 WKWebView GLES/Vulkan Canvas·AGSL 입력·이벤트 WebKit ANGLE/MoltenVK Java2D/Skia | | | | +------------+------------+----------------+ macOS / Apple Silicon

구조도는 책임을 구분한 그림입니다. 모든 코드가 위에서 아래로 한 번씩만 실행되는 것은 아닙니다. 앱의 Java 코드가 JNI로 네이티브 함수를 부르고, 그 네이티브 코드가 다시 Java 메서드를 호출하기도 합니다. 리소스는 화면 생성 중에도 읽힙니다.

4-2. 설계 ① 변환 파이프라인: 복잡한 문제를 단계로 나눈다

runtime/java/com/macapk/dex/DexConverter.java는 변환기의 진입점입니다. open(byte[]... dexes)로 세션을 만들고, Session.classBytes()로 한 클래스를, convertAll()로 전체를 변환합니다. 세션은 여러 DEX의 클래스를 모아 부모 타입과 중첩 클래스 관계를 함께 판단합니다.

DEX 바이트 → DexFile.parse() : 파일 구조와 클래스 읽기 → 명령·제어 흐름 분석 : InstructionDecoder / ControlFlowGraph → 타입 추론 : TypeInference / ClassHierarchyOracle → JVM 명령 생성 : Translator.translate() → 클래스 파일 작성 : ClassFileWriter / StackMapWriter → ClassLoader.defineClass() 또는 JAR 저장

왜 이름만 바꿔서는 안 될까요? 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을 던지는 몸체를 만들 수 있습니다. “클래스를 로드했다”와 “그 안의 모든 메서드가 올바르게 작동한다”는 서로 다른 성공 조건입니다.

4-3. 같은 네이티브 코드라도 ARM64와 ARM32는 다르다

경로핵심 파일·함수해결하는 문제
ARM64arm64_pack.cpp, arm64_macho_emit.cpp, arm64_native.cpp::__macapk_arm64_native_load_from_path()작은 Mach-O wrapper로 진입하고 ELF를 메모리에 올려 재배치·심볼 연결. 호스트와 다른 동작은 shim·패치로 처리
ARM32runtime/translator/src/main.cpp의 함수 발견 → arm32rc::build_module() → macho_link.cppARM·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 생성을 설명합니다. 빌드 파일의 오래된 주석보다 현재 분기와 호출을 우선해야 합니다.

같은 ARM이면 끝날까?
ISA와 ABI
ISA는 CPU가 알아듣는 명령어 집합입니다. ABI는 함수 인자를 어느 레지스터에 넣는지, 구조체를 어떻게 배치하는지, 어떤 실행 파일 형식을 쓰는지 같은 바이너리 수준의 약속입니다. 같은 ISA를 써도 ABI와 OS 서비스가 다르면 연결 작업이 남습니다.

4-4. 설계 ② 어댑터: Android의 겉모양, macOS의 실제 기능

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 상태가 동시에 바뀌어 문제가 생길 수 있습니다.

4-5. 설계 ③ 캐시: 파일의 존재보다 만들어진 조건을 확인한다

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는 필요해진 클래스만 변환하고 없는 클래스의 조회 결과도 캐시합니다.

4-6. 설계 ④ 공유 런타임과 중단 복구가 가능한 설치

launcher/AppStubMain.swift::main()은 앱별 작은 실행 파일에서 공용 런타임으로 execv()합니다. 실행 파일을 다른 프로그램으로 바꾸는 방식이므로, 앱마다 JVM·그래픽 라이브러리 전체를 복제할 필요가 줄어듭니다. BundleGenerator.generate()는 작은 stub과 앱 정보로 .app을 구성합니다.

기존 앱 A.app → .A.app.previous 로 이동 준비된 .A.app.staging → A.app 으로 이동 이전 데이터 인계 성공 → .previous 정리 중간에 중단됨 → recoverInterruptedSwap()가 다음 설치에서 복구 시도

각 이동은 같은 디렉토리의 이름 변경을 활용하지만, 설치 전체가 단일 원자 연산은 아닙니다. 여러 이동 사이에 중단될 수 있어서 recoverInterruptedSwap()가 필요합니다. 이전 데이터를 인계하지 못했으면 .previous를 삭제하지 않고 다음 시도를 위해 보존합니다. 현재 앱 데이터는 번들 밖의 ~/Library/Application Support/MacApk/에 저장됩니다. 교체 과정은 이를 가리키는 Contents/android 링크나 이전 버전의 내부 데이터 폴더도 보존합니다.

이 아키텍처에서 재사용할 만한 생각은 “무엇을 공유하고, 어떤 조건에서 다시 만들며, 실패하면 무엇을 남길까”를 명시하는 습관입니다.

입문자는 먼저 WebView 어댑터와 설치 상태 전이를 읽고, 그다음 DEX 변환기로 들어가세요. 처음부터 ARM64 로더 전체를 따라가면 포인터·메모리 매핑·재배치·신호 처리 문제가 한꺼번에 등장합니다.

5디렉토리 구조 해부

폴더 이름보다 책임과 변경 범위를 먼저 기억하기
Mac.apk/ ├── README.md 제품 개요·설치·요구사항 ├── BUILDING.md 빌드 절차·공개되지 않은 부분 ├── COMPATIBILITY.md 앱·버전별 수동 시험 기록 ├── VERSION 1.0.2303 ├── launcher/ Mac 관리 UI·설치·앱별 실행 파일 │ ├── MacApkLauncherApp.swift │ ├── InstallCore.swift 공통 설치·데이터·레지스트리 처리 │ ├── BundleGenerator.swift .app 생성·교체·복구 │ └── AppStubMain.swift 공용 런타임으로 실행 인계 ├── runtime/ │ ├── CMakeLists.txt 호스트 런타임 빌드 │ ├── java/ │ │ ├── android/ Android 프레임워크 API 재구현 │ │ ├── androidx/ 관련 호환 API │ │ ├── dalvik/system/ DEX 로딩·변환 연결 │ │ ├── com/macapk/dex/ DEX → JVM 변환기 │ │ ├── com/macapk/loader/ 부팅·클래스로더·리소스 │ │ ├── com/macapk/graphics/ Skia 연결 │ │ └── com/google/ 일부 서비스 호환 구현·스텁 │ ├── java_base_patch/ JDK java.base에 적용할 클래스 │ ├── jdk_unsupported_patch/ Unsafe 호환 계층 │ ├── net_spi/ JVM 네트워크 이름 해석 연동 │ ├── include/ 호스트 C++ 인터페이스 │ ├── src/ C++·Objective-C++ 호스트 구현 │ ├── scripts/ 빌드·산출물 정체성 계산 │ ├── thirdparty/ Bouncy Castle·디코더 등 │ └── translator/ │ ├── CMakeLists.txt translator·Bionic shim 빌드 │ ├── include/ ELF·발견·연결 인터페이스 │ └── src/ ARM64 로더·Bionic shim │ └── arm32rc/ ARM32 → ARM64 생성·Mach-O 작성 ├── thirdparty/ │ ├── angle-prebuilt/ ANGLE·MoltenVK 바이너리·헤더 │ ├── angle-patches/ ANGLE 변경 이력 │ ├── moltenvk-patches/ MoltenVK 변경 이력 │ └── skia-prebuilt/ Skiko/Skia 바이너리·출처·라이선스 ├── scripts/ 빌드·번들 의존성·서명·배포 ├── debug/ 공개 검사·코드 생성·개발 도구 ├── .github/workflows/cla.yml CLA 서명 자동화 └── THIRD_PARTY_NOTICES.txt 함께 배포하는 구성요소의 고지

5-1. 증상에서 폴더로 거꾸로 찾아가기

관찰한 문제먼저 읽을 곳추적할 질문
설치·업데이트 오류launcher/InstallCore.swift, BundleGenerator.swift어느 상태까지 진행했나? 기존 앱과 데이터가 어디에 남았나?
클래스 로딩 오류AppClassLoader.java, runtime/java/com/macapk/dex/클래스가 없나, 변환이 실패했나, 부모 클래스와 충돌했나?
문자열·레이아웃 오류ResourceIndex.java, ArscReader.java리소스 ID와 화면·언어·테마 조건을 어떻게 골랐나?
웹 화면 오류runtime/java/android/webkit/, webview_bridge.mmJava 이벤트와 WKWebView 이벤트 사이에서 무엇이 달라졌나?
네이티브 로딩 오류AppClassLoader.findLibrary(), runtime/translator/ABI 선택, 캐시 정체성, 필요한 심볼 중 무엇이 문제인가?

ResourceIndex는 설치된 APK와 split APK의 리소스 테이블·ZIP 항목을 다룹니다. 현재 경로를 “apktool로 모든 XML을 풀어 놓고 폴더를 읽는다”고만 설명하면 오래된 구조를 옮기게 됩니다. ArscReader·BinaryXmlParser와 실제 리소스 선택 코드를 함께 읽어야 합니다.

5-2. 코드 규모와 집계 범위

커밋 5a503e1의 확장자별 물리 줄 수이며, 파일 끝에 개행이 없는 마지막 줄도 1줄로 셉니다. 다음 표는 경로 구성요소가 thirdparty인 폴더를 모두 제외한 값입니다. 빈 줄·주석·생성 코드·API 스텁·JDK 패치도 포함하므로, 순수한 자체 구현량이나 완성도를 뜻하지 않습니다.

구분파일 수물리 줄 수
Java3,060583,719줄 · 약 58.4만
C++49159,866줄 · 약 16.0만
Objective-C++2230,222줄
Swift718,551줄
헤더3814,096줄 · .h·.hpp
Shell307,432줄
Python71,885줄
C255줄
총계3,215815,826줄 · 약 81.6만

thirdparty 경로까지 포함하면 같은 확장자 범위에서 3,246개 파일, 840,519줄입니다. CMake·JSON·문서·바이너리는 이 집계에서 제외했습니다. 특히 사전 빌드 라이브러리의 원본 구현량은 바이너리 크기만으로 환산할 수 없습니다.

가장 넓은 Java 코드 영역은 runtime/java/의 578,305줄입니다. 하지만 처음부터 모두 읽을 필요는 없습니다. 증상 하나 → 진입 함수 하나 → 관련 인터페이스 하나로 좁히는 것이 이 규모의 저장소를 읽는 현실적인 방법입니다.

6학습 포인트 (기술별)

파일을 읽고 끝내지 않고, 설명할 수 있는 작은 개념으로 바꾸기

6-1. Java: API 모양과 실제 행동은 별개다

먼저 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 구현도 읽어 보세요. 빈 몸체, 고정 반환값, 호스트 브리지 호출, 완전한 상태 관리 중 어느 유형인지 구분하면 호환성의 깊이가 보입니다.

작은 실습 아이디어

API 세 개를 “모양·상태·부작용”으로 분해하기

각 메서드에 대해 인자와 반환형, 바뀌는 메모리 상태, 파일·네트워크·UI 부작용을 적습니다. 빈 메서드와 WKWebView로 연결되는 메서드를 비교하면 API가 존재한다는 말의 한계를 설명할 수 있습니다.

6-2. 클래스 로더: 이름이 같아도 어디서 왔는지가 중요하다

AppClassLoader.findClass()는 클래스 이름의 슬래시를 점으로 정규화하고, 필요한 재작성과 정의를 거칩니다. 지연 DEX 경로에서도 ClassFileRewriter.maybeRewrite()를 통과합니다. 변환 방식이 달라져도 공통으로 코드를 보정하는 지점을 유지하는 설계를 볼 수 있습니다.

findLibrary()는 System.loadLibrary()에 대응하는 네이티브 라이브러리 위치를 찾습니다. 따라서 ClassNotFoundException과 네이티브 라이브러리 로딩 오류는 서로 다른 질문으로 출발해야 합니다. 클래스 파일과 .so를 같은 탐색 경로라고 생각하지 마세요.

6-3. 컴파일러: 타입은 분기와 예외를 따라 흘러간다

runtime/java/com/macapk/dex/TypeInference.java는 각 명령 진입 시점의 레지스터 타입을 추론합니다. 한 명령의 결과를 다음 명령으로 전달하고, 여러 경로가 합쳐지는 곳에서는 상태를 병합합니다. 더 이상 상태가 바뀌지 않을 때까지 반복하는 고정점 계산입니다.

컴파일러 용어
제어 흐름 그래프와 데이터 흐름 분석
제어 흐름 그래프는 “다음에 어느 코드로 갈 수 있나”를 그린 지도입니다. 데이터 흐름 분석은 그 길을 따라 “이 변수에 어떤 값·타입이 올 수 있나”를 계산합니다. 코드 생성 전에 이 지도가 맞아야 분기와 예외를 올바르게 옮길 수 있습니다.
[조건 검사] / \ [객체 타입 A] [객체 타입 B] \ / [합류 지점] | 양쪽에서 안전하게 쓸 수 있는 공통 타입은?

특히 예외 경로를 보세요. 명령 실행 도중 실패할 수 있으므로 예외 처리기로 전달할 상태는 그 명령이 완성된 뒤의 결과가 아닙니다. 소스 주석과 상태 전파 코드는 명령 실행 전 상태를 강조합니다. 이것이 단순 opcode 치환보다 변환기가 훨씬 복잡한 이유입니다.

6-4. C++·운영체제: 포인터와 파일 형식이 만나는 지점

runtime/translator/src/elf64_reader.cpp와 arm64_native.cpp를 연결해 읽으면 파일의 바이트가 메모리의 실행 가능한 영역으로 바뀌는 과정을 볼 수 있습니다. 로더는 “파일을 연다”에서 끝나지 않습니다. 영역을 배치하고, 주소를 고치고, 외부 심볼을 찾고, 초기화 함수를 실행해야 합니다.

ARM32 경로는 포인터 폭이 추가 난점입니다. 32비트 게스트가 생각하는 주소와 64비트 호스트의 실제 포인터를 연결해야 합니다. runtime/translator/src/arm32rc/regmap.cpp는 게스트 레지스터와 호스트 레지스터의 대응을 다룹니다. 처음에는 코드 생성 세부보다 입력 상태·출력 상태·보존할 레지스터만 표로 정리해도 충분합니다.

비유

ELF 로딩은 압축 상자에서 물건을 꺼내는 것보다 이사에 가깝습니다. 새 방에 가구를 놓은 뒤, 예전 방 번호를 가리키던 주소록까지 새 주소에 맞춰 고쳐야 합니다. 그 주소 보정이 재배치입니다.

실습 아이디어: 동적 라이브러리를 설명하는 카드에 “CPU 아키텍처, 파일 형식, 외부 심볼, 초기화 함수” 네 항목을 적고 ELF와 Mach-O의 차이를 정리합니다. 실제 바이너리를 실행하지 않아도 로더의 책임을 공부할 수 있습니다.

6-5. 그래픽: GPU를 쓰는 것과 전송 비용은 함께 봐야 한다

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로 옮겼다는 설명과 구분해야 합니다.

6-6. Swift·제품 설계: 성공보다 실패 뒤 상태를 먼저 그린다

BundleGenerator.generate()의 정상 경로와 recoverInterruptedSwap()의 복구 경로를 나란히 읽으세요. 새 번들을 준비하는 책임과 기존 데이터를 인계하는 책임을 나눕니다. InstallPipeline.Hooks에 클로저로 번들 동작을 연결하는 부분에서는 UI와 공통 설치 로직을 분리하는 방법도 보입니다.

학습할 핵심은 Swift 문법보다 실패했을 때 지우면 안 되는 데이터입니다. “오류를 잡았다”는 것만으로 복구가 되지는 않습니다. 사용자의 유일한 데이터가 있는 폴더를 보존하고, 다음 실행에서 그 상태를 알아볼 수 있어야 합니다.

기능이 한 번 성공하는 코드를 넘어, 실패해도 진단하고 다시 시도할 수 있는 코드를 읽는 것이 이 저장소의 가장 실용적인 공부입니다.

7하드웨어 / 시스템 요구사항

사용 조건, 개발 조건, 코드 읽기 조건을 분리해서 보기

7-1. 배포 앱을 사용하는 조건

항목요구사항근거와 해석
CPUApple Silicon MacREADME 기준. Intel Mac 지원 없음
운영체제macOS 26.4 이상README·BUILDING.md·launcher/Info.plist가 일치. macOS 27은 README 기준 v1.0.1702부터 지원
설치 공간Mac.apk 약 280 MBREADME의 대략값. 배포 파일·런타임 구성에 따라 달라질 수 있음
앱별 공간APK 크기의 약 1.5~3배README 기준 추정. 앱 데이터와 장기 캐시 증가는 별도 고려
메모리공식 최소 RAM 수치 미기재앱과 변환 작업에 따라 다름. 코드의 힙 설정을 제품 최소 RAM으로 읽으면 안 됨
최초 준비앱 크기에 따라 지연 가능README는 큰 앱에서 최초 실행 준비가 최대 약 1분이라고 안내. 보편적 시간 보장은 아님

runtime/src/jvm_loader.cpp에는 -Xmx1g 설정이 있습니다. 이것은 Java 힙의 상한 설정이며, 프로세스 전체 메모리나 Mac의 최소 RAM이 1 GB라는 뜻이 아닙니다. 네이티브 코드·그래픽 메모리·JIT·공유 라이브러리 등은 별도 영역을 사용합니다.

7-2. 왜 macOS 26.4가 경계인가

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 선언과 실행 중 기능 검사가 더 높은 조건을 요구합니다.

macOS 용어
Entitlement
서명된 프로그램이 특정 시스템 기능을 사용할 수 있도록 선언하는 권한 항목입니다. JIT나 장치 접근처럼 일반적인 실행 권한만으로 끝나지 않는 기능에 관여합니다. 서명, 사용 목적 문자열, OS 지원 여부는 각각 확인할 조건입니다.

7-3. 소스 개발을 위한 조건

코드를 읽는 데 필요한 환경과 APK를 실제로 실행하는 데 필요한 환경은 다르므로, 입문 학습은 읽기·상태도 작성만으로도 시작할 수 있습니다.

실행 전에 알아둘 사실
Android식 앱 샌드박스가 없다

README는 Android 앱이 macOS 사용자 계정 권한의 일반 네이티브 코드로 실행되며, 휴대전화식 앱 샌드박스가 없다고 명시합니다. 파일 선택·카메라 등에 macOS 권한 처리가 있더라도, 그것이 APK 전체를 Android 권한 체계로 격리한다는 뜻은 아닙니다.

알 수 없는 APK를 시험 실행하는 일과 소스를 읽는 일은 위험 수준이 다릅니다. 아래 실습은 설치나 APK 실행 없이도 학습 결과를 만들 수 있도록 구성했습니다.

8직접 해볼 수 있는 실습 과제

실행 없이 시작해서, 근거가 남는 산출물을 만드는 다섯 과제

다음 과제는 커밋 5a503e1의 파일을 읽고 표·그림·설계안을 만드는 방식입니다. 명령 예시는 저장소 루트 기준의 읽기 전용 탐색입니다. 빌드·설치·APK 실행은 요구하지 않습니다. 각 과제의 결과에는 근거 파일과 함수 이름을 함께 남기세요.

과제 1

APK를 세 갈래로 분류하기 입문 · 30~45분

목표: 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
과제 2

API 구현의 깊이를 비교하기 입문 · 45~60분

목표: 메서드 존재와 기능 구현을 구분합니다.

읽을 파일: FirebaseAnalytics.java, runtime/java/android/webkit/WebView.java, MacApkWebViewBridge.java, webview_bridge.mm.

진행: 빈 메서드 하나와 호스트로 연결되는 메서드 하나를 고릅니다. “입력·상태 변화·외부 동작·콜백 스레드·실패 처리” 다섯 열의 비교표를 만듭니다.

완료 기준: “클래스가 있으므로 서비스 전체가 지원된다”는 주장을 반박할 수 있는 코드 근거 두 개와, 지원 여부를 추가 확인해야 할 질문 두 개를 적습니다.

과제 3

캐시 무효화 결정표 작성하기 중급 · 60~90분

목표: “언제 재사용하고 언제 다시 만드는가”를 결정 규칙으로 바꿉니다.

읽을 파일: 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
과제 4

설치가 중단되는 순간을 모델링하기 중급 · 60~90분

목표: 파일 교체와 데이터 보존을 상태 머신으로 설명합니다.

읽을 함수: BundleGenerator.generate(), recoverInterruptedSwap(), adoptAndroidDir().

진행: final·staging·previous의 존재 여부를 상태로 적습니다. 이전 번들 이동 직후, 새 번들 승격 직후, 데이터 인계 실패 직후에 중단됐다고 각각 가정합니다.

완료 기준: 세 상황마다 다음 실행이 무엇을 복구하는지, 어느 폴더를 삭제하면 안 되는지 적습니다. 실제 파일을 이동하거나 삭제하지 말고 상태표로 검토합니다.

중단 시점남은 상태코드에서 찾아야 할 답
이전 번들 이동 뒤previous 존재, final 없음이전 앱을 원래 위치로 돌리는가?
새 번들 승격 뒤previous와 final 존재이전 데이터가 어디로 인계되는가?
데이터 인계 실패이전 데이터가 아직 previous에 있음삭제를 막고 다음 시도를 남기는가?
과제 5

“그림이 같다”를 픽셀 계약으로 바꾸기 고급 · 90~120분

목표: 화면이 대충 비슷하다는 평가를 구체적인 검사 조건으로 바꿉니다.

읽을 파일: debug/LayerColorFilterTest.java, debug/AgslRenderingSmoke.java, runtime/java/android/graphics/Canvas.java.

진행: transparentTiles()와 레이어 색 필터 검사 중 하나를 고릅니다. 입력 크기·색·변환 행렬, 검사 픽셀 좌표, 기대 ARGB 값 또는 허용 오차를 표로 옮깁니다.

완료 기준: 알파가 0인 입력 재사용, 겹친 레이어의 필터 순서, 클립 경계 중 하나에 대해 새 회귀 검사 설계안을 작성합니다. 테스트 코드를 실행하지 않아도 조건과 기대값을 검토할 수 있어야 합니다.

공부 기록법
“읽었다” 대신 “판단 근거가 남았다”를 완료 조건으로

각 과제 끝에 파일 경로, 함수 이름, 확인한 조건, 아직 모르는 질문을 기록하세요. 막연한 기능 소개보다 다음에 같은 문제를 만났을 때 다시 쓸 수 있는 자료가 됩니다.

9관련 기술 심화 학습 로드맵

작은 API에서 시작해 컴파일러와 운영체제 경계로 확장하는 6주

Java의 클래스·인터페이스·예외와 파일 읽기를 알고 있다면 다음 순서로 접근할 수 있습니다. 주당 3~5시간을 가정한 학습 제안이며, 저장소 전체를 여섯 주 만에 이해한다는 일정은 아닙니다. 매주 하나의 개념과 하나의 결과물만 가져가는 편이 좋습니다.

주차핵심 개념과 읽을 코드주간 결과물
1주차APK·DEX·Manifest·리소스. main.cpp의 Boot.run() 호출과 Manifest.java의 역할 구분APK 구성과 실행 경로를 잇는 한 장의 지도
2주차Java 클래스 로더·스텁·어댑터. AppClassLoader, FirebaseAnalytics, WebViewAPI 세 개의 입력·상태·부작용·실패 처리 비교표
3주차JNI·UI 스레드·이벤트 루프. MacApkWebViewBridge → NativeCreate() → Java 이벤트호출 방향과 실행 스레드를 표시한 왕복 시퀀스도
4주차바이트코드·제어 흐름·타입 추론. DexConverter, TypeInference, Translator작은 분기·예외 코드의 레지스터 상태표와 JVM 대응
5주차ELF·Mach-O·ABI·재배치. arm64_pack, arm64_native, arm32rc_driverARM64 직접 로딩과 ARM32 재컴파일의 책임 비교표
6주차캐시 정체성·설치 복구·픽셀 회귀. build_ids.py, BundleGenerator, LayerColorFilterTest무효화 결정표 + 설치 중단 상태표 + 회귀 검사 설계

9-1. 3주차까지의 우선 목표: 호출을 왕복으로 설명하기

보통은 Java에서 네이티브로 들어가는 화살표만 그립니다. 그러나 UI는 “페이지 열기 요청 → 호스트 처리 → 페이지 완료 이벤트 → 앱 콜백”이라는 왕복입니다. MacApkWebViewBridge의 이벤트 코드와 Handler 전달까지 포함해야 비동기 API의 약속을 설명할 수 있습니다.

Android 앱 스레드 macOS 주 스레드 호스트 이벤트 | | | | nativeCreate() | | +--------------------->| WKWebView 준비 | | |------------------->| | | | 탐색 결과 | JNI 이벤트 / Handler.post() <-----------+ | 앱의 UI 콜백 실행 | |

위 그림은 스레드 경계를 익히기 위한 개념도입니다. 실제 이벤트별 전달 규칙은 MacApkWebViewBridge.java에 따로 있습니다. JavaScript 인터페이스 호출은 전용 백그라운드 큐를 사용하므로 모든 콜백을 같은 UI 스레드라고 묶으면 안 됩니다.

9-2. 4~5주차의 우선 목표: 하나의 변환만 깊게 보기

처음에는 정수 덧셈·조건 분기·반환만 대상으로 삼으세요. 그다음 객체 생성과 예외를 더합니다. ARM 네이티브 쪽도 정수 계산과 함수 호출부터 시작하고, 신호·스레드·SIMD는 뒤로 미룹니다. 읽는 범위를 좁히면 변환 전후 상태를 끝까지 확인할 수 있습니다.

한 함수를 입력부터 출력까지 정확히 설명하는 경험이, 수십만 줄을 훑고 이름만 기억하는 것보다 다음 단계로 이어집니다.

9-3. 관심 분야에 따른 다음 갈림길

막혔을 때의 기준
모르는 단어보다 모르는 경계를 표시하기

“이 함수는 무엇을 받아 무엇을 돌려주는가”, “누가 소유하고 언제 해제하는가”, “어느 스레드에서 실행되는가”, “실패하면 무엇이 남는가”를 적어 보세요. 세부 구현을 몰라도 다음에 찾아볼 문서와 코드가 선명해집니다.

10핵심 키워드 사전

처음 등장한 낯선 단어를 코드의 역할과 연결하는 찾아보기
용어쉬운 설명
APKAndroid 앱의 코드·리소스·설치 정보를 담은 배포 묶음. ZIP 구조를 사용하며, Mac.apk는 안의 서로 다른 짐을 별도 경로로 처리합니다.
Split APK기본 앱과 CPU·언어·화면별 구성을 여러 APK로 나눈 것. 기본 파일 하나만 읽어서는 필요한 코드·리소스를 놓칠 수 있습니다.
DEXAndroid용 클래스·메서드·명령을 담은 바이트코드 형식. Java 소스나 JVM .class와 같은 파일은 아닙니다.
JVM·HotSpotJVM은 Java 바이트코드 실행 규칙을 제공하는 가상 머신, HotSpot은 그 구현 중 하나입니다. Mac.apk는 이를 프로세스 안에서 사용합니다.
JIT프로그램 실행 중 자주 쓰는 코드를 기계어로 컴파일하는 방식. 게스트 운영체제를 부팅하는 가상화와는 다른 층의 기술입니다.
호환성 계층앱이 기대하는 다른 플랫폼의 규칙을 현재 플랫폼에서 재현하는 코드. 함수 표면뿐 아니라 상태·오류·순서도 중요합니다.
스텁·no-op연결에 필요한 모양만 제공하거나 아무 동작도 하지 않는 구현. 호출 성공만으로 실제 서비스가 구현됐다고 볼 수 없습니다.
JNIJava 코드와 C·C++ 같은 네이티브 코드를 연결하는 인터페이스. 이름·인자·반환형·스레드 규칙이 맞아야 합니다.
ISACPU가 이해하는 명령어 집합. ARM32와 ARM64는 명령과 레지스터 체계가 다릅니다.
ABI이미 컴파일된 코드끼리 연결될 때 지키는 규칙. 인자 전달, 데이터 배치, 보존 레지스터 등이 포함됩니다.
ELF·Mach-O실행 파일·공유 라이브러리의 형식. Android 네이티브 라이브러리는 ELF를, macOS는 Mach-O를 사용합니다.
동적 로더라이브러리를 메모리에 올리고 필요한 주소·함수를 연결하는 부품. 파일 읽기보다 훨씬 많은 책임을 가집니다.
심볼·재배치심볼은 함수·데이터를 찾는 이름표입니다. 재배치는 실제 배치 위치에 맞게 코드·데이터의 주소 참조를 고치는 작업입니다.
Bionic shimAndroid C 라이브러리 Bionic을 기대하는 호출을 호스트 쪽 기능과 연결하는 호환 코드입니다.
트램펄린실제 함수로 실행을 넘기는 작은 코드 조각. 호출 규칙이나 주소 경계를 연결하는 데 쓰입니다.
ARM·ThumbARM32 코드에서 사용하는 명령 인코딩 방식. 같은 바이트도 올바른 모드와 시작 위치로 해석해야 합니다.
TLS여기서는 스레드별 저장 공간인 Thread-Local Storage. HTTPS의 암호 통신 규약 TLS와 문맥이 다릅니다.
Handler·LooperAndroid의 메시지를 특정 스레드 큐에 넣고 처리하는 도구. UI 콜백의 순서를 맞출 때 중요합니다.
ANGLEOpenGL ES 호출을 호스트가 지원하는 그래픽 API로 옮기는 계층. Mac.apk의 GLES 경로에서 사용됩니다.
MoltenVKVulkan의 일부 기능을 Apple Metal 위에서 제공하는 구현. Vulkan 전체 기능과 무조건 같다고 가정하면 안 됩니다.
Skia·AGSLSkia는 그래픽 엔진, AGSL은 Android의 런타임 셰이더 언어입니다. 이 저장소는 관련 효과를 Metal GPU 경로로 연결합니다.
ReadbackGPU의 렌더 결과를 CPU 쪽 메모리로 가져오는 작업. 전송량과 완료 대기로 비용이 발생할 수 있습니다.
LRU오랫동안 사용하지 않은 항목부터 내보내는 캐시 정책. 렌더 타깃처럼 크기가 큰 자원은 항목 수와 바이트 용량을 함께 제한합니다.
캐시 무효화예전 결과를 더는 재사용하면 안 된다고 판단하는 일. APK뿐 아니라 변환기·ABI·프레임워크 조건도 원인이 됩니다.
픽셀 계약특정 입력을 그렸을 때 어느 픽셀이 어떤 값이어야 하는지 정한 검사 조건. 막연한 육안 평가를 구체화합니다.
Entitlement·서명프로그램의 특정 시스템 기능 사용 선언과 코드의 서명 정보. 서명되어 있다는 사실이 제3자 APK의 안전성 보증은 아닙니다.
CLA·AGPLCLA는 기여 시 체결하는 별도 계약이고, AGPL은 이 저장소 코드의 공개 라이선스입니다. 적용 대상과 역할이 다릅니다.

키워드를 암기할 때는 정의 옆에 근거 파일 하나를 붙이세요. 예를 들어 “JNI → jvm_loader.cpp와 MacApkWebViewBridge.java”, “캐시 무효화 → CacheIsFresh()”처럼 연결하면 실제 문제를 만났을 때 찾아가기 쉽습니다.

11참고 링크

제품 문서, 실제 구현, 기반 기술 문서를 순서대로 연결하기

11-1. 프로젝트 공식 자료

11-2. 본문에서 읽은 핵심 구현

11-3. 기반 기술의 공식 문서

현재 상태 (2026-10-23 확인)

항목상태
저장소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
최근 push2026-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의 성능·안전성까지 보장되지는 않습니다. 이후 버전은 해당 버전의 변경 사항과 문서를 함께 확인하세요.