שימוש בממשקי API חדשים יותר

בדף הזה מוסבר איך האפליקציה יכולה להשתמש בפונקציונליות חדשה של מערכת ההפעלה כשהיא פועלת במכשיר חדש גרסאות של מערכות הפעלה תוך שמירה על תאימות למכשירים ישנים יותר.

כברירת מחדל, הפניות לממשקי API של NDK באפליקציה שלכם הן קובצי עזר חזקים. המטען הדינמי של Android ישמח לפתור את הבעיות האלה כשהספרייה שלך נטען. אם הסמלים לא יימצאו, האפליקציה תבוטל. סתירה זו האופן שבו Java מתנהגת, כאשר לא תוחלף חריגה עד שה-API החסר שנקראה.

לכן, ה-NDK ימנע ממך ליצור הפניות ברורות אל ממשקי API חדשים יותר מ-minSdkVersion של האפליקציה. הפעולה הזו מגינה עליך שילבנו בטעות קוד שפעל במהלך הבדיקה אבל לא נטען (המערכת תזרוק UnsatisfiedLinkError מ-System.loadLibrary()) במכשירים ישנים יותר מכשירים. מצד שני, קשה יותר לכתוב קוד שעושה שימוש בממשקי API חדש יותר מ-minSdkVersion של האפליקציה, כי עליך לקרוא לממשקי ה-API באמצעות את dlopen() ואת dlsym() ולא בקשה רגילה לפונקציה.

החלופה לשימוש בהפניות חזקות היא שימוש בהפניות חלשות. A חלש הפניה שלא נמצאה כשהספרייה נטענת שהסמל מוגדר ל-nullptr במקום שטעינתו תיכשל. הם עדיין לא ניתן להתקשר בבטחה, אלא כל עוד אתרי קריאה מוגנים כדי למנוע ביצוע שיחות את ה-API כאשר הוא לא זמין, את שאר הקוד תוכלו להריץ, קוראים ל-API כרגיל בלי להשתמש ב-dlopen() וב-dlsym().

הפניות API חלשות לא דורשות תמיכה נוספת מהמקשר הדינמי, כך שאפשר להשתמש בהן בכל גרסה של Android.

הפעלת הפניות API חלשות ב-build

CMake

עוברים -DANDROID_WEAK_API_DEFS=ON כשמריצים את CMake. אם משתמשים ב-CMake באמצעות externalNativeBuild, יש להוסיף את הערכים הבאים אל build.gradle.kts (או אל שווה ערך מגניב אם עדיין משתמשים ב-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, צריך לעיין במסמכי התיעוד של ה-build כדי לבדוק אם יש דרך מומלצת להפעיל את התכונה הזו. אם גרסת ה-build שלך המערכת לא תומכת באפשרות הזו במקור, אפשר להפעיל את התכונה על ידי העברת הדגלים הבאים במהלך ההידור:

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

הראשונה מגדירה את כותרות ה-NDK כך שיאפשרו קובצי עזר חלשים. התור השני הצגת האזהרה על קריאות לא בטוחות ל-API.

מידע נוסף זמין במדריך למתחזקים של מערכות Build.

קריאות ל-API מוגנות

התכונה הזו לא מבצעת באורח פלא קריאות לממשקי API חדשים בצורה בטוחה. הדבר היחיד הוא לדחות שגיאה בזמן טעינה לשגיאה בזמן השיחה. היתרון הוא שאתם יכולה לשמור על הקריאה הזו בזמן הריצה ולחזור באלגנטיות, בין אם באמצעות יישום חלופי או הודעה למשתמש שהתכונה הזו באפליקציה לא זמין במכשיר שלהם, או להימנע לגמרי מנתיב הקוד הזה.

קלנג יכול לקלוט אזהרה (unguarded-availability) כשמבצעים פעולה של ל-API שלא זמין ל-minSdkVersion של האפליקציה שלך. אם אתם באמצעות ndk-build או את קובץ ה-toolchain של CMake, האזהרה תתקבל באופן אוטומטי מופעלת ומקודמת לשגיאה בעת הפעלת התכונה הזו.

לפניכם דוגמה לקוד שמבצע שימוש מותנה ב-API ללא התכונה הזו מופעלת באמצעות dlopen() ו-dlsym():

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, *). אם אין שגיאת build, אז ה-API תמיד זמין minSdkVersion ולא צריך אמצעי הגנה, או שה-build שלך מוגדר באופן שגוי האזהרה unguarded-availability מושבתת.

לחלופין, בהפניה של NDK API יופיע משהו כמו 'הושקה ב-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. צפייה לקבלת מידע נוסף, אפשר לעיין במאמר LLVM Issue 33161.

הבדיקות של unguarded-availability חלות רק על היקף הפונקציה שבו הן בשימוש. Clang ישמיע את האזהרה גם אם הפונקציה עם הקריאה ל-API בוצעה קריאה רק מתוך היקף מוגן. כדי להימנע מחזרה על שומרים את הקוד אפשר למצוא במאמר בנושא הימנעות מחזרה על אמצעי הגנה על API.

מדוע זו לא ברירת המחדל?

אם לא משתמשים בצורה נכונה, ההבדל בין הפניות API חזקות לבין API חלש היא שהאסטרטגיה הראשונה תיכשל במהירות וברורה, ואילו רכיבי ה-API לא ייכשלו עד שהמשתמש יבצע פעולה שגורמת ל-API החסר. שצריך לקרוא לו. במקרה כזה, הודעת השגיאה לא תהיה ברורה הידור-זמן "AFoo_bar() אינו זמין" שגיאה, זו תהיה שגיאה. ב- הפניות מבוססות, הודעת השגיאה הרבה יותר ברורה ומהירות נכשלת ברירת מחדל בטוחה יותר.

מדובר בתכונה חדשה, ולכן נכתב מעט מאוד קוד קיים כדי לטפל בה את ההתנהגות הזאת בצורה בטוחה. קוד של צד שלישי שלא נכתב תוך התייחסות ל-Android סביר להניח שתמיד תהיה את הבעיה הזו, לכן אין כרגע תוכניות ולא לשנות את התנהגות ברירת המחדל שלהם.

אנחנו ממליצים להשתמש בזה, אבל זה יגרום לבעיות רבות יותר שקשה לזהות ולנפות באגים, צריך לקבל את הסיכונים האלה ביודעין מאשר ההתנהגות המשתנה ללא ידיעתך.

נקודות שצריך לשים לב אליהן:

התכונה הזו עובדת ברוב ממשקי ה-API, אבל יש כמה מקרים שבהם היא לא בעבודה.

הסבירות הכי פחות בעייתית היא ממשקי API חדשים יותר של libc. בניגוד לשאר ממשקי API של Android, שמורים באמצעות #if __ANDROID_API__ >= X בכותרות ולא רק __INTRODUCED_IN(X), ולכן גם הצהרה חלשה לראות אותו. מאחר שהתמיכה הישנה ביותר ברמת ה-API המודרנית של NDK היא r21, כבר קיימים ממשקי API של libc, המערכת מוסיפה ממשקי API חדשים של libc גרסה חדשה יותר (ראו status.md), אבל ככל שהיא חדשה יותר, כך יש סיכוי גבוה יותר הוא גרסת קצה שכמה מפתחים יצטרכו. עם זאת, אם אתם המפתחים האלה, כרגע צריך להמשיך להשתמש ב-dlsym() כדי לקרוא להם ממשקי API אם ה-minSdkVersion שלכם ישן יותר מה-API. זוהי בעיה שניתנת לפתרון, אבל הדבר עלול לגרום לשבירה של תאימות המקור לכל האפליקציות ( קוד שמכיל polyfills של ממשקי API של libc לא יוכל להדר בגלל חוסר התאמה במאפייני availability ב-libc ובהצהרות המקומיות), לכן אנחנו לא בטוחים אם או מתי נתקן את הבעיה.

סביר להניח שיותר מפתחים ייתקלו בספרייה מכיל את ה-API החדש חדש יותר מה-minSdkVersion שלך. התכונה הזו בלבד הפעלת הפניות לסמלים חלשים; אין דבר כזה ספרייה חלשה הפניה. לדוגמה, אם מספר minSdkVersion הוא 24, אפשר לקשר libvulkan.so ולבצע קריאה מאובטחת ל-vkBindBufferMemory2, כי libvulkan.so זמין במכשירים עם API 24 בלבד. לעומת זאת, אם minSdkVersion היה 23, עליך לחזור אל dlopen ואל dlsym כיוון שהספרייה לא תהיה קיימת במכשיר במכשירים שתומכים רק API 23. אנו לא יודעים על פתרון טוב לתיקון המקרה, אבל הוא יפתור את עצמו מפני (כשזה אפשרי) אנחנו לא מאפשרים ממשקי API ליצירת ספריות חדשות.

למחברי ספרייה

אם אתם מפתחים ספרייה לשימוש באפליקציות ל-Android, להימנע משימוש בתכונה הזו בכותרות הציבוריות שלכם. ניתן להשתמש בה בבטחה קוד חדש, אבל אם הסתמכת על __builtin_available בקוד כלשהו , כמו פונקציות בתוך השורה או הגדרות של תבניות, מאלצים את כל לצרכנים להפעיל את התכונה הזו. מאותן סיבות, אנחנו לא מפעילים את האפשרות הזו כברירת מחדל ב-NDK, עליך להימנע מבחירה באפשרות הזו בקרב הצרכנים.

אם נדרשת התנהגות כזו בכותרות הציבוריות שלכם, חשוב לתעד כדי שהמשתמשים שלכם ידעו שהם יצטרכו להפעיל את התכונה, להיות מודעים לסיכונים הכרוכים בכך.