ควบคุมระดับการมองเห็นสัญลักษณ์

การควบคุมระดับการมองเห็นสัญลักษณ์จะช่วยลดขนาด APK, ปรับปรุงเวลาในการโหลด และช่วยให้นักพัฒนาแอปรายอื่นๆ หลีกเลี่ยงการพึ่งพารายละเอียดการใช้งานโดยไม่ตั้งใจ วิธีที่มีประสิทธิภาพมากที่สุดในการทำเช่นนี้คือการใช้สคริปต์เวอร์ชัน

สคริปต์เวอร์ชันคือฟีเจอร์ของ ELF Linker ที่ใช้เป็น-fvisibility=hiddenรูปแบบที่มีประสิทธิภาพมากขึ้นได้ ดูประโยชน์ด้านล่างเพื่อดูคำอธิบายโดยละเอียด หรืออ่านต่อเพื่อเรียนรู้วิธีใช้สคริปต์เวอร์ชันในโปรเจ็กต์ของคุณ

ในเอกสารประกอบของ GNU ที่ลิงก์ไว้ด้านบนและที่อื่นๆ อีก 2-3 แห่งในหน้านี้ คุณจะเห็นการอ้างอิงถึง "เวอร์ชันสัญลักษณ์" นั่นเป็นเพราะความตั้งใจเดิมของไฟล์เหล่านี้คืออนุญาตให้สัญลักษณ์ (มักเป็นฟังก์ชัน) หลายเวอร์ชันอยู่ในไลบรารีเดียวเพื่อรักษาความเข้ากันได้กับข้อบกพร่องในไลบรารี Android รองรับการใช้งานดังกล่าวเช่นกัน แต่โดยทั่วไปแล้วก็มีไว้สำหรับผู้ให้บริการไลบรารีระบบปฏิบัติการเท่านั้น และถึงแม้เราจะไม่ได้ใช้บริการดังกล่าวใน Android เนื่องจาก targetSdkVersion ก็ให้ประโยชน์เหมือนกันกับขั้นตอนการเลือกใช้ที่รอบคอบมากกว่า สำหรับหัวข้อของเอกสารนี้ ไม่ต้องกังวลกับคําอย่าง "สัญลักษณ์เวอร์ชัน" หากคุณไม่ได้กำหนดสัญลักษณ์เดียวกันหลายเวอร์ชัน "symbol version" จะเป็นเพียงการจัดกลุ่มสัญลักษณ์ในไฟล์ที่มีชื่อตามอำเภอใจ

หากคุณเป็นผู้เขียนไลบรารี (ไม่ว่าอินเทอร์เฟซจะเป็น C/C++ หรือ Java/Kotlin และโค้ดเนทีฟเป็นเพียงรายละเอียดการใช้งาน) ไม่ใช่นักพัฒนาแอป โปรดอ่านคําแนะนําสําหรับผู้ให้บริการมิดเดิลแวร์ด้วย

เขียนสคริปต์เวอร์ชัน

ในกรณีที่ดีที่สุด แอป (หรือ AAR) ที่มีโค้ดเนทีฟจะมีไลบรารีที่ใช้ร่วมกันเพียง 1 รายการ โดยมีทรัพยากร Dependency ทั้งหมดลิงก์แบบคงที่ไปยังไลบรารีนั้น 1 รายการ และอินเทอร์เฟซสาธารณะที่สมบูรณ์ของไลบรารีดังกล่าวคือ 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.
    *;
};

หากแอปมีไลบรารีที่ใช้ร่วมกันมากกว่า 1 รายการ คุณต้องเพิ่มสคริปต์เวอร์ชัน 1 รายการต่อการมีไลบรารี 1 รายการ

สําหรับไลบรารี JNI ที่ไม่ได้ใช้ JNI_OnLoad และ RegisterNatives() คุณสามารถแสดงเมธอด 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.

อื่นๆ

หากระบบบิลด์ที่คุณใช้รองรับสคริปต์เวอร์ชันอย่างชัดเจน ให้ใช้ระบบนั้น

หรือใช้ Flag Linker ต่อไปนี้

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

โดยวิธีระบุจะขึ้นอยู่กับระบบบิลด์ของคุณ แต่โดยทั่วไปแล้วจะมีตัวเลือกชื่อ LDFLAGS หรือชื่ออื่นที่คล้ายกัน path/to/libapp.map.txt ต้องแก้ไขได้จากไดเรกทอรีที่ใช้งานอยู่ในปัจจุบันของ linker การใช้เส้นทางแบบสัมบูรณ์มักง่ายกว่า

หากคุณไม่ได้ใช้ระบบบิลด์ หรือเป็นผู้ดูแลระบบบิลด์ที่ต้องการเพิ่มการรองรับสคริปต์เวอร์ชัน คุณควรส่ง Flag เหล่านั้นไปยัง clang (หรือ clang++) เมื่อลิงก์ แต่ไม่ควรส่งเมื่อคอมไพล์

ข้อดี

คุณปรับปรุงขนาด APK ได้เมื่อใช้สคริปต์เวอร์ชัน เนื่องจากจะลดชุดสัญลักษณ์ที่มองเห็นได้ในไลบรารี การบอก linker ว่าฟังก์ชันใดบ้างที่ผู้เรียกเข้าถึงได้จะช่วยให้ linker นำโค้ดที่เข้าถึงไม่ได้ทั้งหมดออกจากไลบรารีได้ กระบวนการนี้เป็นประเภทการกำจัดโค้ดที่ตายแล้ว Linker จะนำคําจํากัดความของฟังก์ชัน (หรือสัญลักษณ์อื่นๆ) ที่ไม่ได้ซ่อนออกไม่ได้ แม้ว่าจะไม่เคยเรียกใช้ฟังก์ชันนั้นก็ตาม เนื่องจาก Linker ต้องถือว่าสัญลักษณ์ที่มองเห็นได้เป็นส่วนหนึ่งของอินเทอร์เฟซสาธารณะของไลบรารี การซ่อนสัญลักษณ์ช่วยให้โปรแกรมลิงก์นำฟังก์ชันที่ไม่ได้เรียกใช้ออกได้ ซึ่งจะช่วยลดขนาดของไลบรารี

ประสิทธิภาพการโหลดไลบรารีได้รับการปรับปรุงด้วยเหตุผลที่คล้ายกัน นั่นคือ จำเป็นต้องย้ายสัญลักษณ์ที่มองเห็นได้เนื่องจากสัญลักษณ์เหล่านั้นซ้อนทับกันได้ ลักษณะการทำงานนี้แทบจะไม่เป็นที่ต้องการเลย แต่ก็เป็นสิ่งที่ข้อกำหนดของ ELF กำหนดให้ใช้ จึงเป็นค่าเริ่มต้น แต่เนื่องจาก Linker ไม่สามารถรู้ได้ว่าสัญลักษณ์ใด (ถ้ามี) ที่จะใช้แทนกันได้ จึงต้องสร้างตำแหน่งใหม่สำหรับสัญลักษณ์ที่มองเห็นได้ทั้งหมด การซ่อนสัญลักษณ์เหล่านั้นช่วยให้ linker ละเว้นการเปลี่ยนตำแหน่งเหล่านั้นเพื่อใช้การข้ามโดยตรง ซึ่งจะช่วยลดปริมาณงานที่ linker แบบไดนามิกต้องทำเมื่อโหลดไลบรารี

การระบุแพลตฟอร์ม API อย่างชัดเจนยังช่วยป้องกันไม่ให้ผู้บริโภคของไลบรารีของคุณอิงตามรายละเอียดการใช้งานไลบรารีโดยไม่ได้ตั้งใจด้วย เนื่องจากรายละเอียดเหล่านั้นจะไม่แสดง

การเปรียบเทียบกับทางเลือกอื่นๆ

สคริปต์เวอร์ชันให้ผลลัพธ์ที่คล้ายกับทางเลือกอื่นๆ เช่น -fvisibility=hidden หรือต่อฟังก์ชัน __attribute__((visibility("hidden"))) ทั้ง 3 วิธีควบคุมว่าสัญลักษณ์ใดของไลบรารีที่ผู้อื่นและ dlsym จะมองเห็น

ข้อเสียที่ใหญ่ที่สุดของอีก 2 วิธีคือสามารถซ่อนเฉพาะสัญลักษณ์ที่กําหนดไว้ในไลบรารีที่สร้างเท่านั้น แต่จะซ่อนสัญลักษณ์จากไลบรารีแบบคงที่ของไลบรารีไม่ได้ กรณีทั่วไปที่การดําเนินการนี้ทําให้เกิดความแตกต่างคือเมื่อใช้ libc++_static.a แม้ว่าบิลด์ของคุณจะใช้ -fvisibility=hidden แต่สัญลักษณ์ทั้งหมดของ libc++_static.a จะกลายเป็นสัญลักษณ์สาธารณะของไลบรารี แม้ว่าสัญลักษณ์ของไลบรารีเองจะซ่อนอยู่ก็ตาม ในทางตรงกันข้าม สคริปต์เวอร์ชันจะควบคุมอินเทอร์เฟซสาธารณะของไลบรารีได้อย่างชัดเจน หากไม่ได้ระบุไว้อย่างชัดเจนว่าสัญลักษณ์นั้นแสดงอยู่ในสคริปต์เวอร์ชัน สัญลักษณ์ดังกล่าวจะซ่อนอยู่

ความแตกต่างอีกอย่างหนึ่งอาจเป็นได้ทั้งข้อดีและข้อเสีย นั่นคืออินเทอร์เฟซสาธารณะของไลบรารีต้องได้รับการกำหนดอย่างชัดเจนในสคริปต์เวอร์ชัน จริงๆ แล้วสำหรับไลบรารี JNI สิ่งนี้ก็ไม่ใช่ปัญหาแต่อย่างใด เพราะอินเทอร์เฟซเดียวที่จำเป็นสำหรับไลบรารี JNI คือ JNI_OnLoad (เนื่องจากเมธอด JNI ที่ลงทะเบียนกับ RegisterNatives() ไม่จำเป็นต้องเป็นแบบสาธารณะ) สำหรับไลบรารีที่มีอินเทอร์เฟซสาธารณะขนาดใหญ่ การดำเนินการนี้อาจเพิ่มภาระการบำรุงรักษา แต่โดยทั่วไปแล้วการดำเนินการนี้คุ้มค่า