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 geçmek için derlediği bir yöntemi tanımlar. JNI, tedarikçiden bağımsızdır, dinamik paylaşılan kitaplıklardan kod yükleme desteği sunar ve zaman zaman elverişli olsa da makul düzeyde verimlidir.

Not: Android, Java programlama diline benzer ş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ını inceleyin.

Bu konuda bilgi sahibi değilseniz JNI'nin nasıl çalıştığı ve mevcut özellikler hakkında bilgi edinmek için Java Yerel Arayüz Spesifikasyonu'nu okuyun. Arayüzün bazı özellikleri ilk okumada hemen net bir şekilde anlaşılmayabilir. Bu nedenle sonraki bölümleri kullanışlı 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 Bellek Profil Aracı'nda bulunan JNI yığın görünümünü kullanın.

Genel ipuçları

JNI katmanınızın kapladığı alanı 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 sıralama işlemini en aza indirin. JNI katmanında yeniden pazarlama yapmanın önemsiz 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ılan kod arasındaki eşzamansız iletişimden kaçının. Böylece JNI arayüzünüzün bakımı daha kolay olacaktı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. Burada iki iş parçacığından biri, engelleme çağrısı yapan 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 tanımlanabilir C++ ve Java kaynak konumunda tutun. Uygun şekilde bir JNI otomatik oluşturma kitaplığı kullanabilirsiniz.

JavaVM ve JNIEnv

JNI, "JavaVM" ve "JNIEnv" adlı iki temel veri yapısı tanımlar. Bunların her ikisi de temelde işlev tablolarına işaret eden öğelerdir. (C++ sürümünde, fonksiyon tablosu işaretçisi ve tablo üzerinden dolaylı her bir JNI işlevi için üye işlevi olan sınıflardır.) JavaVM, bir JavaVM oluşturmanıza ve yok etmenize olanak sağlayan "çağrı arayüzü" işlevlerini sağlar. Teoride işlem başına birden fazla JavaVM'niz olabilir ama Android sadece bir JavaVM'ye izin verir.

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

JNIEnv, iş parçacığı yerel depolaması için kullanılır. Bu nedenle, bir JNIEnv dosyasını ileti dizileri arasında paylaşamazsınız. Bir kod parçasının JNIEnv'i almasını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 tane 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" içerme dosyası, C veya C++'ya eklenip eklenmediğine bağlı olarak farklı typedef'ler sağlar. Bu nedenle, JNIEnv bağımsız değişkenlerinin her iki dilde de bulunan başlık 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 ile ilgiliyse fazladan işlem yapmanız gerekebilir.)

Sohbetler

Tüm ileti dizileri, çekirdek tarafından programlanmış Linux ileti dizileridir. Genellikle yönetilen koddan başlarlar (Thread.start() kullanılarak), ancak başka bir yerde de oluşturulup JavaVM öğesine 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 olmaz ve JNI çağrıları yapamaz.

Java kodunu çağırması gereken iş parçacığı oluşturmak için Thread.start() genellikle en iyi seçenektir. Bu işlem yeterli yığın alanınızın olmasını, doğru ThreadGroup konumunda olduğunuzdan ve Java kodunuzla aynı ClassLoader kodunu kullandığınızdan emin olmanızı sağlar. İş parçacığının adını hata ayıklama için Java'da ayarlamak da yerel koddan daha kolaydır (pthread_t veya thread_t kullanıyorsanız pthread_setname_np() öğesine, std::thread kullanıyorsanız ve pthread_t istiyorsanız std::thread::native_handle() bölümüne bakın).

Yerel olarak oluşturulmuş bir iş parçacığı eklemek, bir java.lang.Thread nesnesinin derlenip "ana" ThreadGroup öğesine eklenmesine ve böylece hata ayıklayıcının görebilmesine neden olur. Zaten ekli olan bir ileti dizisinde AttachCurrentThread() çağrısı yapılamaz.

Android, yerel kod yürüten ileti dizilerini 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 iş parçacığını duraklatır.

JNI üzerinden eklenen ileti dizileri çıkış yapmadan önce DetachCurrentThread() işlevini çağırmalıdır. Bunu doğrudan kodlamak tuhafsa Android 2.0 (Eclair) ve sonraki sürümlerde pthread_key_create(), iş parçacığı çıkmadan önce çağrılacak bir yıkıcı işlev tanımlamak ve buradan DetachCurrentThread() çağrısı yapmak için kullanabilirsiniz. (JNIEnv'ithread-local-storage'da 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ı alın
  • GetFieldID ile alanın alan kimliğini alın
  • Alanın içeriğini GetIntField gibi uygun bir metinle alın.

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

Performans önemliyse değerleri bir kez aramak ve sonuçları yerel kodunuzda önbelleğe almak faydalıdır. İşlem başına bir JavaVM sınırı bulunduğ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ılıncaya kadar geçerli olacaktır. Sınıflar, yalnızca ClassLoader ile ilişkili tüm sınıflar atık halde toplanabilirse boşaltılır. Bu, nadiren olsa da 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 yeniden yüklenip yeniden yüklenmezse kimlikleri otomatik olarak yeniden önbelleğe almak isterseniz 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 genel referanslar

Yerel yönteme iletilen her bağımsız değişken ve bir JNI işlevi tarafından döndürülen hemen hemen her nesne bir "yerel başvuru"dur. Bu, yeni iş parçacığının mevcut 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, 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 adlı çağrıya kadar geçerli olacağı garanti edilir.

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

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

Tüm JNI yöntemleri bağımsız ve genel referansları bağımsız değişken olarak kabul eder. Aynı nesneye yapılan referansların farklı değerlere sahip olması mümkündür. Örneğin, aynı nesnede art arda yapılan çağrılardan NewGlobalRef öğesine yapılan döndürülen değerler farklı olabilir. İki başvurunun aynı nesneye başvurup bakmadığı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 da, yerel kodda nesne referanslarının sabit veya benzersiz olduğunu varsaymamak gerektiğidir. 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. Pratikte bu, çok sayıda yerel referans oluşturuyorsanız (örneğin, bir nesne dizisi üzerinden çalışırken) JNI'nin bunu sizin için yapmasına izin vermek yerine bunları DeleteLocalRef ile manuel olarak serbest bırakmanız gerektiği anlamına gelir. Uygulama yalnızca 16 yerel referans için alan ayırmak demektir. Bu nedenle, bundan daha fazlasına ihtiyacınız varsa ilerledikçe 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. (Bunlar, ileti 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 bir yerel 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ü içinde yerel referanslar oluşturan yerel kodların muhtemelen manuel olarak silme işlemi 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 global referansa sahip bir çözüm muhtemelen daha iyidir.

UTF-8 ve UTF-16 dizeleri

Java programlama dili UTF-16 kullanır. JNI, kolaylık sağlamak amacıyla 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 kullanışlıdır. Bunun en güzel tarafı, standart libc dizesi işlevleriyle kullanıma uygun C stili sıfır sonlandırılmış dizelere sahip olmanızdır. Kötü tarafı ise rastgele UTF-8 verilerini JNI'ye aktaramayacak ve düzgün çalışmasını bekleyemeyeceğinizdir.

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

Get işlemini gerçekleştirdiğiniz dizeleri Release işlemini yapmayı unutmayın. Dize işlevleri, jchar* veya jbyte* değerini döndürür. Bunlar, yerel başvurular yerine temel verilere yönlendiren C stili işaretçilerdir. Bunlar, Release çağrılana kadar geçerli olur. Diğer bir deyişle, yerel yöntem geri döndüğünde bu politikalar yayınlanmaz.

NewStringUTF'e iletilen veriler, Değiştirilmiş UTF-8 biçiminde olmalıdır. Yaygın bir hata, bir dosya veya ağ akışındaki karakter verilerini okumak ve bunları filtrelemeden NewStringUTF adresine vermektir. Verilerin geçerli bir MUTF-8 (veya uyumlu bir alt küme olan 7 bit ASCII) olduğundan emin değilseniz geçersiz karakterleri çıkarmanız veya uygun bir 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ği için Android 8'den önce UTF-16 dizeleriyle çalışmak genellikle daha hızlıydı. Öte yandan GetStringUTFChars, bir 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ştirerek (bellek tasarrufu amacıyla) hareketli bir çö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 çoğu dize kısaysa yığın ayrılmış arabellek ve GetStringRegion veya GetStringUTFRegion kullanılarak çoğu durumda ayırma ve dağıtımın önüne geçmek 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şle erişilmesi 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 bellek ayırarak 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 olması garanti edilir (veriler kopyalanmazsa dizi nesnesinin sabitleneceği ve yığının sıkıştırılması kapsamında yeniden konumlandırılamayacağı anlamına gelir). Get değerindeki her dizi için Release işlemi 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 birini içerebilen 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 bunun bir kopyasını mı döndürdüğüne bağlıdır:

  • 0
    • Gerçek: Dizi nesnesinin sabitlemesi kaldırıldı.
    • Kopyalama: Veriler tekrar kopyalanır. Kopyayı içeren arabellek serbest bırakılır.
  • JNI_COMMIT
    • Gerçek: hiçbir şey yapmaz.
    • Kopyalama: Veriler tekrar 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 JNI_COMMIT ile Release çağrısı yapmanız gerekip gerekmediğini öğrenmektir. Değişiklik yapmak ve dizinin içeriğini kullanan kodu yürütmek arasında geçiş yapıyorsanız işlemsiz kaydı atlayabilirsiniz. İşareti kontrol etmenin bir diğer olası nedeni de JNI_ABORT öğesinin verimli bir şekilde ele alınmasıdır. Örneğin, bir dizi almak, diziyi yerinde değiştirmek, parçaları diğer işlevlere geçirmek 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 aslını iletiyorsa kendi kopyanızı oluşturmanız gerekir.

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

Ayrıca, JNI_COMMIT işaretinin diziyi serbest bırakmadığını ve son olarak farklı bir işaretle Release öğesini yeniden çağırmanız gerekeceğini 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ılar çok kullanışlı olabilecek bir alternatiftir. 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, ilk len baytlık öğelerini kopyalar ve daha sonra, diziyi serbest bırakır. Uygulamaya bağlı olarak Get çağrısı, dizi içeriğini sabitler veya kopyalar. Kod, verileri kopyalar (örneğin ikinci bir kez) ve ardından Release çağırır. Bu durumda JNI_ABORT, üçüncü bir kopyalama yapma şansı olmamasını sağlar.

Kullanıcı aynı şeyi daha basit bir şekilde başarabilir:

    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. 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 karakterleri kopyalamak için GetStringRegion veya GetStringUTFRegion çağrısını kullanabilirsiniz.

İstisnalar

Bir istisna beklemedeyken çoğu JNI işlevini çağırmamalısınız. Kodunuzun istisnayı fark edip (işlevin döndürdüğü değer, ExceptionCheck veya ExceptionOccurred aracılığıyla) ve geri 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 yaratabilir ancak genellikle hata kontrolü için daha basit bir yol sunar. Örneğin, NewString NULL olmayan bir değer döndürürse istisnai kontrol etmeniz gerekmez. Bununla birlikte, bir yöntemi çağırırsanız (CallObjectMethod gibi bir işlev kullanarak) her zaman istisna olup olmadığını kontrol etmeniz gerekir. Çünkü istisna atılırsa döndürülen değer geçerli olmaz.

Yönetilen kod tarafından oluşturulan istisnaların yerel yığın çerçevelerini gevşetmediğini 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ı, geçerli 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ı "yakalayabilir" ve bunu ExceptionClear ile temizleyebilir. Her zaman olduğu gibi, istisnaları işleme almadan silmek sorunlara yol açabilir.

Throwable nesnesinin kendisini değiştirmek için kullanabileceğiniz yerleşik işlevler yoktur. Bu nedenle, istisna dizesini almak istiyorsanız (örneğin) 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 elde etmek için GetStringUTFChars kullanın.

Uzatılmış 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 tablo işaretçileri, standart uygulamayı çağırmadan önce genişletilmiş bir dizi denetim gerçekleştiren işlev tablolarına dönüştürülür.

Ek kontroller şunlardır:

  • Diziler: Negatif boyutlu bir dizi ayırmaya çalışıyor.
  • Hatalı işaretçiler: Bir JNI çağrısına bozuk bir jarray/jclass/jobject/jstring iletme veya null olmayan bir bağımsız değişkenle bir 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şimle buna karşılık gelen sürüm arasında bir JNI çağrısı yapılması.
  • Direct ByteBuffers: NewDirectByteBuffer işlevine hatalı bağımsız değişkenler iletilme.
  • İstisnalar: Bekleyen bir istisna olduğunda 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 alan için jfieldID kullanma (veya tam tersi) ya da bir sınıftan jfieldID'i başka bir sınıfın örnekleriyle kullanma.
  • jmethodIDs: Bir Call*Method JNI çağrısı yaparken yanlış jmethodID türünün kullanılması: Yanlış döndürme 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 yanlış referans türünde kullanılıyor.
  • Sürüm modları: Sürüm ç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 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 Değiştirilmiş UTF-8 bayt dizisi 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 sırasını 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 şuna 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 etkilenmeyecektir. Ancak, o noktadan itibaren başlatılan tüm uygulamalarda CheckJNI etkinleştirilecektir. (Özelliği başka bir değerle değiştirin veya yalnızca yeniden başlatmak, CheckJNI'yi tekrar devre dışı bırakır.) Bu durumda, bir uygulama tekrar 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 CheckJNI'yi yalnızca uygulamanız için etkinleştirecek şekilde android:debuggable özelliğini 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

System.loadLibrary standardını kullanarak, 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.

Statik sınıf ilkleştiriciden System.loadLibrary (veya ReLinker.loadLibrary) çağrısı yapın. Bağımsız değişken, "decoted" kitaplığının adıdır. Bu nedenle, libfubar.so öğesini yüklemek için "fubar" içinde iletebilirsiniz.

Yerel yöntemler içeren yalnızca bir sınıfınız varsa System.loadLibrary çağrısının ilgili 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 dlsym ile dinamik olarak aramasına izin verebilirsiniz. RegisterNatives özelliğinin 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 olabilmenizdir. Çalışma zamanının işlevlerinizi keşfetmesine izin vermenin avantajı, kod yazmanın 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 hesabınızda, 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 izlemeleri 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++ dilinde 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 spesifikasyonlarına bakın). Yani, bir yöntem imzası yanlışsa yöntem 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ükleyicinin 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ükleyiciyi kullanır. Böyle bir yükleme yoksa (çağrı, az önce eklenmiş bir yerel iş parçacığından geldiği için) "system" (sistem) 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, FindClass ile kendi sınıflarınızı bu bağlamda 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ı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

Yönetilen ve yerel kodlar arasındaki geçişleri hızlandırmak için yerel yöntemlere @FastNative veya @CriticalNative (ikisi birden değil) ile ek açıklama eklenebilir. Ancak bu ek açıklamalar, kullanılmadan önce dikkatli bir şekilde ele alınması gereken bazı davranış değişikliklerine neden olur. Aşağıda bu değişikliklerden kısaca bahsedecek olsak da 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önüş değerlerinde ya da örtülü this olarak) uygulanabilir ve bu ek açıklama JNI geçişi ABI'sını değiştirir. Yerel uygulama, JNIEnv ve jclass parametrelerini işlev imzasından hariç tutmalıdır.

@FastNative veya @CriticalNative yöntemi yürütülürken çöp toplama işlemi, iş parçacığını temel işler için askıya alamaz ve engellenebilir. Bu ek açıklamaları genellikle hızlı ancak genellikle sınırsız olan 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 testinden geçmiş herkese açık API haline gelmiştir. Bu optimizasyonlar büyük olasılıkla Android 8-13 cihazlarda da çalışır (güçlü CTS garantileri 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ümlerinde ç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 kaldırılmasına ve kilitlenmelere yol açabilir.

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ı elde etmek için @FastNative veya @CriticalNative yöntemlerini arayanları temel profile eklemeniz önerilir. Android 12'den beri, derlenmiş yönetilen bir yöntemden @CriticalNative yerel yöntemine yapılan çağrı, tüm bağımsız değişkenler kayıtlara sığdığı sürece (örneğin, kol64'te en fazla 8 integral ve en fazla 8 kayan nokta bağımsız değişkeni) C/C++'ta satır içi olmayan bir çağrı kadar ucuzdur.

Bazen yerel bir yöntemi ikiye bölmek tercih edilebilir. Bu iki yöntem, başarısız olabilen çok hızlı bir yöntem ve yavaş durumları ele alan bir başka yöntemdir. Ö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ıya işaretçi depolarken 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 yüzden ikili sınıf verilerinin aktarılması işe yaramaz.

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 adı aramaları sırasında "$" karakteri düzgün şekilde "_00024" biçimine dönüştürülmemişti. Bu sorunu çözmek için açık kayıt kullanmanız veya yerel yöntemleri iç sınıfların dışına taşımanız 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ılamıyordu. (Çalışma zamanı ayrıca bir pthread anahtar yıkıcı işlevi de kullandığından hangisinin önce çağrıldığını görmek için bir yarış olur.)

  • Zayıf küresel referanslar

    Zayıf küresel referanslar Android 2.2'ye (Froyo) kadar uygulanmadı. Eski sürümler, bunları kullanma girişimlerini kesinlikle reddeder. Desteği test etmek için Android platform sürümü sabit değerlerini 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ı onlarla bir şey yapmadan önce zayıf genel dünyaya açık referanslar yapmaya teşvik eder. Bu nedenle, bu açıklama 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 tüm JNI referansları gibi kullanılabilir.

  • Yerel referanslar

    Android 4.0'a (Ice Cream Sandwich) kadar yerel referanslar aslında doğrudan işaretçilerdi. Ice Cream Sandwich, daha iyi çöp toplayıcıları desteklemek için gereken yönergeyi ekledi. Ancak bu, eski sürümlerde çok sayıda JNI hatasının algılanamadığı 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 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 doğru şekilde uygulanması mümkün değildi. Bunun yerine, zayıf geneller tablosunu, bağımsız değişkenleri, yerel tabloyu ve geneller tablosunu bu sırayla inceleyen sezgisel bir 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. Yani, örneğin statik yerel yönteminize örtülü bağımsız değişken olarak iletilen jclass ile aynı olan global bir jclass'da GetObjectRefType çağrısı yaptıysanız JNIGlobalRefType yerine JNILocalRefType 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şkenin kaldırılmasına ve büyük olasılıkla 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çerir. Bu optimizasyonların JNI RegisterNatives ile açık bir şekilde kaydedilmeden kullanılması, Android 8-11'de kilitlenmelere yol açabilir.

  • FindClass, ClassNotFoundException fırlatıyor

    Geriye dönük uyumluluk için Android, FindClass tarafından bir sınıf bulunamadığında NoClassDefFoundError yerine ClassNotFoundException hatasını verir. Bu davranış, Java yansıma API'si Class.forName(name) ile tutarlıdır.

SSS: Neden UnsatisfiedLinkError alıyorum?

Yerel kod üzerinde çalışırken, aşağıdaki gibi bir hata ile karşılaşılması çok normaldir:

java.lang.UnsatisfiedLinkError: Library foo not found

Bazı durumlarda bu ifade şu anlama gelir: Kütüphane 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. Kitaplığın 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ığa neden olabilir.

Başka bir UnsatisfiedLinkError başarısız 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. 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.
  • Bir ad veya imza uyuşmazlığı nedeniyle yöntem bulunamadı. Bu durumun nedeni genellikle:
    • Geç yöntem araması için extern "C" ve uygun görünürlük (JNIEXPORT) ile C++ işlevleri bildirilememiştir. Ice Cream Sandwich'ten önceki JNIEXPORT makrosunun yanlış olduğunu, bu nedenle eski bir jni.h ile yeni bir GCC'nin kullanılamayacağını unutmayın. Simgeleri kitaplıkta göründükleri şekilde görmek için arm-eabi-nm kullanabilirsiniz. Karışık görünüyorlar (Java_Foo_myfunc yerine _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass gibi) veya simge türü büyük harf "T" yerine küçük harf "t" ise beyanı ayarlamanız gerekir.
    • Açık kayıt için yöntem imzası girilirken küçük hatalar verilir. 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 "/" ve iç sınıf adlarını ayırmak için "$" kullanılır (ör. Ljava/util/Map$Entry;).

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

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

(Bu önerilerin çoğu, GetMethodID veya GetStaticMethodID ile yöntemler ya da GetFieldID veya GetStaticFieldID bulunan alanlar bulunamama konusunda eşit derecede geçerlidir.)

Sınıf adı dizesinin doğru biçimde 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 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ü, yalnızca JNI'den kullanılan sınıf, yöntem veya alanları kaldırabileceğinden, doğru saklama kurallarının 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 aramasını başlatmak istiyor. Bu çağrı, 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) artık sorun yaşayabilirsiniz. Artık uygulamanızdan hiçbir yığın çerçevesi yoktur. Bu iş parçacığından FindClass çağrısı yaparsanız JavaVM, uygulamanızla ilişkilendirilmiş olan 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 çözmek için birkaç yöntem vardır:

  • 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şkilendirilen sınıf yükleyicisini kullanır (bu, kitaplığın ilk kullanıma hazırlanmasını kolaylaştırmak için sağlanan özel bir kuraldır). Uygulama kodunuz kitaplığı yüklüyorsa FindClass doğru sınıf yükleyiciyi kullanır.
  • Class bağımsız değişkenini alacak yerel yönteminizi tanımlayıp ardından Foo.class öğesini ileterek sınıfın bir örneğini buna ihtiyaç duyan işlevlere geçirin.
  • ClassLoader nesnesinin referansını 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şırım?

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 yer alır. İki temel yaklaşım vardır.

Verileri byte[] içinde 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ılarında ise yerel yığına bir arabellek ayırıp verileri kopyalar.

Bunun alternatifi, verilerin doğrudan 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 seçeneğin 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 sonunda 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 yapılması mantıklı olmayabilir.)

Net bir kazanan yoksa, doğrudan baytlık tamponu kullanın. Bunlara yönelik destek doğrudan JNI'de yerleşiktir ve gelecekteki sürümlerde performansın artması beklenir.