การควบคุมระดับการมองเห็นสัญลักษณ์จะช่วยลดขนาด 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()
ไม่จำเป็นต้องเป็นแบบสาธารณะ) สำหรับไลบรารีที่มีอินเทอร์เฟซสาธารณะขนาดใหญ่ การดำเนินการนี้อาจเพิ่มภาระการบำรุงรักษา แต่โดยทั่วไปแล้วการดำเนินการนี้คุ้มค่า