नए एपीआई इस्तेमाल करना

इस पेज पर बताया गया है कि आपका ऐप्लिकेशन, ओएस के नए वर्शन पर काम करते समय, ओएस की नई सुविधाओं का इस्तेमाल कैसे कर सकता है. साथ ही, यह भी बताया गया है कि ऐप्लिकेशन को पुराने डिवाइसों के साथ काम करने की सुविधा कैसे मिलती है.

डिफ़ॉल्ट रूप से, आपके ऐप्लिकेशन में NDK API के रेफ़रंस, स्ट्रॉन्ग रेफ़रंस होते हैं. आपकी लाइब्रेरी लोड होने के बाद, Android का डाइनैमिक लोडर उन्हें हल कर देगा. अगर सिंबल नहीं मिलते हैं, तो ऐप्लिकेशन बंद हो जाएगा. यह Java के काम करने के तरीके से अलग है. Java में, जब तक मौजूद न होने वाले एपीआई को कॉल नहीं किया जाता, तब तक कोई अपवाद नहीं दिखाया जाता.

इस वजह से, NDK टूल आपको अपने ऐप्लिकेशन के minSdkVersion से नए एपीआई के लिए, सटीक रेफ़रंस बनाने से रोकेगा. इससे, आपको गलती से ऐसा कोड शिप करने से बचाया जाता है जो टेस्टिंग के दौरान काम करता है, लेकिन पुराने डिवाइसों पर लोड नहीं होता (UnsatisfiedLinkError को System.loadLibrary() से थ्रो किया जाएगा). दूसरी ओर, आपके ऐप्लिकेशन के minSdkVersion से नए एपीआई का इस्तेमाल करने वाला कोड लिखना ज़्यादा मुश्किल होता है. इसकी वजह यह है कि आपको सामान्य फ़ंक्शन कॉल के बजाय, dlopen() और dlsym() का इस्तेमाल करके एपीआई को कॉल करना होगा.

मज़बूत रेफ़रंस के बजाय, कमज़ोर रेफ़रंस का इस्तेमाल किया जा सकता है. लाइब्रेरी लोड होने पर, अगर कोई कमज़ोर रेफ़रंस नहीं मिलता है, तो उस सिंबल का पता लोड न होने के बजाय nullptr पर सेट हो जाता है. हालांकि, अब भी इन कॉल को सुरक्षित तरीके से कॉल नहीं किया जा सकता. हालांकि, जब तक कॉल साइटों को एपीआई के उपलब्ध न होने पर उसे कॉल करने से रोकने के लिए सुरक्षित रखा जाता है, तब तक आपका बाकी कोड चलाया जा सकता है. साथ ही, dlopen() और dlsym() का इस्तेमाल किए बिना, एपीआई को सामान्य तरीके से कॉल किया जा सकता है.

कमज़ोर एपीआई रेफ़रंस के लिए, डाइनैमिक लिंकर की अतिरिक्त मदद की ज़रूरत नहीं होती. इसलिए, इनका इस्तेमाल Android के किसी भी वर्शन के साथ किया जा सकता है.

अपने बिल्ड में कमज़ोर एपीआई रेफ़रंस चालू करना

CMake

CMake चलाते समय -DANDROID_WEAK_API_DEFS=ON पास करें. अगर externalNativeBuild के ज़रिए CMake का इस्तेमाल किया जा रहा है, तो अपने 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 फ़ाइल वाली उसी डायरेक्ट्री में बनाएं. ndk-build के लिए, आपकी build.gradle.kts (या build.gradle) फ़ाइल में अन्य बदलाव करने की ज़रूरत नहीं है.

अन्य बिल्ड सिस्टम

अगर CMake या ndk-build का इस्तेमाल नहीं किया जा रहा है, तो अपने बिल्ड सिस्टम के दस्तावेज़ देखें. इससे आपको पता चलेगा कि इस सुविधा को चालू करने का सुझाया गया तरीका मौजूद है या नहीं. अगर आपका बिल्ड सिस्टम, इस विकल्प के साथ काम नहीं करता है, तो इसे चालू करने के लिए, कोड को कंपाइल करते समय ये फ़्लैग पास करें:

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

पहला, कमज़ोर रेफ़रंस की अनुमति देने के लिए NDK हेडर को कॉन्फ़िगर करता है. दूसरा, असुरक्षित एपीआई कॉल के लिए चेतावनी को गड़बड़ी में बदल देता है.

ज़्यादा जानकारी के लिए, बिल्ड सिस्टम के रखरखाव करने वालों के लिए गाइड देखें.

सुरक्षित एपीआई कॉल

इस सुविधा से, नए एपीआई को कॉल करना अपने-आप सुरक्षित नहीं हो जाता. यह सिर्फ़ लोड होने में लगने वाले समय की गड़बड़ी को कॉल के समय की गड़बड़ी में बदलता है. इसका फ़ायदा यह है कि रनटाइम के दौरान उस कॉल को सुरक्षित किया जा सकता है और उसे आसानी से बदला जा सकता है. इसके लिए, किसी अन्य तरीके का इस्तेमाल किया जा सकता है या उपयोगकर्ता को सूचना दी जा सकती है कि ऐप्लिकेशन की वह सुविधा उनके डिवाइस पर उपलब्ध नहीं है. इसके अलावा, उस कोड पाथ को पूरी तरह से हटाया जा सकता है.

अगर आपने बिना सुरक्षा वाले किसी ऐसे एपीआई को कॉल किया है जो आपके ऐप्लिकेशन के minSdkVersion के लिए उपलब्ध नहीं है, तो Clang चेतावनी (unguarded-availability) दिखा सकता है. अगर ndk-build या हमारी CMake टूलचेन फ़ाइल का इस्तेमाल किया जा रहा है, तो यह चेतावनी अपने-आप चालू हो जाएगी. साथ ही, इस सुविधा को चालू करने पर, इसे गड़बड़ी के तौर पर दिखाया जाएगा.

यहां कुछ कोड का उदाहरण दिया गया है, जो 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 में पास किए गए फ़ंक्शन के नाम में गलती से टाइप किया जाता है, तो रनटाइम के दौरान हमेशा फ़ॉलबैक का इस्तेमाल किया जाएगा. साथ ही, आपको हर एपीआई के लिए इस पैटर्न का इस्तेमाल करना होगा.

कमज़ोर एपीआई रेफ़रंस के साथ, ऊपर दिए गए फ़ंक्शन को इस तरह से फिर से लिखा जा सकता है:

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 से करता है. 31 वह एपीआई लेवल है जिसने AImageDecoder_resultToString() को लॉन्च किया था.

__builtin_available के लिए कौनसी वैल्यू का इस्तेमाल करना है, यह तय करने का सबसे आसान तरीका यह है कि आप गार्ड (या __builtin_available(android 1, *) के गार्ड) के बिना बाइनरी बनाएं और गड़बड़ी के मैसेज में बताए गए निर्देशों का पालन करें. उदाहरण के लिए, minSdkVersion 24 के साथ AImageDecoder_createFromAAsset() को बिना सुरक्षा वाला कॉल करने पर, यह दिखेगा:

error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]

इस मामले में, कॉल को __builtin_available(android 30, *) से सुरक्षित किया जाना चाहिए. अगर बिल्ड में कोई गड़बड़ी नहीं है, तो इसका मतलब है कि आपके minSdkVersion के लिए एपीआई हमेशा उपलब्ध है और गार्ड की ज़रूरत नहीं है. इसके अलावा, ऐसा भी हो सकता है कि आपका बिल्ड गलत तरीके से कॉन्फ़िगर किया गया हो और unguarded-availability चेतावनी बंद हो.

इसके अलावा, NDK API रेफ़रंस में हर एपीआई के लिए, "एपीआई लेवल 30 में लॉन्च किया गया" जैसा कुछ लिखा होगा. अगर वह टेक्स्ट मौजूद नहीं है, तो इसका मतलब है कि एपीआई, काम करने वाले सभी एपीआई लेवल के लिए उपलब्ध है.

एपीआई गार्ड को दोहराने से बचना

अगर इसका इस्तेमाल किया जा रहा है, तो हो सकता है कि आपके ऐप्लिकेशन में कोड के ऐसे सेक्शन हों जिनका इस्तेमाल सिर्फ़ नए डिवाइसों पर किया जा सकता है. अपने हर फ़ंक्शन में __builtin_available() की जांच दोहराने के बजाय, अपने कोड में एनोटेट किया जा सकता है, ताकि यह पता चल सके कि किसी खास एपीआई लेवल की ज़रूरत है. उदाहरण के लिए, ImageDecoder API खुद को एपीआई 30 में जोड़ा गया था. इसलिए, उन एपीआई का ज़्यादा इस्तेमाल करने वाले फ़ंक्शन के लिए, ऐसा कुछ किया जा सकता है:

#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();
    }
}

एपीआई गार्ड की खास बातें

Clang, __builtin_available के इस्तेमाल के तरीके को लेकर बहुत सख्त है. सिर्फ़ लिटरल (हालांकि, मैक्रो से बदला गया हो) if (__builtin_available(...)) काम करता है. if (!__builtin_available(...)) जैसे सामान्य ऑपरेशन भी काम नहीं करेंगे. Clang, unsupported-availability-guard और unguarded-availability, दोनों चेतावनियां दिखाएगा. हालांकि, Clang के आने वाले वर्शन में इस समस्या को ठीक किया जा सकता है. ज़्यादा जानकारी के लिए, LLVM Issue 33161 देखें.

unguarded-availability के लिए जांच, सिर्फ़ उस फ़ंक्शन के दायरे पर लागू होती है जहां उनका इस्तेमाल किया जाता है. Clang चेतावनी तब भी दिखाएगा, जब एपीआई कॉल वाले फ़ंक्शन को सिर्फ़ सुरक्षित दायरे में से कॉल किया गया हो. अपने कोड में गार्ड के दोहराव से बचने के लिए, एपीआई गार्ड के दोहराव से बचना लेख पढ़ें.

यह डिफ़ॉल्ट रूप से क्यों नहीं सेट होता?

सही तरीके से इस्तेमाल न करने पर, एपीआई के अच्छे रेफ़रंस और खराब रेफ़रंस के बीच का अंतर यह है कि पहले वाले रेफ़रंस तुरंत और साफ़ तौर पर काम नहीं करेंगे. वहीं, दूसरे रेफ़रंस तब तक काम नहीं करेंगे, जब तक उपयोगकर्ता कोई ऐसी कार्रवाई न करे जिससे एपीआई को कॉल किया जा सके. ऐसा होने पर, गड़बड़ी का मैसेज, संकलन के समय "AFoo_bar() उपलब्ध नहीं है" गड़बड़ी के बारे में साफ़ तौर पर नहीं बताएगा. यह एक सेगफ़ॉल्ट होगा. बेहतर रेफ़रंस की मदद से, गड़बड़ी का मैसेज ज़्यादा साफ़ तौर पर दिखता है. साथ ही, फ़ॉलिंग-फ़ास्ट एक बेहतर डिफ़ॉल्ट विकल्प है.

यह एक नई सुविधा है. इसलिए, इस व्यवहार को सुरक्षित तरीके से मैनेज करने के लिए, बहुत कम मौजूदा कोड लिखा गया है. तीसरे पक्ष के ऐसे कोड में हमेशा यह समस्या होगी जिसे Android के लिए नहीं लिखा गया है. इसलिए, फ़िलहाल डिफ़ॉल्ट व्यवहार में कोई बदलाव नहीं किया जाएगा.

हमारा सुझाव है कि आप इसका इस्तेमाल करें. हालांकि, इससे समस्याओं का पता लगाना और उन्हें डीबग करना मुश्किल हो जाएगा. इसलिए, आपको इन जोखिमों को जान-बूझकर स्वीकार करना चाहिए, ताकि आपकी जानकारी के बिना व्यवहार में बदलाव न हो.

सीमाएं

यह सुविधा ज़्यादातर एपीआई के लिए काम करती है. हालांकि, कुछ मामलों में यह काम नहीं करती.

libc के नए एपीआई में समस्या होने की संभावना कम होती है. बाकी Android एपीआई के मुकाबले, इन एपीआई को हेडर में #if __ANDROID_API__ >= X के साथ सुरक्षित किया जाता है, न कि सिर्फ़ __INTRODUCED_IN(X) के साथ. इससे, कमजोर एलान को भी नहीं देखा जा सकता. NDKs के साथ काम करने वाले सबसे पुराने एपीआई लेवल का वर्शन r21 है. इसलिए, आम तौर पर ज़रूरी libc एपीआई पहले से ही उपलब्ध हैं. हर रिलीज़ में नए libc API जोड़े जाते हैं (status.md देखें). हालांकि, नए API होने पर, यह ज़्यादा संभावना होती है कि वे एज केस हों, जिनकी ज़रूरत कुछ ही डेवलपर को होगी. हालांकि, अगर आप उन डेवलपर में से एक हैं, तो फ़िलहाल आपको उन एपीआई को कॉल करने के लिए dlsym() का इस्तेमाल करना जारी रखना होगा. ऐसा तब करना होगा, जब आपका minSdkVersion एपीआई से पुराना हो. इस समस्या को ठीक किया जा सकता है. हालांकि, ऐसा करने से सभी ऐप्लिकेशन के लिए सोर्स के साथ काम करने की सुविधा बंद हो सकती है. libc और स्थानीय एलान में availability एट्रिब्यूट के मेल न खाने की वजह से, libc API के polyfills वाले किसी भी कोड को कंपाइल नहीं किया जा सकेगा. इसलिए, हमें नहीं पता कि हम इसे कब या कैसे ठीक करेंगे.

ज़्यादातर डेवलपर को यह समस्या तब आती है, जब नए एपीआई वाली लाइब्रेरी, आपकी minSdkVersion से नई हो. यह सुविधा सिर्फ़ कमज़ोर सिंबल रेफ़रंस को चालू करती है. कमज़ोर लाइब्रेरी रेफ़रंस जैसी कोई चीज़ नहीं होती. उदाहरण के लिए, अगर आपका minSdkVersion 24 है, तो libvulkan.so को लिंक किया जा सकता है और vkBindBufferMemory2 को गार्ड किया गया कॉल किया जा सकता है, क्योंकि libvulkan.so, एपीआई 24 से शुरू होने वाले डिवाइसों पर उपलब्ध है. दूसरी ओर, अगर आपका minSdkVersion 23 था, तो आपको dlopen और dlsym पर वापस जाना होगा, क्योंकि सिर्फ़ API 23 के साथ काम करने वाले डिवाइसों पर लाइब्रेरी मौजूद नहीं होगी. हम इस समस्या को ठीक करने का कोई अच्छा तरीका नहीं जानते. हालांकि, लंबे समय में यह समस्या अपने-आप ठीक हो जाएगी, क्योंकि हम अब (जब भी संभव हो) नए एपीआई को नई लाइब्रेरी बनाने की अनुमति नहीं देते.

लाइब्रेरी के लेखकों के लिए

अगर Android ऐप्लिकेशन में इस्तेमाल करने के लिए कोई लाइब्रेरी बनाई जा रही है, तो आपको अपने सार्वजनिक हेडर में इस सुविधा का इस्तेमाल नहीं करना चाहिए. इसका इस्तेमाल, आउट-ऑफ़-लाइन कोड में सुरक्षित तरीके से किया जा सकता है. हालांकि, अगर आपने अपने हेडर में किसी भी कोड में __builtin_available का इस्तेमाल किया है, जैसे कि इनलाइन फ़ंक्शन या टेंप्लेट की परिभाषाएं, तो आपको अपने सभी उपयोगकर्ताओं को यह सुविधा चालू करने के लिए मजबूर करना होगा. हम NDK में इस सुविधा को डिफ़ॉल्ट रूप से चालू नहीं करते. इसलिए, आपको अपने उपभोक्ताओं के लिए यह विकल्प नहीं चुनना चाहिए.

अगर आपको अपने सार्वजनिक हेडर में इस तरह के व्यवहार की ज़रूरत है, तो इसकी जानकारी ज़रूर दें. इससे आपके उपयोगकर्ताओं को यह पता चलेगा कि उन्हें इस सुविधा को चालू करना होगा और उन्हें इसके जोखिमों के बारे में भी पता होगा.