JNI ipuçları

JNI, Java Native Interface'in kısaltmasıdır. Android'in yönetilen koddan (Java veya Kotlin programlama dillerinde yazılmış) derlediği bayt kodunun yerel kodla (C/C++'da yazılmış) etkileşime geçme şeklini tanımlar. JNI, satıcıdan bağımsızdır, dinamik paylaşılan kitaplıklardan kod yüklemeyi destekler ve zaman zaman hantal olsa da makul ölçüde verimlidir.

Not: Android, Kotlin'i Java programlama diline benzer şekilde ART dostu bayt koduna derlediği için bu sayfadaki yönergeleri JNI mimarisi ve ilişkili maliyetler açısından hem Kotlin hem de Java programlama dillerine uygulayabilirsiniz. Daha fazla bilgi edinmek için Kotlin ve Android başlıklı makaleyi inceleyin.

Henüz bilmiyorsanız JNI'nin nasıl çalıştığı ve hangi özelliklerin kullanılabildiği hakkında bilgi edinmek için Java Native Interface Specification'ı (Java Yerel Arayüz Spesifikasyonu) okuyun. Arayüzün bazı yönleri ilk okumada hemen anlaşılmayabilir. Bu nedenle, sonraki birkaç bölümü faydalı 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ümlerinde Memory Profiler'daki 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. Bu konuda dikkate alınması gereken birkaç boyut vardır. JNI çözümünüz aşağıdaki yönergeleri (en önemliden başlayarak önem sırasına göre listelenmiştir) uygulamaya çalışmalıdır:

  • JNI katmanında kaynakların sıralanmasını en aza indirin. JNI katmanında sıralama, önemsiz olmayan maliyetlere neden olur. Marshall etmeniz gereken veri miktarını ve verileri marshall etmeniz gereken sıklığı 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 sayede JNI arayüzünüzün bakımı daha kolay olur. Genellikle, asenkron güncellemeyi kullanıcı arayüzüyle aynı dilde tutarak asenkron kullanıcı arayüzü güncellemelerini basitleştirebilirsiniz. Örneğin, Java kodundaki kullanıcı arayüzü iş parçacığından JNI aracılığıyla bir C++ işlevini çağırmak yerine, Java programlama dilinde iki iş parçacığı arasında geri çağırma yapmak daha iyidir. Bu iş parçacıklarından biri, engelleme 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'nin dokunması veya JNI'ye 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 tek tek çalışan iş parçacıkları arasında değil, havuz sahipleri arasında tutmaya çalışın.
  • Gelecekteki yeniden düzenlemeleri kolaylaştırmak için arayüz kodunuzu kolayca tanımlanabilen az sayıda C++ ve Java kaynak konumunda tutun. Gerekirse JNI otomatik oluşturma kitaplığı kullanabilirsiniz.

JavaVM ve JNIEnv

JNI, "JavaVM" ve "JNIEnv" olmak üzere iki temel veri yapısı tanımlar. Bunların her ikisi de aslında işlev tablolarına yönelik işaretçilere yönelik işaretçilerdir. (C++ sürümünde, bunlar bir işlev tablosuna yönelik işaretçi ve tablo üzerinden dolaylı olarak geçen her JNI işlevi için bir üye işlevi içeren sınıflardır.) JavaVM, "invocation interface" işlevlerini sağlar. Bu işlevler, JavaVM oluşturmanıza ve yok etmenize olanak tanır. Teoride, işlem başına birden fazla JavaVM'niz olabilir ancak Android yalnızca bir tanesine izin verir.

JNIEnv, JNI işlevlerinin çoğunu sağlar. Yerel işlevlerinizin tümü, @CriticalNative yöntemleri hariç olmak üzere ilk bağımsız değişken olarak JNIEnv alır. Daha hızlı yerel çağrılar bölümüne bakın.

JNIEnv, iş parçacığına özel depolama için kullanılır. Bu nedenle, iş parçacıkları arasında JNIEnv paylaşamazsınız. Bir kod parçasının JNIEnv'sini almanın başka bir yolu yoksa JavaVM'yi paylaşmanız ve iş parçacığının JNIEnv'sini bulmak için GetEnv kullanmanız gerekir. (Varsa; AttachCurrentThread bölümüne bakın.)

JNIEnv ve JavaVM'nin C beyanları, C++ beyanlarından farklıdır. "jni.h" include dosyası, C veya C++'a dahil edilip edilmediğine bağlı olarak farklı typedef'ler sağlar. Bu nedenle, her iki dil tarafından dahil edilen başlık dosyalarına JNIEnv bağımsız değişkenleri eklemek iyi bir fikir değildir. (Başka bir deyişle: Başlık dosyanız #ifdef __cplusplus gerektiriyorsa bu başlıktaki herhangi bir şey JNIEnv'ye atıfta bulunuyorsa biraz daha fazla çalışmanız gerekebilir.)

Mesaj dizileri

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

Java kodunu çağırması gereken tüm iş parçacıklarını oluşturmak için genellikle Thread.start() kullanmak en iyisidir. Bu sayede yeterli yığın alanına sahip olursunuz, doğru ThreadGroup içinde olursunuz ve Java kodunuzla aynı ClassLoader kullanırsınız. Ayrıca, Java'da hata ayıklama için iş parçacığının adını yerel koddan daha kolay ayarlayabilirsiniz (pthread_t veya thread_t varsa pthread_setname_np(), std::thread varsa ve pthread_t istiyorsanız std::thread::native_handle() bölümüne bakın).

Yerel olarak oluşturulmuş bir iş parçacığını eklemek, java.lang.Thread nesnesinin oluşturulup "main" ThreadGroup'e eklenmesine neden olur ve bu nesne hata ayıklayıcıda görünür hale gelir. Calling AttachCurrentThread() on an already-attached thread is a no-op.

Android, yerel kod yürüten iş parçacıklarını askıya almaz. Çöp 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ı yaptığında iş parçacığını duraklatır.

JNI aracılığıyla eklenen iş parçacıkları çıkmadan önce DetachCurrentThread() işlevini çağırmalıdır. Bunu doğrudan kodlamak zor geliyorsa 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 için pthread_key_create() kullanabilir ve oradan DetachCurrentThread() çağrısı yapabilirsiniz. (JNIEnv'yi iş parçacığına özel depolama alanında saklamak için bu anahtarı pthread_setspecific() ile kullanın. Böylece, yıkıcınıza bağımsız değişken olarak iletilir.)

jclass, jmethodID ve jfieldID

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

  • FindClass ile sınıf için 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 öğeyle alın.

Benzer şekilde, bir yöntemi çağırmak için önce bir sınıf nesnesi referansı, ardından bir yöntem kimliği alırsınız. Kimlikler genellikle yalnızca dahili çalışma zamanı veri yapılarına yönelik işaretçilerdir. Bunları aramak için birkaç dize karşılaştırması gerekebilir ancak bunları aldıktan sonra alanı almak veya yöntemi çağırmak için yapılan gerçek arama çok hızlıdır.

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

Sınıf referansları, alan kimlikleri ve yöntem kimliklerinin geçerliliği, sınıf kaldırılana kadar garanti edilir. Sınıflar yalnızca bir ClassLoader ile ilişkili tüm sınıflar çöp toplama işlemine tabi tutulabiliyorsa kaldırılır. Bu durum nadir olsa da Android'de imkansız değildir. Ancak jclass öğesinin bir sınıf referansı olduğunu ve NewGlobalRef çağrısıyla korunması gerektiğini unutmayın (bir 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üklendiğinde kimlikleri otomatik olarak yeniden önbelleğe almak istiyorsanız kimlikleri doğru şekilde başlatmak için uygun sınıfa aşağıdaki gibi bir kod parçası eklemeniz gerekir:

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. Kod, sınıf başlatıldığında bir kez yürütülür. Sınıf kaldırılıp yeniden yüklendiğinde tekrar yürütülür.

Yerel ve küresel referanslar

Yerel bir yönteme iletilen her bağımsız değişken ve JNI işlevi tarafından döndürülen neredeyse her nesne bir "yerel referanstır". Bu, geçerli iş parçacığındaki mevcut yerel yöntem süresince geçerli olduğu anlamına gelir. Nesnenin kendisi yerel yöntem döndükten sonra yaşamaya devam etse bile referans geçerli değildir.

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

Yerel olmayan referanslar yalnızca NewGlobalRef ve NewWeakGlobalRef işlevleri aracılığıyla alınabilir.

Bir referansı daha uzun süre saklamak istiyorsanız "global" bir 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. Global referansın geçerliliği, DeleteGlobalRef tarihine kadar aranmanız garanti edilir.

Bu desen, FindClass işlevinden döndürülen bir jclass'ı önbelleğe alı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, bağımsız değişken olarak hem yerel hem de genel referansları kabul eder. Aynı nesneye yapılan referansların farklı değerlere sahip olması mümkündür. Örneğin, aynı nesnede NewGlobalRef için yapılan ardışık çağrılardan döndürülen değerler farklı olabilir. İki referansın aynı nesneyi ifade edip etmediğini görmek için IsSameObject işlevini kullanmanız gerekir. Referansları yerel kodda == ile asla 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ı derecede tahsis etmemesi" gerekir. Pratikte bu, çok sayıda yerel referans oluşturuyorsanız (ör. bir dizi nesne üzerinde çalışırken) bunları JNI'nin sizin için 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 yer ayırmak üzere gereklidir. Bu nedenle, daha fazlasına ihtiyacınız varsa ilerledikçe silmeniz veya daha fazla yer ayırmak için EnsureLocalCapacity/PushLocalFrame kullanmanız gerekir.

jfieldID ve jmethodID türlerinin nesne referansları değil opak türler olduğunu ve NewGlobalRef'ye iletilmemesi gerektiğini unutmayın. GetStringUTFChars ve GetByteArrayElements gibi işlevler tarafından döndürülen ham veri işaretçileri de nesne değildir. (İş parçacıkları arasında iletilebilir ve eşleşen Release çağrısı yapılana kadar geçerlidir.)

Bir olağandışı durumdan ayrıca bahsetmek 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 yerel referanslar manuel olarak silinmelidir. Genel olarak, bir döngüde yerel referanslar oluşturan tüm yerel kodların manuel olarak silinmesi 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ışlarına neden olabilir. Diğer tüm koşullar aynı 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'yı kullanır. JNI, kolaylık sağlamak için Değiştirilmiş UTF-8 ile de çalışan yöntemler sunar. Değiştirilmiş kodlama, \u0000 karakterini 0x00 yerine 0xc0 0x80 olarak kodladığı için C kodu için kullanışlıdır. Bu yöntemin avantajı, standart libc dize işlevleriyle kullanıma uygun, C tarzı sıfır sonlandırmalı dizelere sahip olabileceğinizden emin olmanızdır. Ancak, rastgele UTF-8 verilerini JNI'ye iletip doğru şekilde çalışmasını bekleyemezsiniz.

Bir String öğesinin UTF-16 gösterimini almak için GetStringChars kullanın. UTF-16 dizelerinin sıfırla sonlandırılmadığını ve \u0000'ın kullanılabildiğini unutmayın. Bu nedenle, dize uzunluğunun yanı sıra jchar işaretçisini de saklamanız gerekir.

Get Release dizeleri Get unutmayın. Dize işlevleri, yerel referanslar yerine temel verilere yönelik C tarzı işaretçiler olan jchar* veya jbyte* değerini döndürür. Bu işlevlerin Release çağrılana kadar geçerli olduğu garanti edilir. Bu nedenle, yerel yöntem döndüğünde serbest bırakılmazlar.

NewStringUTF'e iletilen veriler, Modified UTF-8 biçiminde olmalıdır. Sık yapılan bir hata, bir dosyadan veya ağ akışından karakter verilerini okuyup filtrelemeden NewStringUTF'ya aktarmaktır. Verilerin geçerli MUTF-8 (veya uyumlu bir alt küme olan 7 bit ASCII) olduğunu bilmiyorsanız geçersiz karakterleri kaldırmanız ya da bunları uygun Modified UTF-8 biçimine dönüştürmeniz gerekir. Aksi takdirde, UTF-16 dönüştürme işlemi beklenmedik sonuçlar verebilir. CheckJNI (emülatörlerde varsayılan olarak etkindir), dizeleri tarar ve geçersiz giriş alırsa sanal makineyi durdurur.

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

    constexpr size_t kStackBufferSize = 64u;
    jchar stack_buffer[kStackBufferSize];
    std::unique_ptr<jchar[]> 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 tek seferde bir giriş erişilirken, temel öğe dizileri doğrudan C'de tanımlanmış gibi okunup yazılabilir.

Arayüzü sanal makine uygulamasını kısıtlamadan 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ırıp bir kopya 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 olduğu garanti edilir (bu, veriler kopyalanmadıysa dizi nesnesinin sabitleneceği ve yığın sıkıştırılırken yeniden yerleştirilemeyeceği anlamına gelir). Get işlemi yaptığınız her diziyi Release etmeniz gerekir. Ayrıca, Get çağrısı başarısız olursa kodunuzun daha sonra Release bir NULL işaretçisini denemediğinden 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 özellik nadiren faydalıdır.

Release çağrısı, üç değerden birini alabilen bir mode bağımsız değişkeni alır. Çalışma zamanı tarafından gerçekleştirilen işlemler, gerçek verilere veya verilerin bir kopyasına yönelik bir işaretçi döndürüp döndürmediğine bağlıdır:

  • 0
    • Gerçek: Dizi nesnesi sabitlenmemiş.
    • Kopyalama: Veriler geri kopyalanır. Kopyanın bulunduğu arabellek serbest bırakılır.
  • JNI_COMMIT
    • Gerçek: Hiçbir şey yapmaz.
    • Kopyalama: Veriler geri kopyalanır. Kopyayı içeren arabellek serbest bırakılmaz.
  • JNI_ABORT
    • Gerçek: Dizi nesnesi sabitlenmemiş. Önceki yazma işlemleri iptal edilmez.
    • Kopyalama: Kopyanın bulunduğu arabellek serbest bırakılır ve arabellek üzerinde yapılan tüm değişiklikler kaybolur.

isCopy işaretini kontrol etmenin bir nedeni, bir dizide değişiklik yaptıktan sonra JNI_COMMIT ile Release işlevini ç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 no-op commit'i atlayabilirsiniz. İşaretin kontrol edilmesinin bir diğer olası nedeni de JNI_ABORT'nın verimli bir şekilde ele alınmasıdır. Örneğin, bir dizi almak, bunu yerinde değiştirmek, parçaları diğer işlevlere iletmek 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ışsa Release çağrısını atlayabileceğinizi düşünmek yaygın bir hatadır (örnek kodda tekrarlanır). Buna gerek yoktur. Kopya arabellek ayrılmamışsa orijinal bellek sabitlenmelidir ve çöp toplayıcı tarafından taşınamaz.

Ayrıca JNI_COMMIT işaretinin diziyi serbest bırakmadığını ve sonunda Release işlevini farklı bir işaretle tekrar çağırmanız gerekeceğini unutmayın.

Bölge aramaları

Get<Type>ArrayElements ve GetStringChars gibi aramaların alternatifi olarak, yalnızca verileri kopyalamak istediğinizde çok faydalı olabilecek bir yöntem 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şlev, diziyi alır, ilk len bayt öğesini kopyalar ve ardından diziyi serbest bırakır. Uygulamaya bağlı olarak Get çağrısı, dizi içeriklerini sabitler veya kopyalar. Kod, verileri (belki ikinci kez) kopyalar, ardından Release işlevini çağırır. Bu durumda JNI_ABORT, üçüncü bir kopya oluşturulma olasılığını ortadan kaldırır.

Aynı işlemi daha basit bir şekilde gerçekleştirebilirsiniz:

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

Bu durumun çeşitli avantajları vardır:

  • 2 yerine 1 JNI çağrısı gerektirir ve ek yükü azaltır.
  • Sabitleme veya ek veri kopyaları gerektirmez.
  • Programcı hatası riskini azaltır. Bir şey başarısız olduğunda Release işlevini çağırmayı unutma riski yoktur.

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

İstisnalar

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

Bir istisna beklemedeyken çağırmanıza izin verilen tek JNI işlevleri şunlardır:

  • 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 başarısızlık kontrolü için daha basit bir yol sağlar. Örneğin, NewString işlevi NULL olmayan bir değer döndürürse istisna olup olmadığını kontrol etmeniz gerekmez. Ancak bir yöntemi çağırırsanız (CallObjectMethod gibi bir işlev kullanarak) her zaman bir istisna olup olmadığını kontrol etmeniz gerekir. Çünkü istisna oluşturulursa dönüş değeri geçerli olmaz.

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

Yerel kod, ExceptionCheck veya ExceptionOccurred çağırarak bir istisnayı "yakalayabilir" ve ExceptionClear ile temizleyebilir. Her zamanki gibi, istisnaları ele almadan silmek sorunlara yol açabilir.

Throwable nesnesini doğrudan değiştirmek için yerleşik işlevler yoktur. Bu nedenle, örneğin istisna dizesini almak istiyorsanız Throwable sınıfını bulmanız, getMessage "()Ljava/lang/String;" için yöntem kimliğini aramanız, yöntemi çağırmanız ve sonuç NULL değilse GetStringUTFChars kullanarak printf(3)'a veya eşdeğerine iletebileceğiniz bir şey almanız gerekir.

Genişletilmiş kontrol

JNI, çok az hata kontrolü yapar. Hatalar genellikle kilitlenmeye neden olur. Android ayrıca, standart uygulama çağrılmadan önce genişletilmiş bir dizi kontrol gerçekleştiren işlev tablolarına JavaVM ve JNIEnv işlev tablosu işaretçilerinin değiştirildiği CheckJNI adlı bir mod da sunar.

Ek kontroller şunlardır:

  • Diziler: Negatif boyutlu bir dizi ayırmaya çalışılıyor.
  • Hatalı işaretçiler: Bir JNI çağrısına hatalı bir jarray/jclass/jobject/jstring iletme veya boş olmayan bir bağımsız değişkenle bir JNI çağrısına NULL işaretçi iletme.
  • Sınıf adları: "java/lang/String" sınıf adı stili dışında bir sınıf adı ile JNI çağrısı yapılması.
  • Kritik çağrılar: "Kritik" bir get ile ilgili release arasında JNI çağrısı yapma.
  • Direct ByteBuffers: NewDirectByteBuffer işlevine hatalı bağımsız değişkenler iletiliyor.
  • İstisnalar: Bekleyen bir istisna varken JNI çağrısı yapma.
  • JNIEnv*s: Yanlış iş parçacığından JNIEnv* kullanma.
  • jfieldIDs: NULL jfieldID kullanma, bir alanı yanlış türde bir değere ayarlamak için jfieldID kullanma (örneğin, bir StringBuilder'ı bir String alanına atamaya çalışma), bir örnek alanı ayarlamak için statik bir alan için jfieldID kullanma veya bunun tersini yapma ya da bir sınıftaki jfieldID'yi başka bir sınıfın örnekleriyle kullanma.
  • jmethodIDs: Call*Method JNI çağrısı yaparken yanlış türde jmethodID kullanma: yanlış dönüş türü, statik/statik olmayan eşleşme hatası, "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: Yanlış türde referansta DeleteGlobalRef/DeleteLocalRef kullanma.
  • Yayın modları: Yayın çağrısına kötü bir yayın modu iletme (0, JNI_ABORT veya JNI_COMMIT dışında bir şey).
  • Tür güvenliği: Yerel yönteminizden uyumsuz bir tür döndürme (örneğin, bir String döndürmek üzere tanımlanmış bir yöntemden StringBuilder döndürme).
  • UTF-8: Bir JNI çağrısına geçersiz Modified UTF-8 bayt dizisi iletiliyor.

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

CheckJNI'yi etkinleştirmenin çeşitli yolları vardır.

Emülatörü kullanıyorsanız CheckJNI varsayılan olarak etkindir.

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

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

Bu durumlardan birinde, çalışma zamanı başladığında logcat çıktı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

Bu durum, halihazırda çalışan uygulamaları etkilemez ancak bu noktadan itibaren başlatılan tüm uygulamalarda CheckJNI etkinleştirilir. (Mülkü başka bir değere değiştirme veya yalnızca yeniden başlatma, CheckJNI'yi tekrar devre dışı bırakır.) Bu durumda, bir sonraki uygulama başlatma işleminde logcat çıkışınızda şuna benzer bir şey görürsünüz:

D Late-enabling CheckJNI

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

Yerel kitaplık

Paylaşılan kitaplıklardan yerel kodu standart System.loadLibrary ile yükleyebilirsiniz.

Uygulamada, Android'in eski sürümlerinde PackageManager'da yerel kitaplıkların yüklenmesinin ve güncellenmesinin güvenilir olmaması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şlatıcısından System.loadLibrary (veya ReLinker.loadLibrary) çağrısı yapılıyor. Bağımsız değişken, "dekorasyonlu olmayan" kitaplık adıdır. Bu nedenle, libfubar.so kitaplığını yüklemek için "fubar" değerini iletmeniz gerekir.

Yalnızca yerel yöntemler içeren bir sınıfınız varsa System.loadLibrary çağrısının bu 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ğini bilmek için Application üzerinden arama 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ını sağlayabilirsiniz. RegisterNatives kullanmanın avantajları, sembollerin varlığıyla ilgili önceden kontrol yapılması ve JNI_OnLoad dışında hiçbir şey dışa aktarılmadığı için daha küçük ve hızlı paylaşılan kitaplıklar oluşturabilmenizdir. Çalışma zamanının işlevlerinizi keşfetmesine izin vermenin avantajı, biraz daha az kod yazmanızdır.

RegisterNatives uygulamasını kullanmak için:

  • JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) işlevi sağlama
  • JNI_OnLoad içinde, tüm doğal yöntemlerinizi RegisterNatives kullanarak kaydedin.
  • Sürüm komut dosyası (tercih edilen) ile derleyin veya -fvisibility=hidden kullanın. Böylece kitaplığınızdan yalnızca JNI_OnLoad dışa aktarılır. Bu, daha hızlı ve daha küçük kod üretir ve uygulamanıza yüklenen diğer kitaplıklarla olası çakışmaları önler (ancak uygulamanız yerel kodda çökerse daha az yararlı yığın izleri oluşturur).

Statik başlatıcı şu şekilde görünmelidir:

Kotlin

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

Java

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

JNI_OnLoad işlevi, C++'da yazıldığında 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şfedilmesini" kullanmak için bu yöntemleri belirli bir şekilde adlandırmanız gerekir (ayrıntılar için JNI spesifikasyonuna bakın). Bu nedenle, bir yöntem imzası yanlışsa yöntem ilk kez gerçekten çağrılana kadar bu durumdan haberdar olmazsınız.

FindClass, JNI_OnLoad üzerinden yapılan tüm aramalar, paylaşılan kitaplığı yüklemek için kullanılan sınıf yükleyicinin bağlamındaki sınıfları çözer. Diğer bağlamlardan çağrıldığında FindClass, Java yığınının en üstündeki yöntemle ilişkili sınıf yükleyiciyi kullanır. Bu yükleyici yoksa (çağrı, yeni eklenmiş bir yerel iş parçacığından yapıldığı için) "sistem" sınıfı yükleyiciyi kullanır. Sistem sınıfı yükleyicisi, uygulamanızın sınıfları hakkında bilgi sahibi olmadığından bu bağlamda FindClass ile kendi sınıflarınızı arayamazsınız. Bu, JNI_OnLoad'yı sınıfları aramak ve önbelleğe almak için uygun bir yer haline getirir: Geçerli bir jclass global referansınız olduğunda, bunu ekli herhangi bir iş parçacığından kullanabilirsiniz.

@FastNative ve @CriticalNative ile daha hızlı yerel aramalar

Yönetilen ve yerel kod arasındaki geçişleri hızlandırmak için yerel yöntemlere @FastNative veya @CriticalNative (ancak ikisi birden değil) ek açıklaması eklenebilir. Ancak bu ek açıklamalar, kullanılmadan önce dikkatlice değerlendirilmesi gereken belirli davranış değişiklikleriyle birlikte gelir. Bu değişikliklerden aşağıda kısaca bahsedilse de ayrıntılar için lütfen dokümanlara bakın.

@CriticalNative notu yalnızca yönetilen nesneleri (parametrelerde, dönüş değerlerinde veya örtülü this olarak) kullanmayan yerel yöntemlere uygulanabilir ve bu not, JNI geçiş ABI'sini değiştirir. Doğal uygulama, işlev imzasında JNIEnv ve jclass parametrelerini hariç tutmalıdır.

@FastNative veya @CriticalNative yöntemi yürütülürken çöp toplama işlemi, iş parçacığını temel iş için askıya alamaz ve engellenebilir. Genellikle hızlı ancak genel olarak sınırsız olan yöntemler de dahil olmak üzere uzun süren yöntemler için bu ek açıklamaları kullanmayın. Özellikle kod, önemli G/Ç işlemleri gerçekleştirmemeli veya uzun süre tutulabilecek yerel kilitler edinmemelidir.

Bu ek açıklamalar, Android 8'den beri sistem kullanımı için uygulanmış ve Android 14'te CTS testi yapılmış herkese açık API haline gelmiştir. Bu optimizasyonlar, Android 8-13 cihazlarda da (CTS garantisi olmadan) çalışabilir ancak yerel yöntemlerin dinamik olarak aranması yalnızca Android 12 ve sonraki sürümlerde desteklenir. Android 8-11 sürümlerinde çalıştırmak için JNI ile açıkça kayıt RegisterNatives kesinlikle gereklidir. Bu ek açıklamalar Android 7 ve önceki sürümlerde yok sayılır. @CriticalNative için ABI uyuşmazlığı, yanlış bağımsız değişken sıralamasına ve muhtemelen kilitlenmelere neden olur.

Bu ek açıklamaların gerekli olduğu performansı kritik yöntemler için, yerel yöntemlerin ada dayalı "keşfi" yerine yöntemleri JNI RegisterNatives ile açıkça kaydetmeniz önemle tavsiye edilir. Uygulama başlatma performansının en iyi şekilde olması için @FastNative veya @CriticalNative yöntemlerinin arayanlarını temel profilde bulundurmanız önerilir. Android 12'den itibaren, derlenmiş bir yönetilen 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, arm64'te 8'e kadar tam sayı ve 8'e kadar kayan nokta bağımsız değişkeni) C/C++'daki satır içi olmayan bir çağrı kadar ucuzdur.

Bazen yerel bir yöntemi ikiye bölmek tercih edilebilir. Bunlardan biri çok hızlı olup başarısız olabilir, diğeri ise yavaş durumları işler. Ö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çi depolarken int yerine long alanı kullanın.

Desteklenmeyen özellikler/geriye dönük uyumluluk

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

  • DefineClass uygulanmadı. Android, Java bayt kodlarını veya sınıf dosyalarını kullanmadığından ikili sınıf verilerinin iletilmesi işe yaramaz.

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

  • Yerel işlevlerin dinamik olarak aranması

    Android 2.0'a (Eclair) kadar, yöntem adları aranırken "$" karakteri "_00024" olarak düzgün şekilde dönüştürülmüyordu. Bu sorunu çözmek için açık kayıt kullanmanız veya yerel yöntemleri iç sınıflardan çıkarmanız gerekir.

  • İleti dizilerini ayırma

    Android 2.0 (Eclair) sürümüne kadar, "thread must be detached before exit" (çıkıştan önce iş parçacığı ayrılmalıdır) kontrolünü önlemek için pthread_key_create yok edici işlevi kullanılamıyordu. (Çalışma zamanı, bir pthread anahtarı yıkıcı işlevi de kullandığından hangisinin önce çağrılacağını görmek için bir yarışma olur.)

  • Zayıf küresel referanslar

    Android 2.2 (Froyo) sürümüne kadar zayıf genel referanslar uygulanmıyordu. Eski sürümler, kullanılma girişimlerini kesin bir şekilde reddeder. Desteği test etmek için Android platform sürümü sabitlerini kullanabilirsiniz.

    Android 4.0'a (Ice Cream Sandwich) kadar, zayıf genel referanslar yalnızca NewLocalRef, NewGlobalRef ve DeleteWeakGlobalRef'ye aktarılabilirdi. (Şartname, programcıları zayıf genel değişkenlerle herhangi bir işlem yapmadan önce bunlara sabit referanslar oluşturmaya şiddetle teşvik eder. Bu nedenle, bu durum hiçbir şekilde sınırlayıcı olmamalıdır.)

    Android 4.0 (Ice Cream Sandwich) sürümünden itibaren zayıf genel referanslar, diğer 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 gerekli olan dolaylılığı ekledi ancak bu, eski sürümlerde birçok JNI hatasının tespit edilemediği anlamına geliyor. Daha fazla bilgi için ICS'de JNI Yerel Referans Değişiklikleri başlıklı makaleyi inceleyin.

    Android 8.0'dan önceki Android sürümlerinde yerel referans sayısı, sürüme özel bir sınırla belirlenir. Android 8.0'dan itibaren Android, sınırsız yerel referansı destekler.

  • GetObjectRefType ile referans türünü belirleme

    Android 4.0'a (Ice Cream Sandwich) kadar, doğrudan işaretçilerin (yukarıya bakın) kullanılması nedeniyle GetObjectRefType'nın doğru şekilde uygulanması mümkün değildi. Bunun yerine, zayıf genel değişkenler tablosunu, bağımsız değişkenleri, yerel değişkenler tablosunu ve genel değişkenler tablosunu bu sırayla inceleyen bir sezgisel yöntem kullandık. İlk kez doğrudan işaretçinizi bulduğunda, referansınızın incelediği türde olduğunu bildiriyordu. Bu, örneğin, statik yerel yönteminize örtülü bağımsız değişken olarak geçirilen jclass ile aynı olan global bir jclass üzerinde GetObjectRefType çağrısı yaparsanız JNIGlobalRefType yerine JNILocalRefType alacağınız anlamına geliyordu.

  • @FastNative ve @CriticalNative

    Android 7'ye kadar bu optimizasyon notları yoksayılıyordu. @CriticalNative için ABI uyuşmazlığı, yanlış bağımsız değişken sıralamasına ve muhtemelen çökmelere yol açar.

    @FastNative ve @CriticalNative yöntemleri için yerel işlevlerin dinamik olarak aranması Android 8-10'da uygulanmamıştı ve Android 11'de bilinen hatalar içeriyor. Bu optimizasyonları JNI'ye açıkça kaydetmeden kullanmak RegisterNatives, Android 8-11'de kilitlenmelere neden olabilir.

  • FindClass atış ClassNotFoundException

    Android, geriye dönük uyumluluk için bir sınıf FindClass tarafından bulunamadığında NoClassDefFoundError yerine ClassNotFoundException istisnası oluşturur. Bu davranış, Java Reflection API ile tutarlıdır Class.forName(name).

SSS: Neden UnsatisfiedLinkError alıyorum?

Yerel kod üzerinde çalışırken şu gibi bir hatayla karşılaşmak yaygındır:

java.lang.UnsatisfiedLinkError: Library foo not found

Bazı durumlarda bu mesaj, kitaplığın bulunamadığı anlamına gelir. Diğer durumlarda ise kitaplık vardır ancak dlopen(3) tarafından açılamamıştır ve 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 uygulamaya erişilemiyor. Kitaplığın varlığını ve izinlerini kontrol etmek için adb shell ls -l <path> kullanın.
  • Kitaplık NDK ile oluşturulmamıştır. Bu durum, cihazda bulunmayan işlevlere veya kitaplıklara bağımlılığa neden olabilir.

Başka bir UnsatisfiedLinkError hata sınıfı şu şekilde 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 eşleşen bir yöntem bulmaya çalıştığı ancak başarılı olamadığı 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 yaygın nedenleri şunlardır:
    • Tembel yöntem aramasında, C++ işlevlerini extern "C" ve uygun görünürlükle (JNIEXPORT) bildirmemek. Ice Cream Sandwich'ten önce JNIEXPORT makrosunun yanlış olduğunu, bu nedenle eski bir jni.h ile yeni bir GCC kullanmanın işe yaramayacağını unutmayın. Sembolleri kitaplıkta göründükleri şekilde görmek için arm-eabi-nm kullanabilirsiniz. Semboller bozuk görünüyorsa (Java_Foo_myfunc yerine _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass gibi) veya sembol türü büyük "T" yerine küçük "t" ise bildirimi ayarlamanız gerekir.
    • Açık kayıt için yöntem imzası girilirken küçük hatalar. Kayıt çağrısına ilettiğiniz değerin, günlük dosyasındaki imzayla eşleştiğinden emin olun. "B"nin byte, "Z"nin ise 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 "/" kullanılır 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 kullanmak bazı sorunları önlemeye yardımcı olabilir.

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

(Bu tavsiyelerin çoğu, GetMethodID veya GetStaticMethodID ile yöntemleri ya da GetFieldID veya GetStaticFieldID ile alanları bulamama durumları için de 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 eğik çizgilerle ayrılır (ör. java/lang/String). Bir dizi sınıfı arıyorsanız uygun sayıda köşeli parantez ile başlamanız ve sınıfı "L" ve ";" ile sarmalamanız gerekir. Örneğin, String öğelerinden oluşan tek boyutlu bir dizi [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ı öğrenmenin iyi bir yoludur.

Kod küçültmeyi etkinleştirirseniz hangi kodun korunacağını yapılandırdığınızdan emin olun. Kod küçültücü, aksi takdirde yalnızca JNI'dan kullanılan sınıfları, yöntemleri 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ükleyiciyle ilgili bir sorun yaşıyor olabilirsiniz. FindClass, kodunuzla ilişkili sınıf yükleyicide sınıf araması başlatmak istiyor. Aşağıdaki gibi görünen ç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 genellikle istediğiniz sonucu verir. Kendiniz bir ileti dizisi oluşturursanız (ör. pthread_create çağırıp AttachCurrentThread ile ekleyerek) sorun yaşayabilirsiniz. Bu durumda, uygulamanızdan hiçbir yığın çerçevesi olmaz. Bu iş parçacığından FindClass işlevini çağırırsanız JavaVM, uygulamanızla ilişkili olan yerine "system" sınıf yükleyicisinde başlatılır. Bu nedenle, uygulamaya özel sınıfları bulma girişimleri başarısız olur.

Bu sorunu çözmek için birkaç yöntem kullanabilirsiniz:

  • 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. FindClass çağrıları, JNI_OnLoad yürütülürken System.loadLibrary işlevini çağıran işlevle ilişkili sınıf yükleyiciyi kullanır (bu, kitaplık başlatmayı daha kolay 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.
  • Yerel yönteminizi Class bağımsız değişkeni alacak şekilde bildirip Foo.class değerini ileterek sınıfın bir örneğini ihtiyaç duyan işlevlere aktarın.
  • ClassLoader nesnesine referansı kolayca erişilebilecek bir yerde önbelleğe alın ve loadClass çağrılarını doğrudan gönderin. 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 gereken bir durumla karşılaşabilirsiniz. Bit eşlemlerin veya ses örneklerinin değiştirilmesi bu duruma yaygın bir örnektir. İki temel yaklaşım vardır.

Verileri byte[] içinde saklayabilirsiniz. Bu, yönetilen koddan çok hızlı erişim sağlar. Ancak yerel tarafta, verileri kopyalamadan erişebileceğiniz garanti edilmez. Bazı uygulamalarda GetByteArrayElements ve GetPrimitiveArrayCritical, yönetilen yığındaki ham verilere gerçek işaretçiler döndürürken bazılarında yerel yığında bir arabellek ayırır ve verileri kopyalar.

Alternatif olarak veriler doğrudan bir bayt arabelleğinde depolanabilir. 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, verilere yönetilen koddan erişmek çok yavaş olabilir.

Hangisinin kullanılacağı iki faktöre bağlıdır:

  1. Veri erişimlerinin çoğu Java veya C/C++ ile yazılmış kodlardan mı yapılacak?
  2. Veriler sonunda bir sistem API'sine aktarılacaksa hangi biçimde olmalıdır? (Örneğin, veriler sonunda bir byte[] alan bir işleve aktarılıyorsa doğrudan ByteBuffer içinde işleme yapmak uygun olmayabilir.)

Kesin bir kazanan yoksa doğrudan bayt arabelleği kullanın. Bu türler için destek doğrudan JNI'ye yerleştirilmiştir ve performansın gelecekteki sürümlerde iyileşmesi beklenmektedir.