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
veyaJNI_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ğlamaJNI_OnLoad
içinde, tüm doğal yöntemleriniziRegisterNatives
kullanarak kaydedin.- Sürüm komut dosyası (tercih edilen) ile derleyin veya
-fvisibility=hidden
kullanın. Böylece kitaplığınızdan yalnızcaJNI_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
veDeleteWeakGlobalRef
'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ü belirlemeAndroid 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 üzerindeGetObjectRefType
çağrısı yaparsanızJNIGlobalRefType
yerineJNILocalRefType
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 kullanmakRegisterNatives
, 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ığındaNoClassDefFoundError
yerineClassNotFoundException
istisnası oluşturur. Bu davranış, Java Reflection API ile tutarlıdırClass.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 birjni.h
ile yeni bir GCC kullanmanın işe yaramayacağını unutmayın. Sembolleri kitaplıkta göründükleri şekilde görmek içinarm-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 iseboolean
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;
).
- Tembel yöntem aramasında, C++ işlevlerini
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ürkenSystem.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üyorsaFindClass
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 veloadClass
ç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:
- Veri erişimlerinin çoğu Java veya C/C++ ile yazılmış kodlardan mı yapılacak?
- 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.