کنترل نمایان بودن نماد می تواند اندازه APK را کاهش دهد، زمان بارگذاری را بهبود بخشد و به سایر توسعه دهندگان کمک کند تا از وابستگی تصادفی به جزئیات پیاده سازی جلوگیری کنند. قوی ترین راه برای انجام این کار با اسکریپت های نسخه است.
اسکریپتهای نسخه یکی از ویژگیهای پیوند دهندههای ELF هستند که میتوانند به عنوان یک شکل قویتر از -fvisibility=hidden
استفاده شوند. برای توضیح دقیق تر به مزایای زیر مراجعه کنید یا برای یادگیری نحوه استفاده از اسکریپت های نسخه در پروژه خود به ادامه مطلب مراجعه کنید.
در اسناد گنو پیوند داده شده در بالا و در چند مکان دیگر در این صفحه، ارجاعاتی به "نسخه های نماد" را مشاهده خواهید کرد. به این دلیل که هدف اصلی این فایلها این بود که به چندین نسخه از یک نماد (معمولاً یک تابع) اجازه دهند در یک کتابخانه واحد برای حفظ سازگاری باگ در کتابخانهها وجود داشته باشند. اندروید نیز از آن استفاده میکند، اما عموماً فقط برای فروشندگان کتابخانههای سیستمعامل کاربرد دارد، و حتی ما از آنها در اندروید استفاده نمیکنیم، زیرا targetSdkVersion
همان مزایا را با فرآیند انتخاب عمدیتر ارائه میدهد. برای موضوع این سند، نگران عباراتی مانند "نسخه نماد" نباشید. اگر چندین نسخه از یک نماد را تعریف نمی کنید، "نسخه نماد" فقط یک گروه بندی نام دلخواه از نمادها در فایل است.
اگر یک نویسنده کتابخانه هستید (خواه رابط شما C/C++ باشد، یا اگر جاوا/کوتلین باشد و کد بومی شما صرفاً جزییات پیاده سازی است) به جای توسعه دهنده برنامه، حتماً توصیه هایی برای فروشندگان میان افزار را نیز بخوانید.
یک اسکریپت نسخه بنویسید
در حالت ایدهآل، یک برنامه (یا 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 که از 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.
دیگر
اگر سیستم ساختی که استفاده میکنید پشتیبانی صریح از اسکریپتهای نسخه دارد، از آن استفاده کنید.
در غیر این صورت، از پرچم های پیوند دهنده زیر استفاده کنید:
-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
یا per-function __attribute__((visibility("hidden")))
ارائه می دهند. هر سه رویکرد کنترل می کنند که کدام نمادهای یک کتابخانه برای کتابخانه های دیگر و برای dlsym
قابل مشاهده است.
بزرگترین نقطه ضعف دو رویکرد دیگر این است که آنها فقط می توانند نمادهای تعریف شده در کتابخانه در حال ساخت را پنهان کنند. آنها نمی توانند نمادها را از وابستگی های کتابخانه ایستا کتابخانه پنهان کنند. یک مورد بسیار رایج که در آن تفاوت ایجاد می کند، استفاده از libc++_static.a
است. حتی اگر ساخت شما از -fvisibility=hidden
استفاده میکند، در حالی که نمادهای خود کتابخانه پنهان میشوند، همه نمادهای موجود در libc++_static.a
به نمادهای عمومی کتابخانه شما تبدیل میشوند. در مقابل، اسکریپت های نسخه کنترل صریح رابط عمومی کتابخانه را ارائه می دهند. اگر نماد به صراحت به عنوان قابل مشاهده در اسکریپت نسخه فهرست نشده باشد، پنهان خواهد شد.
تفاوت دیگر می تواند هم طرفدار و هم مخالف باشد: رابط عمومی کتابخانه باید به صراحت در یک نسخه اسکریپت تعریف شود. برای کتابخانههای JNI این در واقع بیاهمیت است، زیرا تنها رابط لازم برای کتابخانه JNI، JNI_OnLoad
است (زیرا روشهای JNI ثبتشده با RegisterNatives()
لازم نیست عمومی باشند. برای کتابخانههایی با رابط عمومی بزرگ، این میتواند یک بار تعمیر و نگهداری اضافی باشد، اما معمولاً ارزشمند است.