Techniques for Reducing Android NDK .so Size: Code, Build, and Dependency Optimization
The article shows how to shrink Android NDK .so binaries by over 30% through code de‑duplication, disabling exceptions/RTTI/iostream, using fine‑grained sections with LTO and dead‑code elimination, limiting exported symbols via version scripts, employing JNI dynamic registration, and consolidating C++ runtime dependencies into a shared library.
Background
Beyond functional features, software quality attributes such as performance, security, usability, and scalability are important, and the binary size of an application also has a profound impact on startup speed, download time, installation success rate, disk usage, and OOM crashes. The author reduced the size of a Cloud Music Android .so from over 30 MB to over 20 MB (more than 30% reduction) by applying three categories of optimizations: code, build/link, and dependencies.
Code Optimization
The focus is on removing duplicate code and disabling expensive C++ language features (exceptions, RTTI, and the iostream library) in the Android NDK.
Removing Duplicate Code
Duplicate code inflates binary size and is a code smell. Static analysis tools can detect duplicates, after which refactoring techniques such as extracting functions or extracting classes should be applied.
Disabling Expensive C++ Features
Exceptions add library code for each throw site; RTTI adds class metadata; iostream pulls in heavy I/O code. These should be avoided. The following CMake flags disable exceptions and RTTI:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -fno-rtti")For iostream, replace std::cout with Android logging APIs from <android/log.h> (e.g., __android_log_print ).
Build and Link Optimization
Key is to control exported symbols in the ELF file and let the linker discard unused sections. Important ELF sections include .text, .data, .dynsym, .dynstr, .symtab, and .debug.
Dynamic symbol table (.dynsym) should contain only necessary exported symbols. Unused functions and variables can be removed by placing each function/variable in its own section and enabling dead‑code elimination:
-ffunction-sections -fdata-sections -Wl,--gc-sectionsExample CMake flags:
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto -fdata-sections -ffunction-sections")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto -fdata-sections -ffunction-sections")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -O3 -flto -Wl,--gc-sections")NDK‑build equivalents:
LOCAL_CFLAGS += -Oz -flto -fdata-sections -ffunction-sections
LOCAL_LDFLAGS += -O3 -flto -Wl,--gc-sectionsTo limit exported symbols, a version script can be used. Example script:
{
global: gValue;
*someFuncs*;
extern "C++" {
CSemaphore::*;
CCritical::*;
};
local: *;
};CMake linking option:
# Export functions via version script
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,versionscript=${CMAKE_CURRENT_SOURCE_DIR}/funcs.map")
# Do not export symbols from static libraries
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,ALL")NDK‑build linking option:
# Export functions via version script
LOCAL_LDFLAGS += -Wl,--version-script=${LOCAL_PATH}/funcs.map
# Do not export symbols from static libraries
LOCAL_LDFLAGS += -Wl,--exclude-libs,ALLJNI Symbol Reduction
Prefer dynamic registration (implement JNI_OnLoad and register native methods there) so that only JNI_OnLoad , JNI_OnUnload , and Java_* symbols need to be exported, reducing the shared library size.
Dependency Optimization
When multiple .so files depend on the same static library, extract it into a separate shared library to avoid duplication. The article emphasizes unifying the C++ runtime (libc++) across the project. The recommended approach is to use the shared libc++_shared.so from a single NDK version and avoid static linking of libc++_static where possible.
Gradle snippet to include the shared runtime:
DANDROID_STL=c++_sharedFor NDK‑build, add to Application.mk :
APP_STL := c++_sharedWhen packaging a feature .aar, exclude the bundled libc++_shared.so to prevent conflicts:
packagingOptions {
exclude '**/libc++_shared.so'
}If a unified NDK version cannot be used, keep static linking but explicitly exclude its symbols:
LOCAL_LDFLAGS += -Wl,--exclude-libs,libc++_static.a -Wl,--exclude-libs,libc++abi.aSummary
By applying code de‑duplication, disabling costly C++ features, fine‑grained sectioning, dead‑code removal, symbol export control, JNI dynamic registration, and unified C++ runtime dependencies, the size of Android NDK .so libraries can be significantly reduced. Continuous monitoring in CI/CD pipelines is recommended to maintain these optimizations.
NetEase Cloud Music Tech Team
Official account of NetEase Cloud Music Tech Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.