Android의 네이티브 라이브러리(native libraries)는 AOSP 소프트웨어 스택에서 Linux kernel과 Android framework 사이에 위치하는 C/C++ 구현 계층이다.[1] 이 계층은 Bionic libc, 그래픽 API, 미디어 처리 라이브러리, 데이터베이스 엔진 등을 포함하며, 앱이 Java나 Kotlin으로 작성된 것과 무관하게 하드웨어 자원을 효율적으로 사용할 수 있도록 기반을 제공한다.[1][2] 네이티브 라이브러리에 직접 접근하려면 Android NDK와 JNI를 거쳐야 하며, 플랫폼이 공개하는 안정 API 범위와 ABI 호환성을 신경 써야 한다.
1. AOSP 스택에서의 위치
AOSP의 공식 아키텍처 문서는 Android 소프트웨어 스택을 Linux kernel, HAL, native libraries, ART, Android framework, 앱의 층으로 설명한다.[1] 네이티브 라이브러리 계층은 HAL 위, ART 및 프레임워크와 나란히 놓이며, 두 가지 방향으로 기능한다. 첫째로는 HAL이나 커널 드라이버와 직접 통신하는 저수준 구성요소로서, 그래픽 렌더링이나 오디오 처리처럼 성능에 민감한 작업을 담당한다. 둘째로는 Android framework나 앱 코드가 NDK를 통해 호출할 수 있는 안정적인 인터페이스를 제공한다.[1][2]
이 계층이 별도로 존재하는 이유는 Java/Kotlin 런타임만으로는 하드웨어 수준의 성능을 내기 어렵고, 기존 C/C++ 코드베이스를 재활용해야 하는 사례가 많기 때문이다. 게임 엔진, 영상 코덱, 신호 처리 라이브러리처럼 이미 최적화된 구현체를 Android 위에서 그대로 쓸 수 있도록 네이티브 계층이 안정적인 진입점을 만들어 준다.
2. Bionic libc
Bionic은 Android 전용으로 설계된 C 표준 라이브러리이다. 일반 Linux 배포판에서 쓰는 glibc와 구조적으로 다른 점이 있는데, Android에서는 별도의 libpthread나 librt가 없고 그 기능이 libc 안에 통합되어 있다.[3] 따라서 NDK로 네이티브 코드를 빌드할 때 pthread나 실시간 기능을 쓰기 위해 별도 라이브러리를 링크할 필요가 없다. 수학 함수는 별도 libm에 있으며, 빌드 시스템이 자동으로 링크해 준다.[3]
Bionic이 glibc 대신 사용되는 주된 이유는 라이선스와 크기다. glibc는 LGPL 계열이라 정적 링크 시 제약이 생기지만, Bionic은 BSD 라이선스 기반이어서 그런 제약 없이 Android 이미지에 포함할 수 있다. 또한 모바일 환경에 맞게 메모리 사용량을 줄이고 성능을 최적화했다. Android 6.0(Marshmallow, API 23, 2015년)부터는 malloc_info(3)을 통해 메모리 할당 상태를 XML로 조회하는 기능도 제공한다.[3]
VNDK(Vendor Native Development Kit)에서는 LL-NDK라는 개념으로 libc.so, libm.so, libdl.so, liblog.so, libvulkan.so 등 하위 호환성이 보장된 핵심 라이브러리 집합을 따로 정의한다.[4] 이 목록은 벤더 파티션의 코드가 안전하게 사용할 수 있는 라이브러리 경계를 선언하며, Android 버전이 바뀌어도 ABI가 유지되는 범위를 명확히 한다.
3. 그래픽 API
3.1 OpenGL ES
OpenGL ES는 임베디드 기기용으로 만든 OpenGL 하위 집합으로, Android에서는 API 수준별로 지원 버전이 달라진다. OpenGL ES 1.0은 API 4부터, 2.0은 API 5부터, 3.0은 API 18부터, 3.1은 API 21부터, 3.2는 API 24부터 지원된다.[5] EGL이 플랫폼 네이티브 인터페이스 역할을 하며, OpenGL ES 컨텍스트와 렌더링 서피스를 할당하고 관리한다.[5]
네이티브 계층에서 OpenGL ES를 쓰려면 NDK가 제공하는 <GLES/gl.h>, <GLES2/gl2.h>, <GLES3/gl3.h> 헤더를 포함하고 해당 라이브러리를 링크한다. 대부분의 앱에서는 Java/Kotlin의 GLSurfaceView를 통해 간접적으로 사용하지만, 게임 엔진이나 고성능 렌더러는 NDK를 통해 직접 호출한다.
3.2 Vulkan
Vulkan은 Khronos Group이 2016년에 공개한 저수준 GPU API로, OpenGL ES보다 드라이버 오버헤드가 낮고 멀티스레드 렌더링을 명시적으로 지원한다.[6] Android에서 Vulkan 라이브러리(libvulkan.so)는 API 24(Android 7.0, 2016년) 이상의 모든 기기에 존재하지만, 실제 GPU 하드웨어가 Vulkan을 지원하는지는 런타임에 별도로 확인해야 한다.[6]
OpenGL ES와 주요한 차이 중 하나는 셰이더 형식이다. OpenGL ES는 GLSL 소스 코드를 텍스트 문자열로 드라이버에 전달하지만, Vulkan은 미리 컴파일된 SPIR-V 바이너리 형식을 요구한다.[6] 이 덕분에 셰이더 컴파일 시간이 예측 가능하고 드라이버 의존성이 줄어들지만, 빌드 과정에서 별도의 셰이더 컴파일 단계가 필요하다.
Android 15(2024년)부터는 ANGLE 프로젝트가 선택적 레이어로 포함되어, OpenGL ES 호출을 Vulkan 위에서 실행할 수 있게 됐다.[5] 이는 OpenGL ES 구현 일관성을 높이고 일부 경우에는 성능도 개선할 수 있도록 한다.
4. 미디어 프레임워크
Android의 네이티브 미디어 라이브러리는 Java의 MediaExtractor, MediaCodec과 유사한 기능을 C/C++ 수준에서 직접 제공한다.[7] 과거에는 OpenMAX AL 1.0.1 API(Khronos Group 표준)가 네이티브 멀티미디어의 기반으로 쓰였으나, 현재는 더 이상 권장되지 않으며 새 코드는 Android 미디어 API를 사용하도록 안내된다.[7]
NDK가 제공하는 미디어 관련 안정 API로는 libmediandk가 있으며, 여기에는 AMediaCodec, AMediaExtractor, AMediaFormat, AMediaMuxer 등이 포함된다. 이 API들은 Android 앱이 Java 레이어를 거치지 않고 코덱과 컨테이너를 직접 다룰 수 있도록 하며, 저지연 오디오·영상 처리가 필요한 앱에서 주로 사용된다.
오디오 쪽에서는 AAudio API(API 26 이상)와 OpenSL ES(API 9 이상)가 네이티브 오디오 접근 경로로 제공된다. AAudio는 Android 8.0(Oreo, 2017년)에 도입된 저지연 오디오 전용 API이며, 지연 시간을 최소화해야 하는 음악·악기 앱에 적합하다.
5. NDK와 JNI
5.1 Android NDK
NDK(Native Development Kit)는 C 및 C++ 코드를 Android 앱에 포함시키기 위한 도구 모음이다.[2] Android Studio 2.2 이상에서는 CMake나 ndk-build를 Gradle과 연동해 네이티브 코드를 빌드하고 APK에 .so 파일로 패키징할 수 있다.[2] NDK가 필요한 대표적인 경우는 다음 세 가지다. 첫째, 기존 C/C++ 라이브러리를 Android에서 재사용할 때. 둘째, OpenGL ES나 Vulkan을 직접 호출하는 게임 엔진처럼 Java/Kotlin 레이어 없이 최고 성능이 필요할 때. 셋째, 신호 처리나 암호화처럼 성능이 중요한 연산을 구현할 때.
NDK가 제공하는 안정 API 범위는 공개 문서로 명확히 선언되어 있다.[2] 이 범위 밖의 내부 시스템 라이브러리를 직접 링크하면 Android 버전 업그레이드 시 앱이 깨질 수 있으므로, 안정 API 목록을 확인하고 그 안에서만 링크하는 것이 원칙이다. Android 7.0(API 24, 2016년)부터는 네이티브 라이브러리 네임스페이스가 도입되어 시스템 내부 라이브러리와 앱 라이브러리가 서로 격리되었다.[4]
5.2 JNI
JNI(Java Native Interface)는 Java/Kotlin 코드와 C/C++ 코드가 서로 통신하는 메커니즘이다.[8] JNI는 두 가지 핵심 구조체인 JavaVM과 JNIEnv를 정의한다. JavaVM은 프로세스당 하나만 허용되며 JVM 생성·소멸과 관련된 함수를 제공한다. JNIEnv는 대부분의 JNI 함수를 담고 있으며, 스레드마다 별도로 관리된다.[8]
실제 호출 흐름을 보면, Java 코드에서 native 키워드로 선언된 메서드를 호출하면 JVM이 미리 로드된 .so 파일에서 대응하는 C 함수를 찾아 실행한다. C 함수는 Java_패키지명_클래스명_메서드명의 규칙으로 이름이 붙으며, 첫 번째 인자로 JNIEnv*, 두 번째 인자로 jobject 또는 jclass를 받는다.[8] JNI를 통한 데이터 전달 시 Java의 참조 관리 규칙이 적용되므로, 로컬 참조와 글로벌 참조를 혼동하지 않는 것이 중요하다.
6. ABI와 라이브러리 배포
Android NDK는 현재 네 가지 ABI를 지원한다. armeabi-v7a(32비트 ARM), arm64-v8a(64비트 ARM), x86(32비트 Intel), x86_64(64비트 Intel)이다.[9] 각 ABI는 서로 다른 명령어 집합과 레지스터 규약을 갖기 때문에 같은 소스 코드라도 ABI별로 따로 빌드해야 한다.
APK 안에서 네이티브 라이브러리는 lib/<abi>/lib<name>.so 경로에 위치한다.[9] 기기에 앱을 설치할 때 패키지 관리자가 기기의 ABI에 맞는 .so 파일을 선택해 앱의 네이티브 라이브러리 디렉터리에 복사한다. 여러 ABI를 하나의 APK에 담는 방식(fat APK)을 쓰면 용량이 커지기 때문에, Android framework 수준에서는 Android App Bundle을 통해 기기별로 필요한 ABI의 라이브러리만 배포하는 방식이 권장된다.
C++ ABI는 과거에 변경된 사례가 있어 안정적이지 않다고 문서에서 명시적으로 경고한다.[9] 여러 네이티브 공유 라이브러리를 쓰는 앱은 반드시 하나의 C++ STL 구현을 공유해야 하며, 각 라이브러리가 STL을 따로 정적으로 링크하면 메모리 오류나 예기치 않은 동작이 생길 수 있다.
8. 인용 및 각주
[1] Architecture overview, Android Open Source Project, source.android.com(새 탭에서 열림)
[2] Get started with the NDK, Android Developers, developer.android.com(새 탭에서 열림)
[3] Native APIs, Android NDK, Android Developers, developer.android.com(새 탭에서 열림)
[4] Vendor Native Development Kit (VNDK) overview, Android Open Source Project, source.android.com(새 탭에서 열림)
[5] OpenGL ES, Android Developers, developer.android.com(새 탭에서 열림)
[6] Vulkan, Android Open Source Project, source.android.com(새 탭에서 열림)
[7] Native APIs, Android NDK — Media section, Android Developers, developer.android.com(새 탭에서 열림)
[8] JNI tips, Android NDK, Android Developers, developer.android.com(새 탭에서 열림)
[9] Android ABIs, Android NDK, Android Developers, developer.android.com(새 탭에서 열림)