Daha yeni API'leri kullanma

Bu sayfada, uygulamanızın yeni işletim sistemi sürümlerinde çalışırken eski cihazlarla uyumluluğunu koruyarak yeni işletim sistemi işlevlerini 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 bunları çözmeye çalışır. Semboller bulunmazsa uygulama iptal eder. Bu, eksik API çağrılana kadar istisna atılmayan Java'nın davranışına aykırıdır.

Bu nedenle NDK, uygulamanızın minSdkVersion sürümünden daha yeni API'lere güçlü referanslar oluşturmanızı engeller. Bu sayede, testiniz sırasında çalışan ancak eski cihazlarda yüklenemeyen (UnsatisfiedLinkError, System.loadLibrary() tarafından atılır) kodları yanlışlıkla göndermekten korunursunuz. Öte yandan, normal bir işlev çağrısı yerine dlopen() ve dlsym() kullanarak API'leri ç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ın alternatifi zayıf referanslardır. Kitaplık yüklenirken bulunmayan zayıf bir referans, söz konusu sembolün adresinin yüklenmemesi yerine nullptr olarak ayarlanmasına neden olur. Bu API'ler güvenli şekilde çağrılamıyor olsa da API'nin kullanılamadığı durumlarda çağrılmasını önlemek için çağrı siteleri korunduğu sürece kodunuzun geri kalanı çalıştırılabilir ve dlopen() ile dlsym() kullanmanıza gerek kalmadan API'yi normal şekilde çağırabilirsiniz.

Zayıf API referansları, dinamik bağlayıcıdan ek destek gerektirmediğinden Android'in herhangi bir sürümünde kullanılabilir.

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

CMake

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

android {
    // Other config...

    defaultConfig {
        // Other config...

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

ndk-build

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

APP_WEAK_API_DEFS := true

Henüz bir Application.mk dosyanız yoksa 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ı görmek için derleme sisteminizin belgelerine bakın. Derleme sisteminiz bu seçeneği doğal olarak desteklemiyorsa derleme sırasında aşağıdaki işaretçileri ileterek özelliği etkinleştirebilirsiniz:

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

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

Daha fazla bilgi için Oluşturma Sistemi Yöneticileri Kılavuzu'na bakın.

Korunan API çağrıları

Bu özellik, sihirli bir şekilde yeni API'lere yapılan çağrıların güvenli olmasını sağlamaz. Tek yaptığı, yükleme zamanı hatasını çağrı zamanı hatasına ertelemek. Bunun avantajı, bu çağrıyı çalışma zamanında koruyabilmeniz ve alternatif bir uygulama kullanarak ya da kullanıcıyı uygulamanın bu özelliğinin cihazında kullanılamadığını bildirerek ya da bu kod yolunu tamamen atlayarak sorunsuz bir şekilde yedek çözüme geçebilmenizdir.

Clang, uygulamanızın minSdkVersion cihazı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.

Aşağıda, bu özellik etkinleştirilmeden API'nin koşullu kullanımını sağlayan dlopen() ve dlsym() kod örneği verilmiştir:

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

Kodu okumak biraz karmaşıktır, işlev adlarında (ve C yazıyorsanız imzalar da) bazı yinelemeler vardır. Kod başarıyla derlenir ancak dlsym işlevine yanlışlıkla yanlış bir işlev adı gönderirseniz çalışma zamanında her zaman yedek işlevi kullanır. Ayrıca her API için bu kalıbı kullanmanız gerekir.

Zayıf API referansları ile 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, *), android_get_device_api_level()'yi çağırır, sonucu önbelleğe alır ve 31 ile karşılaştırır (AImageDecoder_resultToString()'ın kullanıma sunulduğu API düzeyi).

__builtin_available için hangi değerin kullanılacağını belirlemenin en basit yolu, koruyucu olmadan (veya __builtin_available(android 1, *) koruyucusunun) derlemeyi denemek ve hata mesajında belirtilenleri yapmaktır. Örneğin, minSdkVersion 24 ile AImageDecoder_createFromAAsset() çağrısı yapıldığında aşağıdaki sonuç elde edilir:

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

Bu durumda görüşme __builtin_available(android 30, *) tarafından korunmalıdır. Derleme hatası yoksa API, minSdkVersion için her zaman kullanılabilir durumdadır ve korumaya gerek yoktur. Alternatif olarak, derlemeniz yanlış yapılandırılmış olabilir ve unguarded-availability uyarısı devre dışı bırakılmış olabilir.

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

API korumalarının tekrarlanmasını önleme

Bunu kullanıyorsanız muhtemelen uygulamanızda yalnızca yeterince yeni cihazlarda kullanılabilen kod bölümleriniz olacaktır. Her işlevinizde __builtin_available() kontrolünü tekrarlamak yerine, kendi kodunuzu belirli bir API düzeyi gerektirecek şekilde ek açıklamayla belirtebilirsiniz. Örneğin, ImageDecoder API'lerin 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();
    }
}

API korumalarının özellikleri

Clang, __builtin_available işlevinin nasıl kullanıldığı konusunda çok titizdir. Yalnızca gerçek (makro ile değiştirilmiş olsa bile) if (__builtin_available(...)) çalışır. if (!__builtin_available(...)) gibi basit işlemler bile çalışmaz (Clang, unsupported-availability-guard uyarısının yanı sıra unguarded-availability uyarısını da verir). Bu durum Clang'ın gelecekteki bir sürümünde iyileştirilebilir. Daha fazla bilgi için LLVM Issue 33161 başlıklı makaleye göz atın.

unguarded-availability için yapılan kontroller yalnızca kullanıldıkları işlev kapsamı için geçerlidir. Clang, API çağrısı içeren işlev yalnızca korumalı bir kapsam içinde çağrılsa bile uyarıyı verir. Kendi kodunuzda koruyucuların tekrarlanmasını önlemek için API koruyucularının tekrarlanmasını önleme başlıklı makaleyi inceleyin.

Bu neden varsayılan ayar değil?

Doğru şekilde kullanılmadığı takdirde güçlü API referansları ile zayıf API referansları arasındaki fark, güçlü API referanslarının hızlı ve açık bir şekilde başarısız olması, zayıf API referanslarının ise kullanıcı eksik API'nin çağrılmasına neden olacak 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 segfault olacaktır. Güçlü referanslarla hata mesajı çok daha net olur ve hızlı başarısız olma durumu daha güvenli bir varsayılan değerdir.

Bu yeni bir özellik olduğundan, bu davranışı güvenli bir şekilde işlemek için çok az mevcut kod yazılır. Android'e uygun şekilde yazılmayan üçüncü taraf kodlarında bu sorun her zaman ortaya çıkabilir. Bu nedenle, varsayılan davranışın değiştirilmesi şu anda planlanmamaktadır.

Bu özelliği kullanmanızı öneririz. Ancak bu özellik, sorunların tespit edilmesini ve hata ayıklama işlemini zorlaştıracağından, davranışın sizin bilginiz dışında değişmesini önlemek için 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.

Sorun yaşama olasılığı en düşük olan libc API'leridir. Android API'lerinin geri kalanından farklı olarak, bu API'ler yalnızca __INTRODUCED_IN(X) ile değil, başlıklarda #if __ANDROID_API__ >= X ile de korunur. Bu sayede, zayıf beyanın bile görülmesi engellenir. 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üme yeni libc API'leri eklenir (status.md dosyasını inceleyin). Ancak API'ler ne kadar yeni olursa, az sayıda geliştiricinin ihtiyaç duyacağı uç bir durum olma olasılığı o kadar artar. Bununla birlikte, bu geliştiricilerden biriyseniz ve minSdkVersion'ünüz API'den eskiyse bu API'leri çağırmak için şimdilik dlsym() kullanmaya devam etmeniz gerekir. Bu çözülebilir bir sorundur ancak bu şekilde tüm uygulamaların kaynak uyumluluğunu bozma riski vardır (libc API'lerinin polyfill'lerini içeren tüm kodlar, libc ve yerel beyanlardaki eşleşmeyen availability özellikleri nedeniyle derlenemez). Bu nedenle, bu sorunu düzeltip düzeltmeyeceğimizden veya düzelteceksek 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 sürümünüz 24 ise libvulkan.so'ü bağlayabilir ve vkBindBufferMemory2'ye korumalı bir çağrı yapabilirsiniz. Çünkü libvulkan.so, API 24'ten itibaren cihazlarda kullanılabilir. Öte yandan, minSdkVersion değeriniz 23 ise kitaplık yalnızca API 23'ü destekleyen cihazlarda bulunmadığından dlopen ve dlsym değerlerine geri dönmeniz gerekir. Bu sorunu düzeltmek için iyi bir çözüm bilmiyoruz ancak mümkün olduğunda yeni API'lerin yeni kitaplıklar oluşturmasına izin vermediğimiz için bu sorun uzun vadede kendiliğinden çözülecektir.

Kitaplık 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 satır içi işlevler veya şablon tanımları gibi başlıklarınızdaki herhangi bir kodda __builtin_available'ten yararlanırsanız tüm kullanıcılarınızı bu özelliği etkinleştirmeye zorlarsınız. Bu özelliği NDK'da varsayılan olarak etkinleştirmememizin aynı nedenlerinden dolayı, bu seçimi tüketicileriniz adına yapmaktan kaçınmalısınız.

Herkese açık üst bilgileriniz için bu davranışı gerektiriyorsanız kullanıcılarınızın hem özelliği etkinleştirmeleri gerektiğini hem de bunu yapmanın risklerini bilmeleri için bunu belgelediğinizden emin olun.