JNI ipuçları

JNI, Java Yerel Arayüzüdür. Bu model, Android'in yönetilen koddan (Java veya Kotlin programlama dillerinde yazılmış) derlediği bayt kodunun yerel kodla (C/C++ dilinde yazılmış) etkileşime girmesi için bir yol tanımlar. JNI tedarikçiden bağımsızdır, dinamik paylaşılan kitaplıklardan kod yükleme

Not: Android, Kotlin'i Java programlama diline benzer bir şekilde ART'da kullanılabilen bayt koduna derlediğinden, bu sayfadaki bilgileri JNI mimarisi ve 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 konuda bilgi sahibi değilseniz JNI'nin işleyiş şekli ve hangi özelliklerin sunulduğuna dair bir fikir edinmek için Java Yerel Arayüz Spesifikasyonu'nu okuyun. Arayüzün bazı yönleri ilk okumada hemen göze çarpmayacağından sonraki bölümleri kolayca bulabilirsiniz.

Genel JNI referanslarına göz atmak ve genel JNI referanslarının nerede oluşturulup silindiğini görmek için Android Studio 3.2 ve sonraki sürümlerde Memory Profiler'da 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, dikkat edilmesi 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 kaynak karmaşasını en aza indirin. JNI katmanının genelinde Marshall'ın uygulanması önemsiz olmayan maliyetlere sahiptir. Düzenleme için ihtiyacınız olan veri miktarını ve verileri sıralamanız gereken sıklığı en aza indiren bir arayüz tasarlamaya çalışın.
  • Mümkün olduğunda yönetilen bir programlama dilinde yazılan kod ile C++'da yazılan kod arasında eşzamansız iletişimden kaçının. Bu, JNI arayüzünüzün bakımını daha kolay hale getirecektir. 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. Bu iş parçacıklarından biri, engelleyen bir C++ çağrısı yapar ve ardından 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ıklarının 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ı arasında değil, 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 temel veri yapısını tanımlar. Bunların her ikisi de temelde fonksiyon tablolarına işaret eden öğelerdir. (C++ sürümünde, fonksiyon tablosuna işaretçisi ve tablo üzerinden dolaylı her JNI işlevi için üye işlevi bulunan sınıflardır.) JavaVM, bir JavaVM oluşturmanıza ve yok etmenize olanak sağlayan "çağrı arayüzü" işlevleri sağlar. Teoride, işlem başına birden çok Java sanal makinesi olabilir, ancak Android yalnızca bir Java sanal makinesine izin verir.

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

JNIEnv, iş parçacığı yerel depolaması için kullanılır. Bu nedenle, ileti dizileri arasında JNIEnv paylaşamazsınız. Bir kod parçasının JNIEnv'i almanın başka bir yolu yoksa JavaVM'yi paylaşmanız ve iş parçacığının JNIEnv'ini keşfetmek için GetEnv kullanmanız gerekir. (Bir adet olduğu varsayıldığında; aşağıdaki AttachCurrentThread politikasına bakın.)

JNIEnv ve JavaVM'nin C bildirimleri, C++ bildirimlerinden farklıdır. "jni.h" dahil etme dosyası, C veya C++'a eklenmesine bağlı olarak farklı typedef'ler sağlar. Bu nedenle, JNIEnv bağımsız değişkenlerinin her iki dil tarafından eklenen üstbilgi dosyalarına 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'e işaret ediyorsa ek çalışma yapmanız gerekebilir.)

Mesaj dizileri

Tüm ileti dizileri, çekirdek tarafından programlanmış Linux ileti dizileridir. Bunlar genellikle yönetilen koddan başlatılır (Thread.start() kullanılarak) ancak başka bir yerde de oluşturulup JavaVM öğesine eklenebilir. Örneğin, pthread_create() veya std::thread ile başlayan bir ileti dizisi, AttachCurrentThread() ya da AttachCurrentThreadAsDaemon() işlevleri kullanılarak eklenebilir. Bir iş parçacığı eklenene kadar JNIEnv'e sahip değildir ve JNI çağrıları yapamaz.

Java kodunu çağırması gereken bir iş parçacığı oluşturmak için Thread.start() kullanılması genellikle en iyisidir. Bunu yaptığınızda yeterli yığın alanınız olduğundan, doğru ThreadGroup ayarında olduğunuzdan ve Java kodunuzla aynı ClassLoader kullandığınızdan emin olursunuz. İş parçacığının adını Java'da hata ayıklama için ayarlamak, yerel koddan daha kolay (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 java.lang.Thread nesnesinin oluşturulup "main" ThreadGroup öğesine eklenmesine neden olarak hata ayıklayıcı tarafından görülebilir hale gelir. Zaten ekli olan bir ileti dizisinde AttachCurrentThread() çağrısı yapılamaz.

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 ileti dizileri, çıkmadan önce DetachCurrentThread() numarasını çağırmalıdır. Bunu doğrudan kodlamak tuhafsa Android 2.0 (Eclair) ve sonraki sürümlerde iş parçacığı çıkmadan önce çağrılacak bir yıkıcı işlev tanımlamak ve oradan DetachCurrentThread() çağırmak için pthread_key_create() kullanabilirsiniz. (Bu anahtarı pthread_setspecific() ile kullanarak JNIEnv'ithread-local-storage'da depolayın. Bu şekilde anahtar, 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 şunları yaparsınız:

  • 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 değerle alın

Benzer şekilde, bir yöntemi çağırmak için önce bir sınıf nesne referansı, 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 birkaç dize karşılaştırması gerekebilir, ancak onlara alanı almak veya yöntemi çağırmak için asıl çağrı çok hızlıdır.

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

Sınıf referansları, alan kimlikleri ve yöntem kimlikleri, sınıf kaldırılana kadar geçerli olacaktır. Sınıfların yüklemesi yalnızca ClassLoader ile ilişkili tüm sınıflar çöp toplama gerçekleştirilebiliyorsa kaldırılır ancak 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 isterseniz kimlikleri başlatmanın doğru yolu ilgili sınıfa aşağıdakine 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ı yapan 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 tekrar 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 başvuru" olur. 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 çalışmaya 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 bir süre tutmak isterseniz "global" referans kullanmanız gerekir. NewGlobalRef işlevi, yerel referansı bağımsız değişken olarak alır ve genel bir sonuç döndürür. Siz DeleteGlobalRef yöntemini çağırana kadar genel referansın geçerli olacağı garanti edilir.

Bu kalıp genellikle FindClass öğesinden döndürülen bir jclass önbelleğe alınırken 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ın farklı değerleri olabilir. Örneğin, aynı nesnede art arda yapılan NewGlobalRef çağrılarının döndürdüğü 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ırmamaları" gerekir. Pratikte bu, büyük olasılıkla bir dizi nesne ile çalışırken çok sayıda yerel referans oluşturuyorsanız bunu JNI'nin yapmasına izin vermek yerine DeleteLocalRef ile manuel olarak serbest bırakmanız gerektiği anlamına gelir. Uygulama yalnızca 16 yerel referans için slot ayırmak için gereklidir. Bu nedenle, bundan daha fazlasına ihtiyacınız varsa ilerledikçe silmeli veya daha fazla yerel referans için EnsureLocalCapacity/PushLocalFrame kullanmalısınız.

jfieldID ve jmethodID öğelerinin opak türler olduğunu, nesne referansı olmadığını 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 Yayın çağrısına kadar geçerlidir.)

Olağan dışı bir vakanın ayrıca belirtilmesi 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, döngüde yerel referanslar oluşturan her yerel kodun 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 (hatalı) davranışlara neden olabilir. Diğer her şey eşit olduğunda daha az genel referans içeren 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ığından C kodu için yararlıdır. Bunun en iyi tarafı, standart libc dizesi işlevleriyle kullanıma uygun, C stili sıfır sona ermiş dizelere güvenebilmenizdir. Kötü tarafı, rastgele UTF-8 verilerini JNI'ye iletememeniz ve düzgün çalışmasını bekleyememenizdir.

String öğesinin UTF-16 gösterimini almak için GetStringChars işlevini 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çisini ve dize uzunluğunu beklemeniz gerekir.

Get oluşturduğunuz dizeleri Release ilemeyi unutmayın. Dize işlevleri, jchar* veya jbyte* değerini döndürür. Bunlar, yerel referanslar yerine temel verilere yönlendiren C stili işaretçilerdir. Release çağrılana kadar geçerli olmaları garanti edilir. Diğer bir deyişle, yerel yöntem döndürüldüğünde bunlar yayınlanmaz.

NewStringUTF'e iletilen veriler, Değiştirilmiş UTF-8 biçiminde olmalıdır. Bir dosyadan veya ağ akışından karakter verilerini okuyup filtrelemeden NewStringUTF hizmetine vermek yaygın 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 Değiştirilmiş UTF-8 biçimine 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ği için Android 8'den önce UTF-16 dizeleriyle çalışmak genellikle daha hızlıydı, GetStringUTFChars ise ayırma ve UTF-8 biçimine dönüştürme gerektiriyordu. Android 8, String gösterimini, ASCII dizeleri için karakter başına 8 bit kullanacak şekilde değiştirdi (bellek tasarrufu için) ve hareketli çöp toplayıcı kullanmaya başladı. Bu özellikler, ART'ın GetStringCritical için bile, kopyalama yapmadan String verilerine işaretçi sağlayabileceği durumların sayısını önemli ölçüde azaltır. Bununla birlikte, kod tarafından işlenen dizelerin çoğu kısaysa yığına ayrılmış arabellek ve GetStringRegion veya GetStringUTFRegion kullanarak çoğu durumda ayırma ve ayırma işlemlerinden kaçınmak mümkündür. Örneğin:

    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şim için işlevler sağlar. Nesne dizilerine her defasında bir girişe erişilmesi gerekirken, temel öğe dizileri, C'de tanımlanmış 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 biraz bellek ayırmasına ve kopyasını oluşturmasına olanak tanır. Her iki durumda da döndürülen ham işaretçinin karşılık gelen Release çağrısı yapılıncaya kadar geçerli olacağı garanti edilir (yani, veriler kopyalanmıyorsa dizi nesnesinin yığın sıkıştırma kapsamında sabitleneceği ve yerinin değiştirilemeyeceği anlamına gelir). Get değerindeki her dizi için Release işlemini yapmanız gerekir. Ayrıca, Get çağrısı başarısız olursa kodunuzun daha sonra NULL 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 işaretçi mi yoksa kopya mı döndürdüğüne bağlıdır:

  • 0
    • Gerçek: Dizi nesnesinin sabitlemesi kaldırıldı.
    • Kopyala: Veriler geri kopyalanır. Kopyanın bulunduğu arabellek serbest bırakıldı.
  • 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ı. Ö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 ile JNI_COMMIT çağrısı yapmanız gerekip gerekmediğini öğrenmektir. Değişiklik yapmak ve dizinin içeriklerini kullanan kodu yürütmek arasında geçiş yapıyorsanız işlemsiz kaydetmeyi atlayabilirsiniz. İşareti kontrol etmenin bir diğer olası nedeni de JNI_ABORT öğesinin verimli bir şekilde kullanılmasıdır. Örneğin, bir dizi almak, diziyi yerinde 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ız gerekmez. JNI size orijinali iletiyorsa kendi kopyanızı oluşturmanız gerekir.

*isCopy yanlış olduğunda Release çağrısını atlayabileceğinizi varsaymak, 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.

Ayrıca JNI_COMMIT işaretinin diziyi yayınlamadığını ve sonunda Release öğesini farklı bir işaretle tekrar çağırmanız gerektiğini unutmayın.

Bölge çağrıları

Tek yapmanız gereken verileri kopyalamak veya dışarı aktarmak olduğunda Get<Type>ArrayElements ve GetStringChars gibi çağrılar çok kullanışlı 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 yakalar, içindeki ilk len bayt öğelerini kopyalar ve daha sonra diziyi serbest bırakır. Uygulamaya bağlı olarak Get çağrısı, dizi içeriklerini sabitler veya kopyalar. Kod, verileri kopyalar (belki de ikinci kez) ve ardından Release çağrısı yapar. Bu durumda JNI_ABORT, üçüncü kopyanın kullanılmaması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ı gerektirir ve ek yükü azaltır.
  • Sabitleme veya ekstra veri kopyaları gerektirmez.
  • Programcı hatası riskini azaltır. Herhangi bir hata oluştuğunda Release işlevini çağırmayı unutma riski ortadan kalkar.

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

İstisnalar

Bir istisna beklemedeyken çoğu JNI işlevini çağırmamalısınız. Kodunuzun istisnayı fark etmesi (işlevin döndürülen değeri, ExceptionCheck veya ExceptionOccurred aracılığıyla) ve geri alması 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 yol sağlar. Örneğin, NewString NULL olmayan bir değer döndürürse istisna olup olmadığını kontrol etmeniz gerekmez. Bununla birlikte, bir yöntem çağırırsanız (CallObjectMethod gibi bir işlev kullanarak) her zaman istisna olup olmadığını kontrol etmeniz gerekir. Çünkü istisna verilirse döndürülen değer geçerli olmaz.

Yönetilen kod tarafından oluşturulan istisnaların yerel yığın çerçevelerini açmadığını unutmayın. (Ayrıca, Android'de genellikle önerilmeyen C++ istisnaları, C++ kodundan yönetilen koda JNI geçiş sınırını aşmamalı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önüldükten sonra, istisna not edilir ve uygun şekilde işlenir.

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

Throwable nesnesinin kendisinde değişiklik yapmak için yerleşik herhangi bir işlev 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 aktarabileceğiniz bir şey almak için GetStringUTFChars işlevini kullanmanız gerekir.

Kapsamı genişletilmiş vadesiz hesap

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

Ek kontroller şunları içerir:

  • Diziler: Negatif boyutlu bir diziyi ayırmaya çalışıyor.
  • 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ı: Bir JNI çağrısına sınıf adının “java/lang/String” stili dışındaki her şeyi iletme.
  • Kritik çağrılar: "kritik" erişim ile ilgili sürüm arasında JNI çağrısı yapılması.
  • Direct ByteBuffers: NewDirectByteBuffer öğesine hatalı bağımsız değişkenler iletme.
  • İstisnalar: Bekleyen bir istisna varken JNI çağrısı yapma.
  • JNIEnv*s: Yanlış ileti dizisinden JNIEnv* kullanılıyor.
  • jfieldIDs: NULL jfieldID kullanma veya bir alanı yanlış türde bir değere ayarlamak için jfieldID kullanma (örneğin, bir String alanına String alanına StringBuilder atamaya çalışmak) veya bir örnek alanını ayarlamak için statik bir alan için jfieldID kullanma (veya tam tersi), ya da bir sınıftan jfieldID kullanarak başka bir sınıfın örneklerinden yararlanma.
  • jmethodIDs: Call*Method JNI çağrısı yaparken yanlış jmethodID türü kullanma: yanlış dönüş türü, statik/statik olmayan uyumsuzluk, "this" 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 yanlış referans türünde kullanılıyor.
  • Sürüm modları: Yayın çağrısına hatalı sürüm modu iletme (0, JNI_ABORT veya JNI_COMMIT dışında bir mod).
  • Tür güvenliği: Yerel yönteminizden uyumsuz bir tür döndürme (örneğin, bir Dize döndürdüğü bildirilen 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 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 noktadan itibaren başlatılan tüm uygulamalarda CheckJNI etkinleştirilir. (Özelliği başka bir değerle değiştirin veya sadece yeniden başlatmak CheckJNI'yi tekrar devre dışı bırakır.) Bu durumda, bir uygulama tekrar başlatıldığında logcat çıkışınızda aşağıdakine 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 sunmaktadır.

Bir statik sınıf başlatıcıdan System.loadLibrary (veya ReLinker.loadLibrary) çağrısı yapın. Bağımsız değişken "undecorated" kitaplığının adıdır. Bu nedenle, libfubar.so öğesini yüklemek için "fubar" içinde iletirsiniz.

Yerel yöntemler içeren 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 yer alması mantıklıdır. Aksi takdirde, kitaplığın her zaman ve her zaman erken yüklendiğinden emin olmak için Application üzerinden ç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 bunları dlsym ile dinamik olarak aramasına izin verebilirsiniz. RegisterNatives ürününün avantajları, sembollerin mevcut olup olmadığını önceden kontrol etmenizdir. Ayrıca, JNI_OnLoad dışında hiçbir şeyi dışa aktarmadan daha küçük ve daha hızlı paylaşılan kitaplıklara sahip olmanızdır. Çalışma zamanının işlevlerinizi keşfetmesini sağlamanın avantajı, yazılacak daha az kod olmasıdır.

RegisterNatives uygulamasını kullanmak için:

  • JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) işlevi sağlayın.
  • JNI_OnLoad hesabınızda, tüm yerel yöntemlerinizi RegisterNatives kullanarak kaydedin.
  • Kitaplığınızdan yalnızca JNI_OnLoad dışa aktarılması için -fvisibility=hidden ile oluşturun. Bu yöntem, 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 kullanışlı yığın izlemeler 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 yöntemleri belirli bir şekilde adlandırmanız gerekir (ayrıntılar için JNI spesifikasyonuna bakın). Yani bir yöntem imzası yanlışsa yöntem ilk kez çalıştırılana kadar bu imzadan haberdar olmazsınız.

JNI_OnLoad ürününden 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 en üst kısmındaki yöntemle ilişkilendirilen sınıf yükleyicisini kullanır veya bir sınıf yükleyicisi 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ı hakkında bilgi sahibi değildir. Bu nedenle, söz konusu bağlamda FindClass ile kendi sınıflarınızı arayamazsınız. Bu, JNI_OnLoad öğesini sınıfları aramak ve önbelleğe almak için uygun bir yerdir: Geçerli bir jclass genel referansına sahip olduğunuzda ekli herhangi bir iş parçacığından 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) ile ek açıklamalar eklenebilir. Ancak bu ek açıklamalar, kullanılmadan önce dikkatlice değerlendirilmesi gereken bazı davranış değişiklikleriyle gelir. Bu değişikliklerden kısaca bahsetmekle birlikte, ayrıntılar için lütfen dokümanları inceleyin.

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

Bir @FastNative veya @CriticalNative yöntemi yürütülürken atık toplama işlemi, iş parçacığını önemli işler için askıya alamaz ve engellenebilir. Bu ek açıklamaları genellikle hızlı ancak genellikle sınırlı olmayan yöntemler de dahil olmak üzere uzun süreli yöntemler için kullanmayın. Özellikle, kodun önemli G/Ç işlemleri gerçekleştirmemesi veya uzun süre tutulabilen yerel kilitler elde etmemesi gerekir.

Bu ek açıklamalar, Android 8'den beri sistem kullanımı için uygulanmış ve Android 14'te CTS tarafından test edilmiş herkese açık API haline gelmiştir. Bu optimizasyonların Android 8-13 cihazlarda da çalışması muhtemeldir (güçlü CTS garantileri olmasa da) yerel yöntemlerin dinamik araması 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şkenin eşleştirilmesine ve kilitlenmelere yol açabilir.

Bu ek açıklamalara ihtiyaç duyulan performans açısından kritik yöntemler için, yerel yöntemlerin ada dayalı "keşif"ine bağlı kalmak yerine yöntemleri JNI RegisterNatives ile açık bir şekilde kaydetmeniz önerilir. Optimum uygulama başlatma performansı elde etmek için @FastNative veya @CriticalNative yöntemlerini kullananları temel profile eklemeniz önerilir. Android 12'den beri, derlenmiş yönetilen bir yöntemden @CriticalNative yerel yönteme yapılan ç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 8 kayan nokta bağımsız değişkenine kadar).

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

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 gereken noktalar

64 bit işaretçiler kullanan mimarileri desteklemek için Java alanında yerel bir yapıya işaret eden işaretçiyi depolarken int yerine bir 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 verilerinin aktarılması işe yaramaz.

Eski Android sürümleriyle geriye dönük uyumluluk için şunları bilmeniz gerekebilir:

  • Yerel işlevlerin dinamik araması

    Android 2.0'a (Eclair) kadar, yöntem adları için yapılan aramalarda "$" karakteri düzgün şekilde "_00024" karakterine 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

    "İş parçacığının çıkıştan önce ayrılması gerekli" kontrolünden kaçınmak için pthread_key_create yıkıcı işlevi Android 2.0'a (Eclair) kadar kullanılabiliyordu. (Ç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 global referanslar

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

    Zayıf küresel referanslar, Android 4.0'a (Ice Cream Sandwich) kadar yalnızca NewLocalRef, NewGlobalRef ve DeleteWeakGlobalRef için iletilebiliyordu. (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.)

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

  • Yerel referanslar

    Yerel referanslar, Android 4.0'a (Ice Cream Sandwich) kadarki sürümlerde aslında doğrudan işaretçiler olarak biliniyordu. Ice Cream Sandwich, daha iyi çöp toplayıcıları desteklemek için gerekli olan yönergeyi ekledi. Ancak bu, eski sürümlerde çok sayıda JNI hatasının tespit edilemeyeceği anlamına geliyor. Daha fazla bilgi için ICS'deki JNI Yerel Referans Değişiklikleri sayfasını inceleyin.

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

  • GetObjectRefType ile referans türünü belirleme

    Doğrudan işaretçi kullanımı (yukarıya bakın) nedeniyle, Android 4.0'a (Ice Cream Sandwich) kadarki GetObjectRefType yaklaşımını doğru bir şekilde uygulamak imkânsızdı. Bunun yerine, zayıf globals tablosunu, bağımsız değişkenleri, yerel tabloyu ve globals tablosunu bu sırayla inceleyen bir mantıksal yöntem kullandık. Doğrudan işaretçinizi ilk kez bulduğunda, referansınızın incelemekte olduğu türde olduğunu bildirir. Örneğin, statik yerel yönteminize dolaylı bir bağımsız değişken olarak iletilen jclass ile aynı olan global bir jclass'ta GetObjectRefType çağrısı yaptıysanız JNIGlobalRefType yerine JNILocalRefType değeri elde edersiniz.

  • @FastNative ve @CriticalNative

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

    @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 aşağıdaki gibi bir hata görmek yaygın bir durumdur:

java.lang.UnsatisfiedLinkError: Library foo not found

Bazı durumlarda ne anlama geliyor? Kitaplık bulunamadı. Diğer durumlarda kitaplık mevcuttur 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. Kitaplığın varlığını ve izinlerini kontrol etmek için adb shell ls -l <path> öğesini kullanın.
  • Kitaplık NDK ile oluşturulmadı. Bu durum, cihazda bulunmayan işlevlere veya kitaplıklara bağımlılıklara neden olabilir.

Başka 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ştirme yöntemi bulmaya çalıştığı ancak başarısız olduğu anlamına gelir. Bu durumun yaygın nedenlerinden bazıları şunlardır:

  • Kitaplık yüklenmiyor. Kitaplık yüklemesiyle ilgili mesajlar için logcat çıkışını kontrol edin.
  • Ad veya imza uyuşmazlığı nedeniyle yöntem bulunamıyor. Bu durum genellikle aşağıdakilerden kaynaklanır:
    • Geç yöntem araması için C++ işlevlerinin extern "C" ve uygun görünürlük (JNIEXPORT) ile bildirilememesijni.h Simgeleri kitaplıkta göründükleri gibi görmek için arm-eabi-nm kullanabilirsiniz. Bunlar karıştırılmış görünüyorsa (Java_Foo_myfunc yerine _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass gibi bir şey) 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 ilettiklerinizin 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 "/", sınıf içi adları ayırmak için ise "$" kullanılır (ör. Ljava/util/Map$Entry;).

JNI başlıklarını otomatik olarak oluşturmak için javah kullanmak bazı sorunların önüne geçebilir.

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

(Bu önerilerin çoğu, GetMethodID veya GetStaticMethodID içeren yöntemlerin ya da GetFieldID veya GetStaticFieldID içeren alanların bulunamaması 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. Dizi sınıfı arıyorsanız uygun sayıda köşeli parantezle başlamanız ve sınıfı "L" ve ";" ile sarmalamanız gerekir. Böylece, tek boyutlu String dizisi [Ljava/lang/String; olur. Dahili bir 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 daraltmayı etkinleştirirseniz hangi kodu tutacağınızı yapılandırdığınızdan emin olun. Kod küçültücü, aksi takdirde yalnızca JNI'den kullanılan sınıf, yöntem veya alanları kaldırabileceğinden, uygun koruma kurallarını yapılandırmak önemlidir.

Sınıf adı doğru görünüyorsa sınıf yükleyicisi sorunuyla karşılaşmış olabilirsiniz. FindClass, kodunuzla ilişkilendirilmiş sınıf yükleyicide sınıf aramasını başlatmak istiyor. Çağrı yığınını inceler ve aşağıdaki gibi görünür:

    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 ardından AttachCurrentThread ile ekleyerek) artık sorun yaşayabilirsiniz. Artık uygulamanızdan yığın çerçeveler yoktur. Bu iş parçacığından FindClass çağrısı yaparsanız JavaVM, uygulamanızla ilişkilendirilen sınıf yükleyicisi yerine "sistem" sınıf yükleyicisinde başlar. Bu nedenle, uygulamaya özel sınıfları bulma denemeleri başarısız olur.

Bu sorunu çözmek için birkaç yol vardır:

  • FindClass ile JNI_OnLoad içinde bir kez arama yapın ve sınıf referanslarını daha sonra kullanmak üzere önbelleğe alın. JNI_OnLoad yürütme işleminin 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ığın başlatılmasını daha kullanışlı hale getirmek 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ımlayıp ardından Foo.class öğesini geçirerek sınıfın bir örneğini buna ihtiyaç duyan işlevlere iletin.
  • ClassLoader nesnesine ait referansı kolay bir yerde önbelleğe alın ve doğrudan loadClass çağrıları yapın. Bu işlem biraz çaba gerektirir.

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

Hem yönetilen hem de yerel koddan büyük bir ham veri arabelleğine erişmeniz gerektiği bir durumla karşılaşabilirsiniz. Yaygın örnekler arasında bit eşlemlerin veya ses örneklerinin değiştirilmesi sayılabilir. İki temel yaklaşım vardır.

Verileri byte[] klasöründe depolayabilirsiniz. Bu, yönetilen koddan çok hızlı erişime olanak tanır. Bununla birlikte, yerel tarafta, verilere kopyalamak zorunda kalmadan erişmenizin garanti edilmez. Bazı uygulamalarda, GetByteArrayElements ve GetPrimitiveArrayCritical, yönetilen yığındaki ham verilere gerçek işaretçiler döndürür, bazı durumlarda ise yerel yığına bir arabellek ayırıp verileri kopyalar.

Alternatif yöntem, verileri doğrudan bir bayt arabelleğinde depolamaktır. Bunlar java.nio.ByteBuffer.allocateDirect veya JNI NewDirectByteBuffer işleviyle oluşturulabilir. Normal bayt arabelleklerinden farklı olarak, depolama alanı yönetilen yığında tahsis edilmez 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 yaklaşımın kullanılacağı iki faktöre bağlıdır:

  1. Veri erişimlerinin çoğu Java'da veya C/C++'ta yazılan kodlardan mı gerçekleşecek?
  2. Veriler nihayetinde bir sistem API'sine aktarılıyorsa hangi biçimde olması gerekir? (Örneğin, veriler nihayetinde bir bayt [] alan bir fonksiyona aktarılırsa doğrudan ByteBuffer içinde işleme yapılması 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 gelecekteki sürümlerde performans artacaktır.