این صفحه توضیح میدهد که چگونه برنامه شما میتواند از عملکرد سیستمعامل جدید هنگام اجرا در نسخههای سیستمعامل جدید استفاده کند و در عین حال سازگاری با دستگاههای قدیمیتر را حفظ کند.
بهطور پیشفرض، ارجاع به APIهای NDK در برنامه شما، مرجع قوی هستند. زمانی که کتابخانه شما بارگیری می شود، لودر پویا اندروید مشتاقانه آنها را حل می کند. اگر نمادها پیدا نشد، برنامه لغو می شود. این برخلاف رفتار جاوا است، جایی که تا زمانی که API از دست رفته فراخوانی نشود، استثنا ایجاد نخواهد شد.
به همین دلیل، NDK از ایجاد ارجاعات قوی به APIهایی که جدیدتر از minSdkVersion
برنامه شما هستند جلوگیری می کند. این از شما در برابر ارسال تصادفی کدی محافظت میکند که در طول آزمایش شما کار میکرد اما بارگیری نمیشود ( UnsatisfiedLinkError
از System.loadLibrary()
در دستگاههای قدیمیتر پرتاب میشود. از سوی دیگر، نوشتن کدی که از API های جدیدتر از minSdkVersion
برنامه شما استفاده می کند دشوارتر است، زیرا باید API ها را با استفاده از dlopen()
و dlsym()
فراخوانی کنید تا یک فراخوانی تابع معمولی.
جایگزین استفاده از مراجع قوی استفاده از مراجع ضعیف است. یک مرجع ضعیف که هنگام بارگیری کتابخانه یافت نمی شود، به جای اینکه بارگذاری نشود، نشانی آن نماد روی nullptr
تنظیم می شود. هنوز نمی توان آنها را به طور ایمن فراخوانی کرد، اما تا زمانی که سایت های تماس برای جلوگیری از فراخوانی API در زمانی که در دسترس نیست محافظت می شوند، بقیه کد شما می تواند اجرا شود و می توانید به طور معمول بدون نیاز به استفاده از dlopen()
و dlsym()
با API تماس بگیرید. dlsym()
.
مراجع ضعیف API نیازی به پشتیبانی اضافی از پیوند دهنده پویا ندارند، بنابراین می توان آنها را با هر نسخه از Android استفاده کرد.
فعال کردن مراجع ضعیف API در ساخت شما
CMake
هنگام اجرای CMake -DANDROID_WEAK_API_DEFS=ON
را پاس کنید. اگر از CMake از طریق externalNativeBuild
استفاده می کنید، موارد زیر را به build.gradle.kts
خود اضافه کنید (یا معادل Groovy اگر هنوز از build.gradle
استفاده می کنید):
android {
// Other config...
defaultConfig {
// Other config...
externalNativeBuild {
cmake {
arguments.add("-DANDROID_WEAK_API_DEFS=ON")
// Other config...
}
}
}
}
ndk-build
موارد زیر را به فایل Application.mk
خود اضافه کنید:
APP_WEAK_API_DEFS := true
اگر قبلاً فایل Application.mk
ندارید، آن را در همان فهرستی که فایل Android.mk
خود دارید ایجاد کنید. تغییرات اضافی در فایل build.gradle.kts
(یا build.gradle
) شما برای ndk-build ضروری نیست.
سیستم های ساخت دیگر
اگر از CMake یا ndk-build استفاده نمیکنید، با مستندات مربوط به سیستم ساخت خود مشورت کنید تا ببینید آیا روش پیشنهادی برای فعال کردن این ویژگی وجود دارد یا خیر. اگر سیستم ساخت شما به صورت بومی از این گزینه پشتیبانی نمی کند، می توانید با عبور دادن پرچم های زیر هنگام کامپایل، این ویژگی را فعال کنید:
-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability
اولی هدرهای NDK را به گونه ای پیکربندی می کند که به ارجاعات ضعیف اجازه دهد. دوم هشدار تماس های ناامن API را به خطا تبدیل می کند.
برای اطلاعات بیشتر به راهنمای Build System Maintainers مراجعه کنید.
تماس های API محافظت شده
این ویژگی به طور جادویی تماس با API های جدید را ایمن نمی کند. تنها کاری که انجام می دهد این است که خطای زمان بارگذاری را به خطای زمان تماس موکول می کند. مزیت این است که میتوانید از تماس در زمان اجرا محافظت کنید و به آرامی به عقب برگردید، چه با استفاده از یک پیادهسازی جایگزین یا اطلاع دادن به کاربر مبنی بر اینکه آن ویژگی برنامه در دستگاهش در دسترس نیست، یا به طور کامل از آن مسیر کد اجتناب کنید.
وقتی با APIی که برای minSdkVersion
برنامه شما در دسترس نیست، تماس بدون محافظ برقرار میکنید، Clang میتواند یک اخطار صادر کند ( unguarded-availability
). اگر از ndk-build یا فایل زنجیره ابزار CMake ما استفاده می کنید، این هشدار به طور خودکار فعال می شود و هنگام فعال کردن این ویژگی به خطا ارتقا می یابد.
در اینجا نمونهای از کدهایی است که با استفاده از dlopen()
و dlsym()
از یک API بدون فعال بودن این ویژگی استفاده میکنند:
void LogImageDecoderResult(int result) {
void* lib = dlopen("libjnigraphics.so", RTLD_LOCAL);
CHECK_NE(lib, nullptr) << "Failed to open libjnigraphics.so: " << dlerror();
auto func = reinterpret_cast<decltype(&AImageDecoder_resultToString)>(
dlsym(lib, "AImageDecoder_resultToString")
);
if (func == nullptr) {
LOG(INFO) << "cannot stringify result: " << result;
} else {
LOG(INFO) << func(result);
}
}
خواندن آن کمی کثیف است، نام توابع تکراری است (و اگر در حال نوشتن C، امضاها نیز هستید)، با موفقیت ساخته میشود، اما اگر اشتباهاً نام تابع ارسال شده به dlsym
را تایپ کنید، همیشه در زمان اجرا نسخه بازگشتی را انجام میدهد. ، و شما باید از این الگو برای هر API استفاده کنید.
با مراجع ضعیف API، تابع بالا را می توان به صورت زیر بازنویسی کرد:
void LogImageDecoderResult(int result) {
if (__builtin_available(android 31, *)) {
LOG(INFO) << AImageDecoder_resultToString(result);
} else {
LOG(INFO) << "cannot stringify result: " << result;
}
}
زیر سرپوش، __builtin_available(android 31, *)
android_get_device_api_level()
را فراخوانی می کند، نتیجه را در حافظه پنهان ذخیره می کند و آن را با 31
مقایسه می کند (که سطح API است که AImageDecoder_resultToString()
را معرفی کرد).
ساده ترین راه برای تعیین مقدار مورد استفاده برای __builtin_available
این است که سعی کنید بدون محافظ (یا محافظ __builtin_available(android 1, *)
بسازید و آنچه را که پیام خطا به شما می گوید انجام دهید. به عنوان مثال، یک فراخوانی بدون محافظ به AImageDecoder_createFromAAsset()
با minSdkVersion 24
ایجاد می کند:
error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]
در این مورد، تماس باید توسط __builtin_available(android 30, *)
محافظت شود. اگر خطای ساخت وجود نداشته باشد، یا API همیشه برای minSdkVersion
شما در دسترس است و نیازی به محافظ نیست، یا بیلد شما به اشتباه پیکربندی شده است و هشدار unguarded-availability
غیرفعال است.
از طرف دیگر، مرجع NDK API چیزی در امتداد خطوط "Introduced in API 30" برای هر API می گوید. اگر آن متن وجود نداشته باشد، به این معنی است که API برای تمام سطوح API پشتیبانی شده در دسترس است.
اجتناب از تکرار محافظ API
اگر از این استفاده می کنید، احتمالاً بخش هایی از کد را در برنامه خود خواهید داشت که فقط در دستگاه های جدید قابل استفاده هستند. به جای تکرار تیک __builtin_available()
در هر یک از توابع خود، می توانید کد خود را به عنوان نیاز به سطح API مشخصی حاشیه نویسی کنید. به عنوان مثال، خود API های ImageDecoder در API 30 اضافه شده اند، بنابراین برای عملکردهایی که از آن API ها استفاده زیادی می کنند، می توانید کارهایی مانند:
#define REQUIRES_API(x) __attribute__((__availability__(android,introduced=x)))
#define API_AT_LEAST(x) __builtin_available(android x, *)
void DecodeImageWithImageDecoder() REQUIRES_API(30) {
// Call any APIs that were introduced in API 30 or newer without guards.
}
void DecodeImageFallback() {
// Pay the overhead to call the Java APIs via JNI, or use third-party image
// decoding libraries.
}
void DecodeImage() {
if (API_AT_LEAST(30)) {
DecodeImageWithImageDecoder();
} else {
DecodeImageFallback();
}
}
ویژگی های محافظان API
Clang در مورد نحوه استفاده از __builtin_available
بسیار خاص است. if (__builtin_available(...))
فقط به معنای واقعی کلمه (اگرچه احتمالاً جایگزین کلان شود) کار می کند. حتی عملیات های بی اهمیتی مانند if (!__builtin_available(...))
کار نمی کنند (Clang هشدار unsupported-availability-guard
و همچنین unguarded-availability
را منتشر می کند). این ممکن است در نسخه بعدی Clang بهبود یابد. برای اطلاعات بیشتر به شماره 33161 LLVM مراجعه کنید.
بررسیهای unguarded-availability
فقط برای محدوده عملکردی که در آن استفاده میشود اعمال میشود. Clang اخطار را منتشر می کند حتی اگر تابع با فراخوانی API فقط از داخل یک محدوده محافظت شده فراخوانی شود. برای جلوگیری از تکرار محافظها در کد خود، به اجتناب از تکرار محافظهای API مراجعه کنید.
چرا این پیش فرض نیست؟
مگر اینکه به درستی استفاده شود، تفاوت بین مراجع API قوی و مراجع API ضعیف در این است که اولی به سرعت و آشکارا خراب می شود، در حالی که دومی تا زمانی که کاربر اقدامی انجام ندهد که باعث فراخوانی API از دست رفته شود، شکست نمی خورد. هنگامی که این اتفاق می افتد، پیام خطا یک خطای زمان کامپایل واضح "AFoo_bar() در دسترس نیست" نخواهد بود، این یک خطای segfault خواهد بود. با ارجاعات قوی، پیغام خطا بسیار واضح تر است و Failing-fast پیش فرض ایمن تری است.
از آنجا که این یک ویژگی جدید است، کد موجود بسیار کمی برای مدیریت ایمن این رفتار نوشته شده است. کدهای شخص ثالثی که با اندروید نوشته نشدهاند، احتمالا همیشه این مشکل را دارند، بنابراین در حال حاضر هیچ برنامهای برای تغییر رفتار پیشفرض وجود ندارد.
ما به شما توصیه می کنیم که از این استفاده کنید، اما از آنجایی که تشخیص و اشکال زدایی مشکلات را دشوارتر می کند، باید این خطرات را آگاهانه بپذیرید نه اینکه رفتار بدون اطلاع شما تغییر کند.
هشدارها
این ویژگی برای اکثر API ها کار می کند، اما چند مورد وجود دارد که کار نمی کند.
کمترین احتمال مشکل ساز API های جدیدتر libc است. برخلاف بقیه APIهای اندروید، آنها با #if __ANDROID_API__ >= X
در سرصفحهها محافظت میشوند و نه فقط __INTRODUCED_IN(X)
که حتی از مشاهده اعلان ضعیف جلوگیری میکند. از آنجایی که قدیمی ترین سطح API از NDK های مدرن پشتیبانی می کند r21 است، متداول ترین API های libc مورد نیاز در حال حاضر در دسترس هستند. APIهای جدید libc در هر نسخه اضافه می شوند (به status.md مراجعه کنید)، اما هرچه جدیدتر باشند، احتمال بیشتری وجود دارد که یک مورد لبه باشند که تعداد کمی از توسعه دهندگان به آن نیاز داشته باشند. گفتنی است، اگر شما یکی از این توسعه دهندگان هستید، در حال حاضر باید به استفاده از dlsym()
برای فراخوانی آن API ها ادامه دهید، اگر minSdkVersion
شما قدیمی تر از API است. این یک مشکل قابل حل است، اما انجام این کار خطر شکستن سازگاری منبع را برای همه برنامهها به همراه دارد (هر کدی که حاوی چند پری APIهای libc باشد به دلیل عدم تطابق ویژگیهای availability
در libc و اعلامیههای محلی، کامپایل نمیشود)، بنابراین ما مطمئن نیستیم که آیا یا کی آن را تعمیر خواهیم کرد.
موردی که احتمالاً توسعهدهندگان بیشتری با آن مواجه میشوند زمانی است که کتابخانه حاوی API جدید جدیدتر از minSdkVersion
شما باشد. این ویژگی فقط ارجاعات نماد ضعیف را فعال می کند. چیزی به نام مرجع ضعیف کتابخانه وجود ندارد. به عنوان مثال، اگر minSdkVersion
شما 24 است، می توانید libvulkan.so
را پیوند دهید و با vkBindBufferMemory2
یک تماس محافظت شده برقرار کنید، زیرا libvulkan.so
در دستگاه هایی که با API 24 شروع می شوند در دسترس است. از طرف دیگر، اگر minSdkVersion
شما 23 بود، باید سقوط کنید. به dlopen
و dlsym
برمی گردیم زیرا کتابخانه در دستگاه هایی که فقط از API 23 پشتیبانی می کنند وجود نخواهد داشت. ما راه حل خوبی برای رفع این مورد نمی دانیم، اما در دراز مدت خود به خود حل می شود زیرا ما (در صورت امکان ) دیگر به API های جدید اجازه ایجاد کتابخانه های جدید را نمی دهد.
برای نویسندگان کتابخانه
اگر در حال توسعه کتابخانه ای برای استفاده در برنامه های اندروید هستید، باید از استفاده از این ویژگی در هدرهای عمومی خودداری کنید. می توان آن را با خیال راحت در کدهای خارج از خط استفاده کرد، اما اگر در هر کدی در سرصفحه های خود، مانند توابع درون خطی یا تعاریف الگو، به __builtin_available
تکیه کنید، همه مصرف کنندگان خود را مجبور می کنید این ویژگی را فعال کنند. به همان دلایلی که ما این ویژگی را به طور پیشفرض در NDK فعال نمیکنیم، باید از انجام آن انتخاب از طرف مصرفکنندگان خود اجتناب کنید.
اگر به این رفتار در هدرهای عمومی خود نیاز دارید، مطمئن شوید که آن را مستند کنید تا کاربران شما هم بدانند که باید این ویژگی را فعال کنند و هم از خطرات انجام این کار آگاه باشند.