Daha yeni API'leri kullanma

Bu sayfada, uygulamanızın yeni işletim sistemi sürümlerinde çalışırken eski cihazlarla uyumluluğu korurken yeni işletim sistemi işlevini nasıl kullanabileceği açıklanmaktadır.

Varsayılan olarak, uygulamanızdaki NDK API'lerine yapılan referanslar güçlü referanslardır. Android'in dinamik yükleyicisi, kitaplığınız yüklendiğinde bu sorunları çözmek için istekle çalışır. Semboller bulunmazsa uygulama iptal eder. Bu, Java'nın davranışına aykırıdır ve eksik API çağrılana kadar bir istisna uygulanmaz.

Bu nedenle NDK, uygulamanızın minSdkVersion sürümünden daha yeni olan API'lere güçlü referanslar oluşturmanızı engeller. Bu, testiniz sırasında düzgün çalışan ancak eski cihazlarda yüklenemeyen (UnsatisfiedLinkError, System.loadLibrary() üzerinden atılır) sizi yanlışlıkla gönderime karşı korur. Diğer yandan, API'leri normal bir işlev çağrısı yerine dlopen() ve dlsym() kullanarak çağırmanız gerektiğinden, uygulamanızın minSdkVersion sürümünden daha yeni API'leri kullanan kod yazmak daha zordur.

Güçlü referanslar kullanmanın alternatifi, zayıf referanslar kullanmaktır. Kitaplık yüklendiğinde bulunmayan zayıf bir referans, söz konusu simgenin adresinin yüklenmemesi yerine nullptr olarak ayarlanmasıyla sonuçlanır. Bunlar yine de güvenli bir şekilde çağrılamaz ancak çağrı siteleri, kullanılamadığında API'nin çağrılmasını önleyecek şekilde korunduğu sürece kodunuzun geri kalanı çalıştırılabilir ve dlopen() ve dlsym() kullanmak zorunda kalmadan API'yi normal şekilde çağırabilirsiniz.

Zayıf API referansları dinamik bağlayıcıdan ek destek gerektirmez, bu nedenle Android'in herhangi bir sürümüyle kullanılabilir.

Derlemenizde zayıf API referanslarını etkinleştirme

CMake

CMake çalıştırırken -DANDROID_WEAK_API_DEFS=ON öğesini geçirin. CMake'i externalNativeBuild üzerinden kullanıyorsanız aşağıdakini build.gradle.kts sayfanıza (veya hâlâ build.gradle kullanıyorsanız geçerli eşdeğerine) ekleyin:

android {
    // Other config...

    defaultConfig {
        // Other config...

        externalNativeBuild {
            cmake {
                arguments.add("-DANDROID_WEAK_API_DEFS=ON")
                // Other config...
            }
        }
    }
}

ndk-kurum

Application.mk dosyanıza aşağıdakileri ekleyin:

APP_WEAK_API_DEFS := true

Henüz Application.mk dosyanız yoksa bu dosyayı Android.mk dosyanızla aynı dizinde oluşturun. ndk-build için build.gradle.kts (veya build.gradle) dosyanızda ek değişiklik yapılmasına gerek yoktur.

Diğer derleme sistemleri

CMake veya ndk-build kullanmıyorsanız bu özelliği etkinleştirmenin önerilen bir yolu olup olmadığını öğrenmek için derleme sisteminizin belgelerine bakın. Derleme sisteminiz bu seçeneği yerel olarak desteklemiyorsa derleme sırasında aşağıdaki işaretleri ileterek özelliği etkinleştirebilirsiniz:

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

İlki, NDK başlıklarını zayıf referanslara izin verecek şekilde yapılandırır. İkincisi ise güvenli olmayan API çağrılarıyla ilgili uyarıyı hataya dönüştürür.

Daha fazla bilgi için Derleme Sistem Bakımı Kılavuzu'na bakın.

Korumalı API çağrıları

Bu özellik, sihirli bir şekilde yeni API'lere yapılan çağrıların güvenli olmasını sağlamaz. Yaptığı tek şey, yükleme süresi hatasını arama zamanı hatasına ertelemektir. Avantajı, alternatif bir uygulama kullanarak, uygulamadaki bu özelliğin cihazında kullanılamadığını kullanıcıya bildirerek veya bu kod yolundan tamamen kaçınarak çağrıyı çalışma zamanında koruyabilmeniz ve sorunsuz bir şekilde geri çekebilmenizdir.

Clang, uygulamanızın minSdkVersion ayarında kullanılamayan bir API'ye güvenliksiz çağrı yaptığınızda uyarı (unguarded-availability) yayınlayabilir. ndk-build veya CMake araç zinciri dosyamızı kullanıyorsanız bu özellik etkinleştirildiğinde bu uyarı otomatik olarak etkinleştirilir ve hataya yükseltilir.

Bu özellik etkinleştirilmeden, dlopen() ve dlsym() kullanılarak bir API'den koşullu kullanım sağlayan bazı kod örneğini burada bulabilirsiniz:

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

Okuması biraz karmaşıktır. İşlev adlarının bir kısmı yinelenebilir (ve C, imzaları da yazıyorsanız), başarılı bir şekilde derlenir, ancak dlsym işlevine iletilen işlev adını yanlışlıkla yazım hatası yaparsanız ve her API için bu kalıbı kullanmanız gerekirse çalışma zamanında yedeği her zaman alır.

Zayıf API referansları kullanıldığında yukarıdaki işlev şu şekilde yeniden yazılabilir:

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, *), arka planda android_get_device_api_level() yöntemini çağırır, sonucu önbelleğe alır ve 31 ile (AImageDecoder_resultToString()'yı kullanıma sunan API düzeyi) karşılaştırır.

__builtin_available için hangi değerin kullanılacağını belirlemenin en basit yolu, güvenlik görevlisi (veya __builtin_available(android 1, *) güvenlikçisi) olmadan derlemeye çalışmak ve hata mesajında belirtilenleri yapmaktır. Örneğin, minSdkVersion 24 ile AImageDecoder_createFromAAsset() için yapılan güvenliksiz bir çağrı şu sonuçları doğurur:

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

Bu durumda, arama __builtin_available(android 30, *) tarafından korunmalıdır. Derleme hatası yoksa minSdkVersion cihazınız için API her zaman kullanılabilir ve koruma gerekmez ya da derlemeniz yanlış yapılandırılmıştır ve unguarded-availability uyarısı devre dışıdır.

Alternatif olarak, NDK API referansında her API için "API 30'da kullanıma sunuldu" gibi bir ifade verilir. Bu metin yoksa API, desteklenen tüm API düzeylerinde kullanılabilir.

API korumalarının tekrarından kaçınma

Bunu kullanıyorsanız muhtemelen uygulamanızda yalnızca yeterince yeni cihazlarda kullanılabilen kod bölümleriniz olacaktır. __builtin_available() kontrolünü işlevlerinizin her birinde tekrarlamak yerine, belirli bir API düzeyi gerektiriyorsa kendi kodunuza ek açıklama ekleyebilirsiniz. Örneğin, ImageDecoder API'lerinin kendileri API 30'a eklenmiştir. Dolayısıyla, bu API'leri yoğun bir şekilde kullanan işlevler için aşağıdaki gibi bir işlem yapabilirsiniz:

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

İlginç API korumaları

Clang, __builtin_available'nin nasıl kullanıldığı konusunda son derece özeldir. Yalnızca bir değişmez değer (ancak muhtemelen makroyla değiştirilmiş) if (__builtin_available(...)) çalışır. if (!__builtin_available(...)) gibi basit işlemler bile çalışmaz (Clang, unguarded-availability ve unsupported-availability-guard uyarısını gönderir). Bu, Clang'ın gelecekteki bir sürümünde iyileştirilebilir. Daha fazla bilgi için LLVM Sorunu 33161'e bakın.

unguarded-availability kontrolleri yalnızca kullanıldıkları işlev kapsamı için geçerlidir. Clang, API çağrısına sahip işlev yalnızca korumalı bir kapsamdan çağrılsa bile uyarı verir. Kendi kodunuzda korumaların tekrarlanmasını önlemek için API korumalarının tekrarını önleme bölümüne bakın.

Bu neden varsayılan ayar değil?

Doğru kullanılmadığı takdirde güçlü API referansları ile zayıf API referansları arasındaki fark, ilk referansın hızla ve bariz bir şekilde başarısız olması, ikincisinin ise kullanıcı eksik API'nin çağrılmasına neden olan bir işlem yapana kadar başarısız olmamasıdır. Bu durumda hata mesajı, net bir derleme zamanı ("AFoo_bar() kullanılamıyor" hatası) değil, bir hata mesajıdır. Güçlü referanslarla, hata mesajı çok daha net olur ve hata verme hızı daha güvenli bir varsayılan ayardır.

Bu yeni bir özellik olduğundan, bu davranışı güvenli bir şekilde işlemek için çok az mevcut kod yazılır. Android düşünülerek yazılmamış üçüncü taraf kodlarında bu sorun muhtemelen her zaman olacaktır. Bu nedenle, şu anda varsayılan davranışın değişmesiyle ilgili bir plan bulunmamaktadır.

Bu yöntemi kullanmanızı öneririz ancak bu yöntem, sorunların tespit edilmesini ve hata ayıklamasını daha da zorlaştıracağından, davranış, bilginiz dışında değişmek yerine, bu riskleri bilinçli olarak kabul etmeniz gerekir.

Uyarılar

Bu özellik çoğu API'de kullanılabilir ancak çalışmadığı birkaç durum vardır.

Sorunlu olabilecek en düşük tür, daha yeni libc API'leridir. Diğer Android API'lerinden farklı olarak bunlar sadece __INTRODUCED_IN(X) değil, başlıklarda #if __ANDROID_API__ >= X ile korunur. Böylece zayıf bildirimin bile görülmesini önler. API düzeyindeki en eski modern NDK desteği r21 olduğundan, en yaygın olarak ihtiyaç duyulan libc API'leri zaten mevcuttur. Her sürümde yeni libc API'leri eklenir (bkz. status.md). Ancak bunlar ne kadar yeni olursa az sayıda geliştiricinin ihtiyaç duyacağı uç durum oluşturma ihtimali de o kadar artar. Bununla birlikte, bu geliştiricilerden biriyseniz ve minSdkVersion öğenizin API'den daha eski olması halinde şimdilik bu API'leri çağırmak için dlsym() hizmetini kullanmaya devam etmeniz gerekecektir. Bu çözülebilir bir sorundur ancak bunu yapmak, tüm uygulamalar için kaynak uyumluluğunu bozma riskini taşır (libc API'lerinin çoklu dolgularını içeren tüm kodlar, libc ve yerel beyanlardaki eşleşmemiş availability özellikleri nedeniyle derlenemez). Bu nedenle, sorunu düzeltip düzeltmeyeceğimizden veya ne zaman düzelteceğimizden emin değiliz.

Yeni API'yi içeren kitaplık, minSdkVersion öğenizden daha yeni olduğunda daha fazla geliştiriciyle karşılaşırsınız. Bu özellik yalnızca zayıf sembol referanslarını etkinleştirir. Zayıf kitaplık referansı diye bir şey yoktur. Örneğin, minSdkVersion değeriniz 24 ise libvulkan.so API 24 ile başlayan cihazlarda kullanılabildiğinden libvulkan.so ile vkBindBufferMemory2 cihazını bağlayarak korumalı bir çağrı yapabilirsiniz. Diğer yandan, minSdkVersion değeriniz 23 ise dlopen ve dlsym kitaplığına dönmeniz gerekir. Çünkü kitaplık, cihazda yalnızca API 23'ü destekleyen cihazlarda yer almaz. Bu durumun düzeltilmesi için iyi bir çözüm bilmiyoruz ancak uzun vadede kendi kendine çözülecektir çünkü (mümkünse) artık yeni API'lerin yeni kitaplıklar oluşturmasına izin veremeyeceğiz.

Kütüphane yazarları için

Android uygulamalarında kullanılacak bir kitaplık geliştiriyorsanız herkese açık başlıklarınızda bu özelliği kullanmaktan kaçınmalısınız. Satır dışı kodda güvenli bir şekilde kullanılabilir ancak başlıklarınızdaki herhangi bir kodda (satır içi işlevler veya şablon tanımları gibi) __builtin_available kullanırsanız tüm tüketicilerinizi bu özelliği etkinleştirmeye zorlarsınız. Aynı nedenlerle, NDK'da bu özelliği varsayılan olarak etkinleştirmediğimiz için, tüketicileriniz adına bu seçimi yapmaktan kaçınmanız gerekir.

Herkese açık başlıklarınızda bu davranışa ihtiyaç duyarsanız kullanıcılarınızın özelliği etkinleştirmeleri gerektiğini bildikleri ve bunu yapmanın risklerinin farkında olmaları için bunu belgelediğinizden emin olun.