JNI ipuçları

JNI, Java Yerel Arayüzüdür. Bu model, Android'in yönetilen koddan (Java veya Kotlin programlama dillerinde yazılmış) yerel kodla (C/C++ dilinde yazılır) etkileşime geçmek için derlediği bayt kodunun bir yolunu tanımlar. JNI, tedarikçiden bağımsızdır, dinamik paylaşılan kitaplıklardan kod yüklemeyi destekler ve bazen külfetli olsa da makul ölçüde verimlidir.

Not: Android, Java programlama diline benzer bir şekilde Kotlin'i ART uyumlu bayt koduna derlediğinden, bu sayfadaki kılavuzu hem JNI mimarisi hem de ilişkili maliyetleri açısından Kotlin ve Java programlama dillerine uygulayabilirsiniz. Daha fazla bilgi edinmek için Kotlin ve Android sayfasına bakın.

Bu özellikle ilgili bilgi sahibi değilseniz JNI'nin nasıl çalıştığı ve hangi özelliklerin kullanılabildiği hakkında fikir edinmek için Java Yerel Arayüz Spesifikasyonu'nu okuyun. Arayüzün bazı yönleri ilk bakışta kolayca anlaşılmayabileceğinden, sonraki birkaç bölümü işinize yarayabilir.

Genel JNI referanslarına göz atmak ve genel JNI referanslarının nerede oluşturulduğunu ve silindiğini görmek için Android Studio 3.2 ve üzeri sürümlerde Bellek Profilcisi'nde bulunan JNI yığını görünümünü kullanın.

Genel ipuçları

JNI katmanınızın ayak izini en aza indirmeye çalışın. Burada dikkate alınması gereken birkaç boyut vardır. JNI çözümünüz şu yönergelere uymaya çalışmalıdır: (en önemliden başlayarak önem sırasına göre aşağıda listelenmiştir):

  • JNI katmanında kaynakların yeniden düzenlenmesini en aza indirin. JNI katmanında yeniden pazarlamanın kolay olmayan maliyetleri vardır. Karıştırmanız gereken veri miktarını ve verileri karıştırma sıklığınızı en aza indiren bir arayüz tasarlamaya çalışın.
  • Mümkün olduğunda yönetilen bir programlama dilinde yazılmış kod ile C++'da yazılmış kod arasında eşzamansız iletişimden kaçının. Bu, JNI arayüzünüzün bakımını kolaylaştırır. Genellikle eşzamansız güncellemeyi kullanıcı arayüzüyle aynı dilde tutarak eşzamansız kullanıcı arayüzü güncellemelerini basitleştirebilirsiniz. Örneğin, JNI aracılığıyla Java kodundaki kullanıcı arayüzü iş parçacığından bir C++ işlevi çağırmak yerine, Java programlama dilindeki iki iş parçacığı arasında bir geri çağırma yapmak daha iyidir. Bunlardan biri, engelleme çağrısı yapan bir C++ çağrısı yapar ve engelleme çağrısı tamamlandığında kullanıcı arayüzü iş parçacığını bilgilendirir.
  • JNI tarafından dokunulması veya dokunulması gereken iş parçacığı sayısını en aza indirin. Hem Java hem de C++ dillerinde iş parçacığı havuzlarını kullanmanız gerekiyorsa JNI iletişimini ayrı çalışan iş parçacıkları yerine havuz sahipleri arasında tutmaya çalışın.
  • Gelecekte yeniden düzenleme işlemlerini kolaylaştırmak için arayüz kodunuzu az sayıda kolayca tespit edilen C++ ve Java kaynak konumunda tutun. Uygun şekilde bir JNI otomatik oluşturma kitaplığı kullanabilirsiniz.

JavaVM ve JNIEnv

JNI, "JavaVM" ve "JNIEnv" olmak üzere iki önemli veri yapısını tanımlar. Bunların her ikisi de temelde işlev tablolarının işaretçileridir. (C++ sürümünde, fonksiyon tablosu işaretçisi ve tablo üzerinden dolaylı her bir JNI işlevi için üye işlevi bulunan sınıflardır.) JavaVM, bir JavaVM oluşturmanızı ve yok etmenizi sağlayan "çağrı arayüzü" işlevleri sağlar. Teoride, işlem başına birden fazla JavaVM'niz olabilir ancak Android yalnızca bir JavaVM'ye izin verir.

JNI işlevlerinin çoğunu JNIEnv sağlar. @CriticalNative yöntemleri dışında, yerel işlevlerinizin tümü ilk bağımsız değişken olarak bir JNIEnv alır. Ancak, daha hızlı yerel çağrılar'a bakın.

JNIEnv, iş parçacığı yerel depolaması için kullanılır. Bu nedenle, JNIEnv verilerini ileti dizileri arasında paylaşamazsınız. Bir kod parçasının JNIEnv'i almanın başka bir yolu yoksa JavaVM'yi paylaşmalı ve iş parçacığının JNIEnv'ini keşfetmek için GetEnv parametresini kullanmalısınız. (Olduğunu varsayarsak aşağıdaki AttachCurrentThread adresine bakın.)

JNIEnv ve JavaVM'nin C bildirimleri, C++ bildirimlerinden farklıdır. "jni.h" include dosyası, C veya C++'a eklenip eklenmediğine bağlı olarak farklı typedef'ler sağlar. Bu nedenle, her iki dil tarafından dahil edilen üstbilgi dosyalarına JNIEnv bağımsız değişkenlerinin eklenmesi kötü bir fikirdir. (Başka bir deyişle: Başlık dosyanız #ifdef __cplusplus gerektiriyorsa bu başlıktaki herhangi bir şey JNIEnv ile ilgiliyse fazladan bir işlem yapmanız gerekebilir.)

Mesaj dizileri

Tüm ileti dizileri, çekirdek tarafından programlanmış Linux iş parçacıklarıdır. Genellikle yönetilen koddan başlar (Thread.start() kullanılarak) ancak başka bir yerde oluşturulup JavaVM öğesine de eklenebilirler. Örneğin, pthread_create() veya std::thread ile başlayan bir iş parçacığı AttachCurrentThread() veya AttachCurrentThreadAsDaemon() işlevleri kullanılarak eklenebilir. Bir iş parçacığı eklenene kadar JNIEnv içermez ve JNI çağrıları yapamaz.

Java kodunu çağırması gereken herhangi bir iş parçacığını oluşturmak için genellikle Thread.start() kullanılması önerilir. Bunu yaptığınızda yeterli yığın alanınız olduğundan, doğru ThreadGroup konumunda olduğunuzdan ve Java kodunuzla aynı ClassLoader değerini kullandığınızdan emin olabilirsiniz. İş parçacığının adını Java'da hata ayıklama için ayarlamak da yerel koddan daha kolaydır (pthread_t veya thread_t kullanıyorsanız pthread_setname_np(), std::thread kullanıyorsanız ve pthread_t istiyorsanız std::thread::native_handle() konusuna bakın).

Yerel olarak oluşturulmuş bir iş parçacığının eklenmesi, bir java.lang.Thread nesnesinin oluşturulup "ana" ThreadGroup öğesine eklenmesine ve böylece hata ayıklayıcının görüntülemesine neden olur. Zaten ekli olan ileti dizisinde AttachCurrentThread() çağrısının bir işlem yoktur.

Android, yerel kod yürüten iş parçacıklarını askıya almaz. Atık toplama işlemi devam ediyorsa veya hata ayıklayıcı bir askıya alma isteği gönderdiyse Android, bir sonraki JNI çağrısında ileti dizisini duraklatır.

JNI aracılığıyla eklenen iş parçacıkları çıkış yapmadan önce DetachCurrentThread() işlevini çağırmalıdır. Bunu doğrudan kodlamak garipse Android 2.0 (Eclair) ve sonraki sürümlerde iş parçacığından çıkmadan önce çağrılacak bir Yıkıcı işlevi tanımlamak ve buradan DetachCurrentThread() çağrısı yapmak için pthread_key_create() kullanabilirsiniz. (JNIEnv'i iş parçacığı-local-storage içinde depolamak için pthread_setspecific() ile bu anahtarı kullanın. Bu şekilde, bağımsız değişken olarak yıkıcınıza aktarılır.)

jclass, jmethodID ve jfieldID

Bir nesnenin alanına yerel koddan erişmek istiyorsanız aşağıdakileri yapmanız gerekir:

  • FindClass ile sınıfın sınıf nesnesi referansını alma
  • GetFieldID ile alanın alan kimliğini alın
  • Alanın içeriğini GetIntField gibi uygun bir dosyayla alın.

Benzer şekilde, bir yöntemi çağırmak için önce bir sınıf nesne referansı ve ardından bir yöntem kimliği alırsınız. Kimlikler genellikle sadece dahili çalışma zamanı veri yapılarına işaret eder. Bunları aramak için çeşitli dize karşılaştırmaları gerekebilir ancak bunları aldıktan sonra alanı almak veya yöntemi çağırmak için asıl çağrı çok hızlı olur.

Performans önemliyse değerleri bir kez kontrol edip sonuçları yerel kodunuzda önbelleğe almak faydalıdır. İşlem başına bir JavaVM sınırı bulunduğundan bu verilerin statik bir yerel yapıda depolanması makuldür.

Sınıf referansları, alan kimlikleri ve yöntem kimliklerinin, sınıf kaldırılana kadar geçerli olacağı garanti edilir. Sınıflar yalnızca ClassLoader ile ilişkili tüm sınıfların çöp toplama işlemi gerçekleştirilebiliyor olması halinde kaldırılır. Bu, nadir görülen bir durumdur ancak Android'de imkansız değildir. Bununla birlikte, jclass öğesinin bir sınıf referansı olduğunu ve NewGlobalRef çağrısıyla korunması gerektiğini unutmayın (sonraki bölüme bakın).

Bir sınıf yüklendiğinde kimlikleri önbelleğe almak ve sınıf kaldırılıp yeniden yüklenirse otomatik olarak yeniden önbelleğe almak istiyorsanız kimlikleri başlatmanın doğru yolu, uygun sınıfa şuna benzer bir kod parçası eklemektir:

Kotlin

companion object {
    /*
     * We use a static class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private external fun nativeInit()

    init {
        nativeInit()
    }
}

Java

    /*
     * We use a class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private static native void nativeInit();

    static {
        nativeInit();
    }

C/C++ kodunuzda kimlik aramaları gerçekleştiren bir nativeClassInit yöntemi oluşturun. Sınıf başlatıldığında kod bir kez yürütülür. Sınıfın yüklemesi kaldırılıp daha sonra yeniden yüklenirse tekrar yürütülür.

Yerel ve global referanslar

Yerel bir yönteme iletilen her bağımsız değişken ve JNI işlevi tarafından döndürülen hemen hemen her nesne "yerel referans"tır. Bu, geçerli iş parçacığındaki mevcut yerel yöntemin süresi boyunca geçerli olduğu anlamına gelir. Yerel yöntem döndürüldükten sonra nesnenin kendisi kullanılmaya devam etse bile referans geçerli olmaz.

Bu durum, jclass, jstring ve jarray dahil olmak üzere tüm jobject alt sınıfları için geçerlidir. (Genişletilmiş JNI kontrolleri etkinleştirildiğinde çalışma zamanı, çoğu referans hatalı kullanım konusunda sizi uyarır.)

Yerel olmayan referansları almanın tek yolu NewGlobalRef ve NewWeakGlobalRef işlevlerini kullanmaktır.

Bir referansı daha uzun süre saklamak istiyorsanız "global" referans kullanmanız gerekir. NewGlobalRef işlevi, yerel referansı bağımsız değişken olarak alır ve genel bir referans döndürür. Genel referansın, siz DeleteGlobalRef yöntemini çağırana kadar geçerli olacağı garanti edilir.

Bu kalıp, FindClass öğesinden döndürülen bir jclass önbelleğe alınırken yaygın olarak kullanılır. Örneğin:

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

Tüm JNI yöntemleri, hem yerel hem de genel referansları bağımsız değişken olarak kabul eder. Aynı nesneye yapılan referanslar farklı değerlere sahip olabilir. Örneğin, aynı nesnede art arda yapılan çağrılardan NewGlobalRef işlevine yapılan döndürülen değerler farklı olabilir. İki referansın aynı nesneye atıfta bulunup bulunmadığını görmek için IsSameObject işlevini kullanmanız gerekir. Referansları hiçbir zaman yerel kodda == ile karşılaştırmayın.

Bunun bir sonucu olarak, yerel kodda nesne referanslarının sabit veya benzersiz olduğunu varsaymamalısınız. Bir nesneyi temsil eden değer, bir yöntemin çağrılmasından diğerine farklı olabilir ve iki farklı nesnenin ardışık çağrılarda aynı değere sahip olması mümkündür. jobject değerlerini anahtar olarak kullanmayın.

Programcıların yerel referansları "aşırı şekilde ayırmaması" gerekir. Pratik açısından bu, çok sayıda yerel referans oluşturuyorsanız (belki bir dizi nesne üzerinde çalışırken) bunları JNI'nin sizin için yapmasına izin vermek yerine manuel olarak DeleteLocalRef ile serbest bırakmanız gerektiği anlamına gelir. Uygulama yalnızca 16 yerel referans için alan ayırmak için gereklidir. Bu nedenle, bundan daha fazlasına ihtiyacınız varsa kullandıkça silmeniz veya daha fazla referans için EnsureLocalCapacity/PushLocalFrame kullanmanız gerekir.

jfieldID ve jmethodID değerlerinin nesne referansı değil, opak türler olduğunu ve NewGlobalRef öğesine iletilmemesi gerektiğini unutmayın. GetStringUTFChars ve GetByteArrayElements gibi işlevlerin döndürdüğü ham veri işaretçileri de nesne değildir. (İleti dizileri arasında iletilebilir ve eşleşen Sürüm çağrısına kadar geçerlidirler.)

Olağan dışı bir vakanın ayrıca bahsetmesi gerekir. AttachCurrentThread ile yerel bir iş parçacığı eklerseniz çalıştırdığınız kod, iş parçacığı ayrılana kadar yerel referansları hiçbir zaman otomatik olarak serbest bırakmaz. Oluşturduğunuz tüm yerel referansların manuel olarak silinmesi gerekecektir. Genel olarak, bir döngüde yerel referanslar oluşturan yerel kodların muhtemelen bazı manuel silme işlemleri yapması gerekir.

Genel referansları kullanırken dikkatli olun. Genel referanslar kaçınılmaz olabilir ancak hata ayıklaması zordur ve teşhis edilmesi zor bellek (yanlış) davranışlara neden olabilir. Diğer her şey eşit olduğunda daha az global referansa sahip bir çözüm muhtemelen daha iyidir.

UTF-8 ve UTF-16 dizeleri

Java programlama dili UTF-16 kullanır. Kolaylık sağlaması açısından JNI, Değiştirilmiş UTF-8 ile çalışan yöntemler de sağlar. Değiştirilmiş kodlama, \u0000 kodunu 0x00 yerine 0xc0 0x80 olarak kodladığı için C kodu için yararlıdır. Bunun en iyi yanı, standart libc dizesi işlevleriyle kullanıma uygun C stili sıfır sonlandırılmış dizelere sahip olabilmenizdir. Kötü tarafı, rastgele UTF-8 verilerini JNI'ye iletememeniz ve düzgün çalışmasını bekleyememenizdir.

Bir String öğesinin UTF-16 gösterimini almak için GetStringChars kodunu kullanın. UTF-16 dizelerinin sıfır sonlandırılmadığını ve \u0000 öğesine izin verildiğini unutmayın. Bu nedenle, jchar işaretçisinin yanı sıra dize uzunluğunu da kullanmaya devam etmeniz gerekir.

Get tarafından belirttiğiniz dizeleri Release haline getirmeyi unutmayın. Dize işlevleri, jchar* veya jbyte* döndürür. Bunlar, yerel referanslar yerine temel verilere yönlendiren C stili işaretçilerdir. Release çağrılana kadar geçerli oldukları garanti edilir. Diğer bir deyişle, yerel yöntem geri döndüğünde yayınlanmaz.

NewStringUTF'e iletilen veriler, Değiştirilmiş UTF-8 biçiminde olmalıdır. Bir dosya veya ağ akışındaki karakter verilerini okuyup filtrelemeden NewStringUTF adresine teslim etmek, sık yapılan bir hatadır. Verilerin geçerli MUTF-8 (veya uyumlu bir alt küme olan 7 bit ASCII) olduğundan emin değilseniz geçersiz karakterleri çıkarmanız veya uygun Düzenlenmiş UTF-8 formuna dönüştürmeniz gerekir. Aksi takdirde, UTF-16 dönüşümü beklenmedik sonuçlar sağlayabilir. Emülatörler için varsayılan olarak etkin olan CheckJNI, dizeleri tarar ve geçersiz giriş alırsa sanal makineyi iptal eder.

Android, GetStringChars dilinde bir kopya gerektirmediğinden Android 8'den önce UTF-16 dizeleriyle çalışmak genellikle daha hızlıydı, GetStringUTFChars ise ayırma ve UTF-8'e dönüştürme gerektiriyordu. Android 8, String gösterimini, ASCII dizeleri için karakter başına 8 bit kullanacak şekilde değiştirdi (hafızadan tasarruf etmek için) ve hareketli çöp toplayıcı kullanmaya başladı. Bu özellikler, ART'ın GetStringCritical için bile, kopya oluşturmadan String verilerine işaretçi sağlayabileceği durumların sayısını büyük ölçüde azaltır. Bununla birlikte, kod tarafından işlenen dizelerin çoğu kısaysa çoğu durumda yığına ayrılmış arabellek ve GetStringRegion veya GetStringUTFRegion kullanarak ayırma ve ayırma işlemlerinden kaçınmak mümkündür. Örnek:

    constexpr size_t kStackBufferSize = 64u;
    jchar stack_buffer[kStackBufferSize];
    std::unique_ptr heap_buffer;
    jchar* buffer = stack_buffer;
    jsize length = env->GetStringLength(str);
    if (length > kStackBufferSize) {
      heap_buffer.reset(new jchar[length]);
      buffer = heap_buffer.get();
    }
    env->GetStringRegion(str, 0, length, buffer);
    process_data(buffer, length);

Temel diziler

JNI, dizi nesnelerinin içeriğine erişmek için işlevler sağlar. Nesne dizilerine her defasında bir giriş yapılması gerekirken temel öğe dizileri, C'de tanımlanmışlar gibi doğrudan okunabilir ve yazılabilir.

Sanal makine uygulamasını kısıtlamadan arayüzü mümkün olduğunca verimli hale getirmek için Get<PrimitiveType>ArrayElements çağrı ailesi, çalışma zamanının gerçek öğelere bir işaretçi döndürmesine veya bir miktar bellek ayırıp kopyasını oluşturmasına olanak tanır. Her iki durumda da döndürülen ham işaretçinin, ilgili Release çağrısı yapılana kadar geçerli olacağı garanti edilir (yani, veriler kopyalanmazsa dizi nesnesi sabitlenir ve yığın sıkıştırma kapsamında yer değiştirilemez). Get eklediğiniz her dizi için Release yapmanız gerekir. Ayrıca Get çağrısı başarısız olursa kodunuzun daha sonra NULL bir işaretçiyi Release yapmaya çalışmadığından emin olmanız gerekir.

isCopy bağımsız değişkeni için NULL olmayan bir işaretçi ileterek verilerin kopyalanıp kopyalanmadığını belirleyebilirsiniz. Bu nadiren yararlıdır.

Release çağrısı, üç değerden birine sahip olabilecek bir mode bağımsız değişkeni alır. Çalışma zamanı tarafından gerçekleştirilen işlemler, gerçek verilere bir işaretçi mi yoksa bir kopyası mı döndürdüğüne bağlıdır:

  • 0
    • Gerçek: Dizi nesnesinin sabitlemesi kaldırıldı.
    • Kopyala: Veriler geri kopyalanır. Kopyayı içeren arabellek serbest bırakılır.
  • JNI_COMMIT
    • Gerçek: Hiçbir şey yapmaz.
    • Kopyala: Veriler geri kopyalanır. Kopyayı içeren arabellek serbest bırakılmaz.
  • JNI_ABORT
    • Gerçek: Dizi nesnesinin sabitlemesi kaldırıldı. Daha önceki yazma işlemleri iptal edilmez.
    • Kopyala: Kopyayı içeren arabellek serbest bırakılır; üzerinde yapılan değişiklikler kaybolur.

isCopy işaretini kontrol etmenin bir nedeni, bir dizide değişiklik yaptıktan sonra Release işlevini JNI_COMMIT ile çağırmanız gerekip gerekmediğini öğrenmektir. Değişiklik yapma ve dizinin içeriğini kullanan kodu yürütme arasında geçiş yapıyorsanız işlemsiz kaydetme işlemini atlayabilirsiniz. İşareti kontrol etmenin bir başka olası nedeni de JNI_ABORT öğesinin verimli bir şekilde kullanılmasıdır. Örneğin, bir diziyi almak, dizini değiştirmek, parçaları diğer işlevlere aktarmak ve ardından değişiklikleri silmek isteyebilirsiniz. JNI'nin sizin için yeni bir kopya oluşturduğunu biliyorsanız, başka bir "düzenlenebilir" kopya oluşturmanıza gerek yoktur. JNI size orijinal belgeyi iletiyorsa kendi kopyanızı oluşturmanız gerekir.

*isCopy değeri yanlışsa Release çağrısını atlayabilirsiniz. Bu, yaygın bir hatadır (örnek kodda tekrarlanır). Bu doğru değildir. Kopya arabelleği ayrılmamışsa orijinal bellek sabitlenmelidir ve çöp toplayıcı tarafından taşınamaz.

JNI_COMMIT işaretinin diziyi yayınlamadığını ve sonuç olarak Release işaretini farklı bir işaretle tekrar çağırmanız gerektiğini de unutmayın.

Bölge çağrıları

Tek yapmanız gereken verileri içeriye veya dışarı kopyalamak olduğunda Get<Type>ArrayElements ve GetStringChars gibi çağrılara çok yardımcı olabilecek bir alternatif vardır. Aşağıdakileri göz önünde bulundurun:

    jbyte* data = env->GetByteArrayElements(array, NULL);
    if (data != NULL) {
        memcpy(buffer, data, len);
        env->ReleaseByteArrayElements(array, data, JNI_ABORT);
    }

Bu işlem diziyi alır, dizinin ilk len baytlık öğelerini kopyalar ve ardından diziyi serbest bırakır. Uygulamaya bağlı olarak Get çağrısı, dizi içeriğini sabitler veya kopyalar. Kod, verileri kopyalar (belki ikinci bir kez) ve ardından Release işlevini çağırır. Bu durumda JNI_ABORT, üçüncü kopya ihtimalinin olmamasını sağlar.

Aynı şey daha basit şekilde yapılabilir:

    env->GetByteArrayRegion(array, 0, len, buffer);

Bunun çeşitli avantajları vardır:

  • 2 yerine bir JNI çağrısı yapılması gerektiğinden, ek yük azalır.
  • Sabitleme veya ekstra veri kopyaları gerektirmez.
  • Programcı hatası riskini azaltır. Bir hata oluştuğunda Release işlevini çağırmayı unutma riskini ortadan kaldırır.

Benzer şekilde, verileri bir diziye kopyalamak için Set<Type>ArrayRegion çağrısını, bir String öğesinden karakter kopyalamak için GetStringRegion veya GetStringUTFRegion çağrısını kullanabilirsiniz.

İstisnalar

Bir istisna beklemedeyken çoğu JNI işlevini çağırmamanız gerekir. Kodunuzun istisnayı fark etmesi (işlevin döndürdüğü değer, ExceptionCheck veya ExceptionOccurred aracılığıyla) ve döndürmesi ya da istisnayı temizleyip işlemesi beklenir.

Bir istisna beklemedeyken yalnızca şu JNI işlevlerini çağırabilirsiniz:

  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • Release<PrimitiveType>ArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

Birçok JNI çağrısı istisna oluşturabilir, ancak genellikle hata kontrolü için daha basit bir yöntem sunar. Örneğin, NewString NULL olmayan bir değer döndürürse istisna kontrolü yapmanıza gerek yoktur. Bununla birlikte, CallObjectMethod gibi bir işlev kullanarak bir yöntemi çağırırsanız her zaman istisna kontrolü yapmanız gerekir. Çünkü istisna oluşursa döndürülen değer geçerli olmaz.

Yönetilen kod tarafından oluşturulan istisnaların yerel yığın çerçevelerini geri çekmediğini unutmayın. (Ayrıca, Android'de genellikle önerilmeyen C++ istisnaları, C++ kodundan yönetilen koda JNI geçiş sınırı boyunca atılmamalıdır.) JNI Throw ve ThrowNew talimatları yalnızca mevcut iş parçacığında bir istisna işaretçisi ayarlar. Yerel koddan yönetilen koda döndüğünüzde, istisna not edilir ve uygun şekilde işlenir.

Yerel kod, ExceptionCheck veya ExceptionOccurred yöntemini çağırarak bir istisnayı "yakalayıp" ExceptionClear ile temizleyebilir. Her zaman olduğu gibi, istisnaları işleme koymadan silmek sorunlara yol açabilir.

Throwable nesnesinin kendisinde değişiklik yapmak için kullanılan yerleşik işlevler yoktur. Bu nedenle, (örneğin) istisna dizesini almak isterseniz Throwable sınıfını bulmanız, getMessage "()Ljava/lang/String;" için yöntem kimliğini aramanız, çağırmanız ve sonuç NULL değilse printf(3) veya eşdeğeri bir öğeye verebileceğiniz bir şey almak için GetStringUTFChars kullanın.

Uzatılmış çek

JNI çok az hata kontrolü yapar. Hatalar genellikle kilitlenmeyle sonuçlanır. Android, CheckJNI adında bir mod da sunar. Bu modda JavaVM ve JNIEnv işlev tablo işaretçileri, standart uygulamayı çağırmadan önce bir dizi genişletilmiş kontrol gerçekleştiren işlev tablolarına geçirilir.

Ek kontroller aşağıdakileri içerir:

  • Diziler: Negatif boyutlu bir dizi ayırmaya çalışma.
  • Kötü işaretçiler: JNI çağrısına bozuk bir jarray/jclass/jobject/jstring veya null olmayan bir bağımsız değişkenle JNI çağrısına NULL işaretçi iletme.
  • Sınıf adları: JNI çağrısına sınıf adının "java/lang/String" stilinden başka her şeyi iletme.
  • Kritik çağrılar: "kritik" erişim ile bunun karşılık gelen sürümü arasında JNI araması yapılması.
  • Direct ByteBuffers: Hatalı bağımsız değişkenleri NewDirectByteBuffer öğesine iletme.
  • İstisnalar: Bekleyen bir istisna varken JNI çağrısı yapma.
  • JNIEnv*s: Yanlış ileti dizisinden bir JNIEnv* kullanılıyor.
  • jfieldID'ler: NULL jfieldID kullanma veya bir alanı yanlış türdeki bir değere ayarlamak için jfieldID kullanma (örneğin, bir Dize alanına StringBuilder atamaya çalışma) veya bir örnek alanını ayarlamak için statik bir alan için jfieldID kullanma (veya tam tersi) ya da başka bir sınıfın örnekleriyle bir sınıftan jfieldID kullanma.
  • jmethodID'ler: Call*Method JNI çağrısı yaparken yanlış tür jmethodID kullanma: yanlış dönüş türü, statik/statik olmayan uyumsuzluk, "bu" için yanlış tür (statik olmayan çağrılar için) veya yanlış sınıf (statik çağrılar için).
  • Referanslar: DeleteGlobalRef/DeleteLocalRef öğesini yanlış referans türünde kullanma
  • Sürüm modları: Yayın çağrısına kötü sürüm modu iletme (0, JNI_ABORT veya JNI_COMMIT dışında bir mod).
  • Tür güvenliği: Yerel yönteminizden uyumlu olmayan bir tür döndürme (örneğin, bir Dize döndürdüğü beyan edilen bir yöntemden StringBuilder döndürme).
  • UTF-8: Bir JNI çağrısına geçersiz bir Değiştirilmiş UTF-8 bayt sırası iletme.

(Yöntemlerin ve alanların erişilebilirliği hâlâ kontrol edilmemiştir: Erişim kısıtlamaları yerel kod için geçerli değildir.)

CheckJNI'yi etkinleştirmenin birkaç yolu vardır.

Emülatörü kullanıyorsanız CheckJNI varsayılan olarak açıktır.

Root erişimli bir cihazınız varsa çalışma zamanını CheckJNI etkinken yeniden başlatmak için aşağıdaki komut dizisini kullanabilirsiniz:

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

Her iki durumda da, çalışma zamanı başladığında logcat çıkışınızda aşağıdakine benzer bir şey görürsünüz:

D AndroidRuntime: CheckJNI is ON

Normal bir cihazınız varsa aşağıdaki komutu kullanabilirsiniz:

adb shell setprop debug.checkjni 1

Çalışmakta olan uygulamalar bu durumdan etkilenmez, ancak bu andan itibaren başlatılan uygulamalarda CheckJNI etkinleştirilecektir. (Özelliği başka bir değerle değiştirin veya yeniden başlattığınızda CheckJNI tekrar devre dışı bırakılır.) Bu durumda, bir sonraki uygulama başlatıldığında logcat çıkışınızda şuna benzer bir şey görürsünüz:

D Late-enabling CheckJNI

Ayrıca, uygulamanızın manifest dosyasında android:debuggable özelliğini, CheckJNI'yi yalnızca uygulamanız için etkinleştirecek şekilde ayarlayabilirsiniz. Android derleme araçlarının bu işlemi belirli derleme türleri için otomatik olarak yapacağını unutmayın.

Yerel kitaplık

Standart System.loadLibrary ile paylaşılan kitaplıklardan yerel kod yükleyebilirsiniz.

Pratikte, Android'in eski sürümlerinde PackageManager'da yerel kitaplıkların yüklenmesinin ve güncellenmesinin güvenilmez olmasına neden olan hatalar vardı. ReLinker projesi, bu ve diğer yerel kitaplık yükleme sorunları için geçici çözümler sunar.

Statik sınıf başlatma aracından System.loadLibrary (veya ReLinker.loadLibrary) komutunu çağırın. Bağımsız değişken "undecorated" kitaplık adıdır. Bu nedenle, libfubar.so öğesini yüklemek için "fubar" içinde iletilir.

Yerel yöntemlere sahip yalnızca bir sınıfınız varsa System.loadLibrary çağrısının söz konusu sınıf için statik bir başlatıcıda olması mantıklıdır. Aksi takdirde, kitaplığın her zaman ve her zaman erken yüklendiğinden emin olmak için Application adresinden çağrı yapmak isteyebilirsiniz.

Çalışma zamanı, yerel yöntemlerinizi iki şekilde bulabilir. Bunları RegisterNatives ile açıkça kaydedebilir veya çalışma zamanının dlsym ile dinamik olarak aramasına izin verebilirsiniz. RegisterNatives'ın avantajları, sembollerin var olup olmadığını önceden kontrol etmeniz ve JNI_OnLoad dışında hiçbir şeyi dışa aktarmayarak daha küçük ve daha hızlı paylaşılan kitaplıklara sahip olmanızdır. Çalışma zamanının işlevlerinizi keşfetmesine izin vermenin avantajı, yazılacak kodun biraz daha az olmasıdır.

RegisterNatives uygulamasını kullanmak için:

  • Bir JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) işlevi sağlayın.
  • JNI_OnLoad içinde RegisterNatives kullanarak tüm yerel yöntemlerinizi kaydedin.
  • Kitaplığınızdan yalnızca JNI_OnLoad dışa aktarılması için -fvisibility=hidden ile oluşturun. Bu, daha hızlı ve daha küçük kodlar üretir ve uygulamanıza yüklenen diğer kitaplıklarla olası çakışmaları önler (ancak uygulamanız yerel kodda kilitlenirse daha az yararlı yığın izleri oluşturur).

Statik başlatıcı aşağıdaki gibi görünmelidir:

Kotlin

companion object {
    init {
        System.loadLibrary("fubar")
    }
}

Java

static {
    System.loadLibrary("fubar");
}

C++ ile yazılmışsa JNI_OnLoad işlevi aşağıdaki gibi görünmelidir:

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    // Find your class. JNI_OnLoad is called from the correct class loader context for this to work.
    jclass c = env->FindClass("com/example/app/package/MyClass");
    if (c == nullptr) return JNI_ERR;

    // Register your class' native methods.
    static const JNINativeMethod methods[] = {
        {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)},
        {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)},
    };
    int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));
    if (rc != JNI_OK) return rc;

    return JNI_VERSION_1_6;
}

Bunun yerine yerel yöntemlerin "keşfi" özelliğini kullanmak için bunları belirli bir şekilde adlandırmanız gerekir (ayrıntılar için JNI spesifikasyonuna bakın). Yani, yöntem imzası yanlışsa yöntem ilk kez gerçekten çağrılana kadar imzadan haberdar olmazsınız.

JNI_OnLoad üzerinden yapılan tüm FindClass çağrıları, paylaşılan kitaplığı yüklemek için kullanılan sınıf yükleyicisi bağlamında sınıfları çözümler. Diğer bağlamlardan çağrıldığında, FindClass Java yığınının üst kısmındaki yöntemle ilişkilendirilen sınıf yükleyicisini kullanır. Böyle bir yükleyici yoksa (çağrı yeni eklenen yerel bir iş parçacığından geldiği için) "system" sınıf yükleyicisini kullanır. Sistem sınıfı yükleyici, uygulamanızın sınıflarını bilmez. Bu nedenle, bu bağlamda FindClass ile kendi sınıflarınızı arayamazsınız. Bu şekilde, JNI_OnLoad sınıfı sınıfları aramak ve önbelleğe almak için uygun bir yerdir: Geçerli bir jclass genel referansınız olduğunda ekli herhangi bir iş parçacığından bu referansı kullanabilirsiniz.

@FastNative ve @CriticalNative ile daha hızlı yerel çağrılar

Yerel yöntemlere, yönetilen ve yerel kod arasındaki geçişi hızlandırmak için @FastNative veya @CriticalNative (ancak ikisi birden değil) ek açıklamaları eklenebilir. Ancak, bu ek açıklamalar kullanılmadan önce dikkatli bir şekilde ele alınması gereken belirli davranış değişiklikleriyle gelir. Bu değişikliklerden kısaca bahsetmekle birlikte lütfen ayrıntılar için belgelere göz atın.

@CriticalNative ek açıklaması, yalnızca yönetilen nesneler kullanmayan yerel yöntemlere (parametrelerde veya dönüş değerlerinde veya örtülü bir this olarak) uygulanabilir ve bu ek açıklama, JNI geçişi ABI'sini değiştirir. Yerel uygulama, işlev imzasından JNIEnv ve jclass parametrelerini hariç tutmalıdır.

Bir @FastNative veya @CriticalNative yöntemi yürütülürken çöp toplama işlemi, önemli işler için iş parçacığını askıya alamaz ve engellenebilir. Bu ek açıklamaları, genellikle hızlı ancak genellikle sınırsız olan yöntemler dahil olmak üzere uzun süreli yöntemler için kullanmayın. Özellikle, kod önemli G/Ç işlemleri gerçekleştirmemeli veya uzun süre tutulabilen yerel kilitler almamalıdır.

Bu ek açıklamalar, Android 8'den bu yana sistem kullanımı için uygulandı ve Android 14'te CTS testinden geçmiş herkese açık API hâline geldi. Bu optimizasyonlar muhtemelen Android 8-13 cihazlarda da çalışır (güçlü CTS garantisi olmasa da) ancak yerel yöntemlere ilişkin dinamik arama yalnızca Android 12 ve sonraki sürümlerde desteklenir. Android 8-11 sürümlerini çalıştırmak için JNI RegisterNatives ile açık kayıt kesinlikle gereklidir. Bu ek açıklamalar Android 7'de yok sayılır. @CriticalNative için ABI uyuşmazlığı, yanlış bağımsız değişkene ve kilitlenmelere yol açar.

Bu ek açıklamalara ihtiyaç duyan performans açısından kritik yöntemler için yerel yöntemlerin ada dayalı "keşif"ine güvenmek yerine yöntemlerin JNI RegisterNatives ile açık bir şekilde kaydedilmesi önerilir. Optimum uygulama başlatma performansını elde etmek için @FastNative veya @CriticalNative yöntemlerini arayanların temel profile eklenmesi önerilir. Android 12'den beri, derlenmiş yönetilen bir yöntemden @CriticalNative yerel yöntemine yapılan bir çağrı, tüm bağımsız değişkenler kayıtlara uydukları sürece C/C++'ta satır içi olmayan bir çağrı kadar ucuzdur (örneğin, arm64'te en fazla 8 integral ve en fazla 8 kayan nokta bağımsız değişkeni).

Bazen yerel bir yöntemi, başarısız olabilen çok hızlı bir yöntem ve yavaş vakalarla ilgilenen başka bir yönteme bölmek tercih edilebilir. Örnek:

Kotlin

fun writeInt(nativeHandle: Long, value: Int) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value)
    }
}

@CriticalNative
external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean

external fun nativeWriteInt(nativeHandle: Long, value: Int)

Java

void writeInt(long nativeHandle, int value) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value);
    }
}

@CriticalNative
static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value);

static native void nativeWriteInt(long nativeHandle, int value);

64 bit ile ilgili dikkat edilmesi gerekenler

64 bit işaretçiler kullanan mimarileri desteklemek için Java alanında yerel bir yapının işaretçisini saklarken int yerine long alanı kullanın.

Desteklenmeyen özellikler/geriye dönük uyumluluk

Aşağıdaki istisna hariç tüm JNI 1.6 özellikleri desteklenir:

  • DefineClass uygulanmadı. Android, Java bayt kodları veya sınıf dosyaları kullanmaz. Bu nedenle ikili sınıf verileri aktarılamaz.

Eski Android sürümleriyle geriye dönük uyumluluk için aşağıdakilere dikkat etmeniz gerekebilir:

  • Yerel işlevler için dinamik arama

    Android 2.0'a (Eclair) kadar, yöntem adları için yapılan aramalar sırasında "$" karakteri doğru şekilde "_00024" biçimine dönüştürülmemişti. Bu sorunu çözmek için açık kayıt yöntemi kullanmak veya yerel yöntemleri iç sınıfların dışına taşımak gerekir.

  • İleti dizilerini ayırma

    Android 2.0'a (Eclair) kadar, "ileti dizisi çıkıştan önce ayrılmalıdır" denetiminden kaçınmak için pthread_key_create yıkıcı işlevi kullanılamıyordu. (Çalışma zamanı ayrıca bir pthread anahtarı Yıkıcı işlevi de kullanır. Bu nedenle, hangisinin önce çağrıldığını görmek için bir yarış olur.)

  • Zayıf dünya referansları

    Android 2.2'ye (Froyo) kadar zayıf global referanslar uygulanmadı. Eski sürümler, bunları kullanma girişimlerini kesinlikle reddeder. Desteği test etmek için Android platform sürüm sabitlerini kullanabilirsiniz.

    Dünya çapında zayıf referanslar, Android 4.0'a (Ice Cream Sandwich) kadar yalnızca NewLocalRef, NewGlobalRef ve DeleteWeakGlobalRef sürümlerine iletilebilirdi. (Bu spesifikasyon, programcıları herhangi bir şey yapmadan önce zayıf genel dünyaya somut referanslar oluşturmaya teşvik etmektedir. Bu nedenle, bu kesinlikle sınırlayıcı olmamalıdır.)

    Zayıf küresel referanslar, Android 4.0 (Ice Cream Sandwich) sürümünden itibaren diğer tüm JNI referansları gibi kullanılabilir.

  • Yerel referanslar

    Yerel referanslar, Android 4.0'a (Ice Cream Sandwich) kadar aslında doğrudan işaretçilerdi. Ice Cream Sandwich, daha iyi çöp toplayıcıları desteklemek için gerekli olan yönergeyi ekledi. Ancak bu durum, eski sürümlerde çok sayıda JNI hatasının tespit edilemeyeceği anlamına geliyor. Daha fazla bilgi için ICS'de JNI Yerel Referans Değişiklikleri bölümüne bakın.

    Android 8.0'dan önceki Android sürümlerinde yerel referansların sayısı sürüme özel bir sınırla sınırlandırılmıştır. Android 8.0 sürümünden itibaren Android sınırsız yerel referansı destekler.

  • GetObjectRefType ile referans türünü belirleme

    Doğrudan işaretçilerin kullanılması nedeniyle (yukarıya bakın) Android 4.0'a (Ice Cream Sandwich) kadar GetObjectRefType yaklaşımını doğru bir şekilde uygulamak imkansızdı. Bunun yerine, zayıf geneller tablosunu, bağımsız değişkenleri, yerel tabloyu ve geneller tablosunu bu sırayla inceleyen bir buluşsal yöntem kullandık. Doğrudan işaretçinizi ilk kez bulduğunda, referansınızın incelenmekte olduğu türde olduğunu bildirir. Bu, örneğin, statik yerel yönteminize dolaylı bir bağımsız değişken olarak iletilen jclass ile aynı olan genel bir jclass'ta GetObjectRefType çağrısı yaptığınızda, JNIGlobalRefType yerine JNILocalRefType alacağınız anlamına gelir.

  • @FastNative ve @CriticalNative

    Android 7'ye kadar bu optimizasyon ek açıklamaları yoksayıldı. @CriticalNative için ABI uyuşmazlığı, yanlış bağımsız değişkene yol açar ve kilitlenmelere yol açabilir.

    @FastNative ve @CriticalNative yöntemleri için yerel işlevlerin dinamik araması, Android 8-10'da uygulanmamıştır ve Android 11'de bilinen hataları içermektedir. Bu optimizasyonların JNI RegisterNatives ile açık bir şekilde kaydedilmeden kullanılması Android 8-11'de kilitlenmelere yol açabilir.

SSS: Neden UnsatisfiedLinkError alıyorum?

Yerel kod üzerinde çalışırken genellikle aşağıdaki gibi bir hata ile karşılaşabilirsiniz:

java.lang.UnsatisfiedLinkError: Library foo not found

Bazı durumlarda bunun anlamı şudur: Kitaplık bulunamadı. Diğer durumlarda kitaplık mevcut ancak dlopen(3) tarafından açılamadı. Hatanın ayrıntıları, istisnanın ayrıntı mesajında bulunabilir.

"Kitaplık bulunamadı" istisnalarıyla karşılaşmanızın yaygın nedenleri:

  • Kitaplık mevcut değil veya uygulama tarafından erişilebilir değil. Özelliğin varlığını ve izinlerini kontrol etmek için adb shell ls -l <path> öğesini kullanın.
  • Kütüphane NDK ile oluşturulmadı. Bu durum, cihazda bulunmayan işlevlere veya kitaplıklara bağımlılıklara neden olabilir.

Diğer bir UnsatisfiedLinkError başarısızlık sınıfı şöyle görünür:

java.lang.UnsatisfiedLinkError: myfunc
        at Foo.myfunc(Native Method)
        at Foo.main(Foo.java:10)

Logcat'te şunları görürsünüz:

W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

Bu, çalışma zamanının bir eşleşme yöntemi bulmaya çalıştığı ancak başarısız olduğu anlamına gelir. Bunun yaygın nedenlerinden bazıları şunlardır:

  • Kitaplık yüklenmiyor. Kitaplık yüklemeyle ilgili mesajlar için logcat çıkışını kontrol edin.
  • Ad veya imza uyuşmazlığı nedeniyle yöntem bulunamıyor. Bu durumun nedeni genellikle:
    • Geç yöntem araması için C++ işlevlerinin extern "C" ve uygun görünürlük (JNIEXPORT) ile bildirilememesi. Ice Cream Sandwich'ten önce JNIEXPORT makrosu hatalı olduğundan eski jni.h ile yeni bir GCC'nin kullanılamayacağını unutmayın. Simgeleri kitaplıkta göründükleri gibi görmek için arm-eabi-nm kullanabilirsiniz. Semboller karışık görünüyorsa (Java_Foo_myfunc yerine _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass gibi) veya simge türü büyük harf "T" yerine küçük harfli "t" ise beyanı ayarlamanız gerekir.
    • Açıkça kayıt için yöntem imzası girilirken küçük hatalar. Kayıt çağrısına aktardığınız şeyin, günlük dosyasındaki imzayla eşleştiğinden emin olun. 'B'nin byte ve 'Z'nin boolean olduğunu unutmayın. İmzalardaki sınıf adı bileşenleri "L" ile başlar, ";" ile biter, paket/sınıf adlarını ayırmak için "/" karakterini ve sınıf içi adlarını ayırmak için "$" işaretini kullanır (örneğin, Ljava/util/Map$Entry;).

JNI başlıklarını otomatik olarak oluşturmak için javah kullanılması bazı sorunların önüne geçmeye yardımcı olabilir.

SSS: FindClass sınıfımı neden bulamadı?

(Bu tavsiyenin çoğu, GetMethodID veya GetStaticMethodID içeren yöntemlerin ya da GetFieldID veya GetStaticFieldID içeren alanların bulunamadığı için de aynı derecede geçerlidir.)

Sınıf adı dizesinin doğru biçime sahip olduğundan emin olun. JNI sınıf adları paket adıyla başlar ve java/lang/String gibi eğik çizgiyle ayrılır. Bir dizi sınıfı arıyorsanız doğru sayıda köşeli parantezle başlamanız ve sınıfı "L" ve ";" ile sarmalamanız gerekir. Dolayısıyla, tek boyutlu String dizisi [Ljava/lang/String; olur. Bir iç sınıfı arıyorsanız "." yerine "$" kullanın. Genel olarak, .class dosyasında javap kullanmak, sınıfınızın dahili adını bulmanın iyi bir yoludur.

Kod küçültmeyi etkinleştirirseniz hangi kodun tutulacağını yapılandırdığınızdan emin olun. Kod küçültücü, aksi takdirde yalnızca JNI'den kullanılan sınıfları, yöntemleri veya alanları kaldırabileceğinden, uygun koruma kuralları yapılandırılması önemlidir.

Sınıf adı doğru görünüyorsa sınıf yükleyici sorunuyla karşılaşıyor olabilirsiniz. FindClass, kodunuzla ilişkilendirilmiş sınıf yükleyicide sınıf aramayı başlatmak istiyor. Aşağıdaki gibi görünecek çağrı yığınını inceler:

    Foo.myfunc(Native Method)
    Foo.main(Foo.java:10)

En üstteki yöntem Foo.myfunc. FindClass, Foo sınıfıyla ilişkili ClassLoader nesnesini bulur ve bunu kullanır.

Bu işlem genellikle istediğiniz işlemi gerçekleştirir. Kendiniz bir iş parçacığı oluşturursanız (pthread_create yöntemini çağırıp AttachCurrentThread ile ekleyerek) sorun yaşayabilirsiniz. Artık uygulamanızda yığın çerçeveleri yoktur. Bu iş parçacığından FindClass çağırırsanız JavaVM, uygulamanızla ilişkilendirilen sınıf yükleyici yerine "sistem" sınıf yükleyicisinde başlar. Bu nedenle, uygulamaya özel sınıfları bulma girişimleri başarısız olur.

Bu sorunu birkaç şekilde çözebilirsiniz:

  • FindClass aramalarınızı JNI_OnLoad içinde bir kez yapın ve sınıf referanslarını daha sonra kullanmak üzere önbelleğe alın. JNI_OnLoad yürütme işleminin bir parçası olarak yapılan tüm FindClass çağrıları, System.loadLibrary adlı işlevle ilişkilendirilmiş sınıf yükleyiciyi kullanır (bu, kitaplık başlatmanın daha kolay olması için sağlanan özel bir kuraldır). Uygulama kodunuz kitaplığı yüklüyorsa FindClass doğru sınıf yükleyiciyi kullanır.
  • Sınıf bağımsız değişkenini alacak yerel yönteminizi tanımlayarak ve ardından Foo.class öğesini ileterek sınıfın bir örneğini gerekli işlevlere iletin.
  • Referansı ClassLoader nesnesine yakın bir yerde önbelleğe alın ve doğrudan loadClass çağrısı yapın. Bu işlem biraz çaba gerektirir.

SSS: Ham verileri yerel kodla nasıl paylaşırım?

Kendinizi, hem yönetilen hem de yerel koddan büyük bir ham veri arabelleğine erişmeniz gerektiği bir durumda bulabilirsiniz. Yaygın örnekler arasında bit eşlemlerin veya ses örneklerinin işlenmesi sayılabilir. İki temel yaklaşım vardır.

Verileri bir byte[] dosyasında depolayabilirsiniz. Bu, yönetilen koddan çok hızlı erişime olanak tanır. Bununla birlikte, yerel tarafta, verilere kopyalamak zorunda kalmadan erişebilmenizin garantisi yoktur. Bazı uygulamalarda GetByteArrayElements ve GetPrimitiveArrayCritical, yönetilen yığındaki ham verilere gerçek işaretçiler döndürür, diğerlerinde ise yerel yığına bir tampon ayırıp verileri kopyalar.

Alternatif yöntem, verilerin bir bayt arabelleğinde depolanmasıdır. Bunlar java.nio.ByteBuffer.allocateDirect veya JNI NewDirectByteBuffer işleviyle oluşturulabilir. Normal bayt arabelleklerinden farklı olarak, depolama alanı yönetilen yığında ayrılmaz ve her zaman doğrudan yerel koddan erişilebilir (adresi GetDirectBufferAddress ile alın). Doğrudan bayt arabelleği erişiminin nasıl uygulandığına bağlı olarak, yönetilen koddan verilere erişim çok yavaş olabilir.

Hangi yöntemin kullanılacağı iki faktöre bağlıdır:

  1. Veri erişimlerinin çoğu Java'da veya C/C++'ta yazılan koddan mı gerçekleşecek?
  2. Veriler nihayetinde bir sistem API'sine aktarılıyorsa hangi biçimde olması gerekir? (Örneğin, veriler nihayetinde bayt[] alan bir işleve iletilirse, doğrudan ByteBuffer içinde işlem yapmak mantıklı olmayabilir.)

Net bir kazanan yoksa doğrudan bayt arabelleği kullanın. Bunlara yönelik destek doğrudan JNI'de yerleşiktir ve performans gelecekteki sürümlerde yükselecektir.