기호 공개 상태 제어

기호 가시성을 제어하면 APK 크기를 줄이고 로드 시간을 개선할 수 있으며 다른 개발자가 실수로 구현 세부정보에 종속되지 않도록 할 수 있습니다. 이를 위한 가장 강력한 방법은 버전 스크립트를 사용하는 것입니다.

버전 스크립트는 ELF 링커의 기능으로, -fvisibility=hidden의 더 강력한 형태로 사용할 수 있습니다. 자세한 설명은 아래의 이점을 참고하거나 프로젝트에서 버전 스크립트를 사용하는 방법을 알아보세요.

위에 링크된 GNU 문서 및 이 페이지의 다른 몇몇 위치에서 '기호 버전'에 대한 참조를 볼 수 있습니다. 이러한 파일의 원래 의도는 라이브러리의 버그 호환성을 보존하기 위해 단일 라이브러리에 여러 버전의 기호(일반적으로 함수)가 존재하도록 허용하는 것이기 때문입니다. Android도 이러한 사용도 지원하지만 일반적으로 OS 라이브러리 공급업체에만 사용합니다. targetSdkVersion가 좀 더 신중한 선택 프로세스로 동일한 이점을 제공하기 때문에 Android에서도 사용하지 않습니다. 이 문서의 주제에서는 '기호 버전'과 같은 용어에 관해 걱정하지 마세요. 동일한 기호의 여러 버전을 정의하지 않는 경우 '기호 버전'은 파일 내 기호의 임의의 이름 지정된 그룹일 뿐입니다.

앱 개발자가 아닌 라이브러리 작성자 (인터페이스가 C/C++이든 Java/Kotlin이고 네이티브 코드가 단순한 구현 세부정보일 뿐인 경우) 미들웨어 공급업체를 위한 도움말도 읽어 보세요.

버전 스크립트 작성

이상적인 경우 네이티브 코드가 포함된 앱 (또는 AAR)은 정확히 하나의 공유 라이브러리를 포함하며 모든 종속 항목이 하나의 라이브러리에 정적으로 연결되어 있으며 이 라이브러리의 전체 공개 인터페이스는 JNI_OnLoad입니다. 이렇게 하면 이 페이지에 설명된 이점을 최대한 광범위하게 적용할 수 있습니다. 이 경우 라이브러리 이름이 libapp.so이라고 가정하고 다음 콘텐츠(주석은 생략할 수 있음)가 포함된 libapp.map.txt 파일을 만듭니다..map.txt

# The name used here also doesn't matter. This is the name of the "version"
# which matters when the version script is actually used to create multiple
# versions of the same symbol, but that's not what we're doing.
LIBAPP {
  global:
    # Every symbol named in this section will have "default" (that is, public)
    # visibility. See below for how to refer to C++ symbols without mangling.
    JNI_OnLoad;
  local:
    # Every symbol in this section will have "local" (that is, hidden)
    # visibility. The wildcard * is used to indicate that all symbols not listed
    # in the global section should be hidden.
    *;
};

앱에 공유 라이브러리가 두 개 이상 있는 경우 라이브러리당 버전 스크립트를 하나씩 추가해야 합니다.

JNI_OnLoadRegisterNatives()를 사용하지 않는 JNI 라이브러리의 경우 대신 각 JNI 메서드를 JNI 손상된 이름과 함께 나열할 수 있습니다.

JNI가 아닌 라이브러리 (일반적으로 JNI 라이브러리의 종속 항목)의 경우 전체 API 노출 영역을 열거해야 합니다. 인터페이스가 C가 아닌 C++인 경우 헤더 파일에서와 동일한 방식으로 버전 스크립트에서 extern "C++" { ... }를 사용할 수 있습니다. 예를 들면 다음과 같습니다.

LIBAPP {
  global:
    extern "C++" {
      # A class that exposes only some methods. Note that any methods that are
      # `private` in the class will still need to be visible in the library if
      # they are called by `inline` or `template` functions.
      #
      # Non-static members do not need to be enumerated as they do not have
      # symbols associated with them, but static members must be included.
      #
      # The * exposes all overloads of the MyClass constructor, but note that it
      # will also expose methods like MyClass::MyClassNonConstructor.
      MyClass::MyClass*;
      MyClass::DoSomething;
      MyClass::static_member;

      # All members/methods of a class, including those that are `private` in
      # the class.
      MyOtherClass::*;
      #

      # If you wish to only expose some overloads, name the full signature.
      # You'll need to wrap the name in quotes, otherwise you'll get a warning
      # like like "ignoring invalid character '(' in script" and the symbol will
      # remain hidden (pass -Wl,--no-undefined-version to convert that warning
      # to an error as described below).
      "MyClass::MyClass()";
      "MyClass::MyClass(const MyClass&)";
      "MyClass::~MyClass()";
    };
  local:
    *;
};

빌드 시 버전 스크립트 사용

빌드할 때 버전 스크립트를 링커에 전달해야 합니다. 아래에서 빌드 시스템에 적합한 단계를 따르세요.

CMake

# Assuming that your app library's target is named "app":
target_link_options(app
    PRIVATE
    -Wl,--version-script,${CMAKE_SOURCE_DIR}/libapp.map.txt
    # This causes the linker to emit an error when a version script names a
    # symbol that is not found, rather than silently ignoring that line.
    -Wl,--no-undefined-version
)

# Without this, changes to the version script will not cause the library to
# relink.
set_target_properties(app
    PROPERTIES
    LINK_DEPENDS ${CMAKE_SOURCE_DIR}/libapp.map.txt
)

ndk-build

# Add to an existing `BUILD_SHARED_LIBRARY` stanza (use `+=` instead of `:=` if
# the module already sets `LOCAL_LDFLAGS`):
LOCAL_LDFLAGS := -Wl,--version-script,$(LOCAL_PATH)/libapp.map.txt

# This causes the linker to emit an error when a version script names a symbol
# that is not found, rather than silently ignoring that line.
LOCAL_ALLOW_UNDEFINED_VERSION_SCRIPT_SYMBOLS := false

# ndk-build doesn't have a mechanism for specifying that libapp.map.txt is a
# dependency of the module. You may need to do a clean build or otherwise force
# the library to rebuild (such as by changing a source file) when altering the
# version script.

기타

사용 중인 빌드 시스템에서 버전 스크립트를 명시적으로 지원하는 경우 이를 사용하세요.

그렇지 않은 경우 다음 링커 플래그를 사용합니다.

-Wl,--version-script,path/to/libapp.map.txt -Wl,--no-version-undefined

지정 방법은 빌드 시스템에 따라 다르지만 일반적으로 LDFLAGS 또는 유사한 이름의 옵션이 있습니다. path/to/libapp.map.txt는 링커의 현재 작업 디렉터리에서 확인할 수 있어야 합니다. 절대 경로를 사용하는 것이 더 쉬울 때가 많습니다.

빌드 시스템을 사용하지 않거나 빌드 시스템 유지관리자로서 버전 스크립트 지원을 추가하려는 경우 이러한 플래그는 컴파일할 때가 아니라 연결 시 clang (또는 clang++)에 전달해야 합니다.

이점

버전 스크립트를 사용하면 라이브러리의 표시되는 기호 집합이 최소화되므로 APK 크기를 개선할 수 있습니다. 호출자가 액세스할 수 있는 함수를 링커에 정확하게 알려주면 링커는 라이브러리에서 도달할 수 없는 코드를 모두 삭제할 수 있습니다. 이 프로세스는 일종의 데드 코드 제거입니다. 링커는 숨겨지지 않은 함수 (또는 기타 기호)의 정의는 삭제할 수 없습니다. 링커는 표시된 기호가 라이브러리의 공개 인터페이스의 일부라고 가정해야 하기 때문에 함수가 호출되지 않더라도 마찬가지입니다. 기호를 숨기면 링커가 호출되지 않는 함수를 삭제하여 라이브러리 크기를 줄일 수 있습니다.

라이브러리 로드 성능도 비슷한 이유로 개선됩니다. 표시되는 기호는 교체 가능하므로 이러한 기호에는 재배치가 필요합니다. 이는 거의 바람직한 동작이 아니지만 ELF 사양에서 요구하므로 기본값입니다. 그러나 링커는 중간에 삽입할 기호를 알 수 없으므로 모든 표시 가능한 기호에 대한 재배치를 만들어야 합니다. 이러한 기호를 숨기면 링커가 직접 점프를 위해 이러한 재배치를 생략할 수 있으므로 동적 링커가 라이브러리를 로드할 때 실행해야 하는 작업량이 줄어듭니다.

또한 API 노출 영역을 명시적으로 열거하면 라이브러리 소비자가 라이브러리의 구현 세부정보에 종속되는 실수를 방지할 수 있습니다. 이러한 세부정보가 표시되지 않기 때문입니다.

대안과 비교

버전 스크립트는 -fvisibility=hidden 또는 함수별 __attribute__((visibility("hidden")))와 같은 대안과 유사한 결과를 제공합니다. 세 가지 접근 방식 모두 다른 라이브러리와 dlsym에 표시되는 라이브러리의 기호를 제어합니다.

다른 두 가지 접근 방식의 가장 큰 단점은 빌드 중인 라이브러리에 정의된 기호만 숨길 수 있다는 점입니다. 라이브러리의 정적 라이브러리 종속 항목에서 기호를 숨길 수 없습니다. 이로 인해 차이가 발생하는 매우 일반적인 경우는 libc++_static.a를 사용하는 경우입니다. 빌드에서 -fvisibility=hidden를 사용하더라도 라이브러리 자체 기호는 숨겨지지만 libc++_static.a에 포함된 모든 기호는 라이브러리의 공개 기호가 됩니다. 반면 버전 스크립트는 라이브러리의 공개 인터페이스를 명시적으로 제어합니다. 기호가 버전 스크립트에 표시되도록 명시적으로 나열되지 않으면 숨겨집니다.

다른 차이점은 장점과 단점 모두일 수 있습니다. 라이브러리의 공개 인터페이스는 버전 스크립트에 명시적으로 정의되어야 합니다. JNI 라이브러리의 경우 이는 실제로 사소한 일입니다. JNI 라이브러리에 필요한 유일한 인터페이스는 JNI_OnLoad이기 때문입니다 (RegisterNatives()에 등록된 JNI 메서드는 공개일 필요가 없음). 대규모 공개 인터페이스가 있는 라이브러리의 경우 추가 유지보수 부담이 될 수 있지만 일반적으로 그만한 가치가 있습니다.