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.
Uygulamanızdaki NDK API'lerine varsayılan olarak güçlü referanslar verilir. Android'in dinamik yükleyicisi, kitaplığınız yüklendiğinde bunları çözmeye çalışır. Simgeler bulunamazsa uygulama iptal edilir. 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'i çalıştırırken -DANDROID_WEAK_API_DEFS=ON
parametresini iletin. 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 yapmanız gerekmez.
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, yeni API'lere yapılan çağrıları sihirli bir şekilde güvenli hale getirmez. 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
için kullanılamayan bir API'ye korumasız çağrı yaptığınızda uyarı (unguarded-availability
) verebilir. ndk-build veya CMake araç zinciri dosyamızı kullanıyorsanız bu uyarı otomatik olarak etkinleştirilir ve bu özellik etkinleştirildiğinde hata olarak 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üzeyleri için kullanılabilir demektir.
API korumalarının tekrarlanmasını önleme
Bu özelliği kullanıyorsanız uygulamanızda büyük olasılıkla yalnızca yeterince yeni cihazlarda kullanılabilen kod bölümleri vardı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'leri API 30'a eklenmiştir. Bu nedenle, bu API'leri yoğun ş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 ele almak için çok az mevcut kod yazılmıştı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 için çalışır 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. Modern NDK'ların desteklediği en eski API düzeyi r21 olduğundan, en sık ihtiyaç duyulan libc API'leri zaten kullanılabilir. 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.
Daha fazla geliştiricinin karşılaşabileceği durum, yeni API'yi içeren kitaplığın minSdkVersion
'den daha yeni olmasıdır. Bu özellik yalnızca zayıf simge 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 üstbilgilerinizde 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.