JNI הוא Java Native Interface. הוא מגדיר דרך לקוד הבינארי ש-Android אוספת מקוד מנוהל (שנכתב בשפות התכנות Java או Kotlin) כדי לקיים אינטראקציה עם קוד מקומי (שנכתב ב-C/C++). JNI הוא ניטרלי לספקים, יש לו תמיכה בטעינה של קוד מספריות ספריות משותפות דינמיות, ולפעמים הוא מסורבל אבל יעיל למדי.
הערה: כי מערכת Android מהדרת את Kotlin ל-bytecode שמתאים ל-ART, בדומה לשפת התכנות Java, אפשר ליישם את ההנחיות שבדף הזה את שפות התכנות Kotlin ו-Java במונחים של ארכיטקטורת JNI והעלויות שלהן. מידע נוסף זמין במאמר הבא: Kotlin ו-Android.
אם אתם לא מכירים את JNI, כדאי לקרוא את מפרט Java Native Interface כדי להבין איך JNI פועל ואיזה תכונות זמינות. יש היבטים מסוימים בממשק שלא ברורים במבט ראשון, ולכן כדאי לעיין בקטעים הבאים.
כדי לעיין בהפניות JNI גלובליות ולראות איפה נוצרות ונמחקות הפניות JNI גלובליות, צריך להשתמש ב- תצוגת ערימת JNI ב-Memory Profiler ב-Android Studio 3.2 ואילך.
טיפים כלליים
כדאי לצמצם את טביעת הרגל של שכבת ה-JNI. יש כאן כמה מאפיינים שצריך להביא בחשבון. פתרון ה-JNI שלכם צריך לנסות לעמוד בהנחיות האלה (מפורטות בהמשך לפי סדר החשיבות, שמתחילה בחשוב ביותר):
- לצמצם את הסידור של המשאבים בשכבת ה-JNI. ריצות עם הכדור בשכבת ה-JNI יש עלויות לא טריוויאליות. כדאי לתכנן ממשק שמצמצם את כמות הנתונים שצריך לארגן ואת התדירות שבה צריך לארגן את הנתונים.
- להימנע מתקשורת אסינכרונית בין קוד שנכתב בתכנות מנוהל בשפה ובקוד שנכתבים ב-C++ כשהדבר אפשרי. כך יהיה קל יותר לתחזק את ממשק ה-JNI. בדרך כלל אפשר להשתמש בפורמט אסינכרוני יותר ממשק המשתמש מתעדכן על ידי שמירה על העדכון האסינכרוני באותה שפה שבה נמצא ממשק המשתמש. לדוגמה, במקום להפעיל פונקציית C++ משרשור ממשק המשתמש בקוד Java דרך JNI, עדיף לביצוע קריאה חוזרת (callback) בין שני תהליכונים בשפת Java, באחד מהם ביצוע הפעלת C++ חוסם, ולאחר מכן הודעה לשרשור בממשק המשתמש כשהקריאה לחסימה הושלם.
- צמצום מספר השרשור ש-JNI צריך לגעת בהם או שהם צריכים לגעת ב-JNI אם צריך להשתמש במאגרי שרשורים גם בשפת Java וגם בשפת C++, כדאי לנסות לא להשתמש ב-JNI תקשורת בין בעלי המאגרים ולא בין השרשורים של העובדים השונים.
- כדאי לשמור את קוד הממשק במספר קטן של מיקומי מקור ב-C++ וב-Java שקל לזהות, כדי להקל על שינויים מבניים עתידיים. כדאי להשתמש בגנרציה אוטומטית של JNI לפי הצורך.
JavaVM ו-JNIEnv
ב-JNI מוגדרים שני מבני נתונים מרכזיים, JavaVM ו-JNIEnv. ושניהם בעצם מצביעים לטבלאות של פונקציות. (בגרסת C++ יש כיתות עם מצביע אל טבלת פונקציות ופונקציית חבר בכל פונקציית JNI שעקיפה דרך הטבלה.) JavaVM מספק את הפונקציות של 'ממשק ההפעלה', שמאפשרות ליצור JavaVM ולהרוס אותו. בתיאוריה אפשר ליצור כמה מכונות JavaVM לכל תהליך, אבל ב-Android מותר להשתמש רק פעם אחת.
ה-JNIEnv מספק את רוב הפונקציות של JNI. כל הפונקציות המקוריות מקבלות JNIEnv כ-
הארגומנט הראשון, מלבד @CriticalNative
methods,
ראו שיחות מותאמות מהירות יותר.
ה-JNIEnv משמש לאחסון מקומי בפרוטוקול Thread. לכן לא ניתן לשתף JNIEnv בין שרשורים.
אם לקטע קוד אין דרך אחרת לקבל את ה-JNIEnv שלו, צריך לשתף
את ה-JavaVM ומשתמשים ב-GetEnv
כדי לגלות את ה-JNIEnv של השרשור. (בהנחה שיש לו מאפיין כזה, יש לעיין בAttachCurrentThread
בהמשך).
הצהרות C של JNIEnv ו-JavaVM שונות מ-C++
וההצהרות שלו. קובץ ההכללה "jni.h"
מספק הגדרות typedef שונות
בהתאם להכללתו ב-C או ב-C++. לכן, לא כדאי
לכלול ארגומנטים מסוג JNIEnv בקובצי כותרת הכלולים בשתי השפות. (דרך נוספת: אם
בקובץ הכותרת נדרש #ifdef __cplusplus
, ייתכן שתצטרכו לבצע עוד קצת עבודה אם
שהכותרת מתייחסת ל-JNIEnv.)
שרשורים
כל התהליכים הם תהליכים של Linux, שמתוזמנים על ידי הליבה. בדרך כלל הם מופעלים מקובץ קוד מנוהל (באמצעות Thread.start()
), אבל אפשר גם ליצור אותם במקום אחר ולאחר מכן לצרף אותם ל-JavaVM
. עבור
למשל, שרשור שהתחיל עם pthread_create()
או עם std::thread
ניתן לצרף באמצעות הפונקציה AttachCurrentThread()
או
פונקציות AttachCurrentThreadAsDaemon()
. עד שהשרשור
מצורף, הוא לא מכיל JNIEnv ולא ניתן לבצע קריאות JNI.
בדרך כלל מומלץ להשתמש ב-Thread.start()
כדי ליצור כל חוט שצריך להפעיל קוד Java. הפעולה הזו תבטיח שיש לכם מספיק מקום בסטאק, שאתם נמצאים ב-ThreadGroup
הנכון ושאתם משתמשים באותו ClassLoader
כמו בקוד Java. קל יותר גם להגדיר את שם השרשור לניפוי באגים ב-Java מאשר מקוד מקורי (ראו pthread_setname_np()
אם יש לכם pthread_t
או thread_t
, ו-std::thread::native_handle()
אם יש לכם std::thread
ואתם רוצים pthread_t
).
הצמדת חוט שנוצר באופן מקורי גורמת ליצירה של אובייקט java.lang.Thread
ולהוספתו ל-ThreadGroup
'הראשי', כך שהוא יהיה גלוי למנטר הבאגים. קריאה ל-AttachCurrentThread()
בשרשור שכבר מצורף היא פעולה ללא תוצאה.
Android לא משהה חוטים שמריצים קוד מקומי. אם המיקום מתבצע איסוף אשפה, או שהכלי לניפוי באגים הנפיק השעיה בקשה זו, Android ישהה את השרשור בפעם הבאה שתתבצע שיחת JNI.
שרשורים שמצורפים דרך JNI חייבים להפעיל קריאה
DetachCurrentThread()
לפני היציאה.
אם התכנות באופן ישיר הוא מוזר, ב-Android 2.0 (Eclair) ואילך
יכול להשתמש ב-pthread_key_create()
כדי להגדיר כלי הרס
לפונקציה שתיקרא לפני היציאה של ה-thread,
להתקשר אל DetachCurrentThread()
משם. (השתמשו ב...
מפתח עם pthread_setspecific()
כדי לאחסן את ה-JNIEnv
thread-local-storage; כך הוא יועבר ל-Destructor,
הארגומנט.)
jclass , jmethodID ו-jfieldID
כדי לגשת לשדה של אובייקט מקוד מקורי, מבצעים את הפעולות הבאות:
- קבלת ההפניה לאובייקט המחלקה עבור המחלקה באמצעות
FindClass
- קבלת מזהה השדה של השדה
GetFieldID
- לקבל את התוכן של השדה עם משהו מתאים, כמו
GetIntField
באופן דומה, כדי לשלוח קריאה ל-method, קודם מקבלים הפניה לאובייקט מחלקה ואז מקבלים מזהה method. לרוב, המזהים הם רק הפניות למבנים פנימיים של נתונים בסביבת זמן הריצה. כדי לחפש אותם, יכול להיות שיהיה צורך בכמה מחרוזות לביצוע השוואות, אבל לאחר שהם מעודדים אותם לבצע קריאה בפועל כדי לקבל את השדה או להפעיל את השיטה הוא מהיר מאוד.
אם הביצועים חשובים לך, מומלץ לבדוק את הערכים פעם אחת ולשמור את התוצאות במטמון בקוד ה-Native שלכם. מכיוון שיש מגבלה של JavaVM אחד לכל תהליך, זה סביר כדי לאחסן את הנתונים האלה במבנה מקומי סטטי.
הפניות לכיתות, מזהי השדות ומזהי השיטות יהיו תקפים עד להסרת המחלקה. מחלקות
פורקים את הטעינה רק אם אפשר לאסוף אשפה את כל הכיתות המשויכות ל-ClassLoader,
זה נדיר, אבל לא יהיה בלתי אפשרי ב-Android. עם זאת, שימו לב
jclass
הוא הפניה לכיתה וחייב להיות מוגן באמצעות
אל NewGlobalRef
(מידע נוסף מפורט בקטע הבא).
אם אתם רוצים לשמור את המזהים במטמון בזמן טעינת כיתה, ולאחסן אותם מחדש באופן אוטומטי במטמון אם יתבצע פריקים וטעינה מחדש של הכיתה, הדרך הנכונה לאתחל היא הם מוסיפים למחלקה המתאימה קטע קוד שנראה כך:
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(); }
יוצרים רכיב method nativeClassInit
בקוד C/C++ שמבצע את חיפושי המזהים. הקוד
יבוצע פעם אחת, אחרי שהמחלקה מאותחלת. אם הכיתה תהיה זמינה שוב, היא תבוצע שוב.
הפניות מקומיות וגלובליות
כל ארגומנט שמועבר ל-method נייטיב, וכמעט כל אובייקט הוחזר בפונקציה JNI היא "הפניה מקומית". כלומר, היא תקפה משך הזמן של שיטת ה-Native הנוכחית ב-thread הנוכחי. גם אם האובייקט עצמו ממשיך לפעול אחרי השיטה המקורית מחזירה, ההפניה לא חוקית.
הכלל הזה חל על כל תתי-הקטגוריות של jobject
, כולל jclass
, jstring
ו-jarray
.
(סביבת זמן הריצה תציג אזהרה לגבי רוב השימושים הלא נכונים בהפניות כאשר מאריכים את JNI
בדיקות מופעלות).
הדרך היחידה לקבל הפניות לא מקומיות היא באמצעות הפונקציות
NewGlobalRef
וגם NewWeakGlobalRef
אם רוצים לשמור על הפניה לתקופה ארוכה יותר, צריך להשתמש בפניה 'גלובלית'. הפונקציה NewGlobalRef
לוקחת את הפונקציה
כארגומנט ומחזירה אזכור גלובלי.
ההפניה הגלובלית תהיה תקפה עד שתפעילו את DeleteGlobalRef
.
דפוס זה נפוץ כששומרים במטמון jclass שהוחזר מ-FindClass
, למשל:
jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
כל השיטות של JNI מקבלות גם הפניות מקומיות וגם הפניות גלובליות כארגומנטים.
ייתכן שלהפניות לאותו אובייקט יהיו ערכים שונים.
לדוגמה, ערכים חזרה מבקשות רצופות ל-NewGlobalRef
באותו אובייקט עשויים להיות שונים.
כדי לבדוק אם שתי הפניות מפנות לאותו אובייקט,
צריך להשתמש בפונקציה IsSameObject
. לעולם אל תבצעו השוואה בין הפניות ל-==
בקוד מקורי.
אחת הסיבות לכך היא
אסור להניח שהפניות לאובייקטים הן קבועות או ייחודיות
בקוד מקורי. הערך שמייצג אובייקט עשוי להיות שונה בהפעלה אחת של שיטה בהשוואה להפעלה אחרת, ויכול להיות לשני אובייקטים שונים יהיה אותו ערך בקריאות רצופות. אין להשתמש בערכים של jobject
כמפתחות.
מתכנתים נדרשים "לא להקצות יותר מדי" הפניות מקומיות. במילים מעשיות, המשמעות היא שאם אתם יוצרים כמות גדולה של הפניות מקומיות, אולי במהלך ריצה של מערך של אובייקטים, עליכם לפנות אותן באופן ידני באמצעות DeleteLocalRef
במקום לאפשר ל-JNI לעשות זאת בשבילכם.
נדרשת רק כדי לשריין משבצות
16 הפניות מקומיות, כך שאם דרוש לך יותר מזה, עליך למחוק אותו לפי הצורך או להשתמש
EnsureLocalCapacity
/PushLocalFrame
כדי להזמין מקומות נוספים.
חשוב לשים לב שהשדות jfieldID
ו-jmethodID
אטומים
ולא הפניות לאובייקטים, ואין להעביר אותם אל
NewGlobalRef
. גם הפונקציות שמחזירות את הפונקציות של הנתונים הגולמיים, כמו GetStringUTFChars
ו-GetByteArrayElements
, לא הן אובייקטים. (אפשר להעביר אותם בין שרשורים, והם בתוקף עד לקריאה התואמת של Release).
במקרה חריג אחד מגיע אזכור נפרד. אם מחברים חוט מקומי באמצעות AttachCurrentThread
, הקוד שפועל אף פעם לא יפנה באופן אוטומטי למשתנים המקומיים עד שהחוט ינותק. כל מכשיר מקומי
יהיה צורך למחוק קובצי עזר שתיצרו באופן ידני. באופן כללי, בכל קוד מקומי שיוצר הפניות מקומיות בלולאה, סביר להניח שיהיה צורך לבצע מחיקה ידנית.
חשוב להיזהר כשמשתמשים בהפניות גלובליות. הפניות גלובליות יכולות להיות בלתי נמנעות, אבל קשה מאוד כדי לנפות באגים, ועלולים לגרום להתנהגות שגויה של זיכרון. כשכל שאר התנאים שווים, עם פחות הפניות גלובליות, סביר להניח שהוא עדיף.
מחרוזות מסוג UTF-8 ו-UTF-16
שפת התכנות Java משתמשת ב-UTF-16. לנוחיותכם, JNI מספקת שיטות שפועלות עם גם UTF-8 שעבר שינוי. שינוי הקידוד הוא שימושי לקוד C, כי הוא מקודד \u0000 לפי 0xc0 0x80 במקום 0x00. היתרון בכך הוא שאפשר לסמוך על מחרוזות מסוג C עם סיומת אפס, שמתאימות לשימוש עם פונקציות מחרוזת רגילות של libc. הצד הנגדי הוא שאי אפשר לעבור לנתוני UTF-8 שרירותיים ל-JNI והם מצפים שהם יפעלו כמו שצריך.
כדי לקבל ייצוג UTF-16 של String
, צריך להשתמש ב-GetStringChars
.
הערה: מחרוזות UTF-16 אינן עם סיום אפס, ומותר להשתמש בערך \u0000
לכן צריך להיצמד לאורך המחרוזת ולסמן ה-jchar.
אל תשכחו Release
את המחרוזות שאתם Get
.
פונקציות המחרוזת מחזירות את הערך jchar*
או jbyte*
,
הם סימנים בסגנון C לנתונים פרימיטיביים ולא להפניות מקומיות. הם מובטחים בתוקף עד שמפעילים את Release
, כלומר הם לא משוחררים כשה-method המקורי חוזר.
הנתונים שמועברים אל NewStringUTF צריכים להיות בפורמט UTF-8 שעבר שינוי. שגיאה נפוצה היא קריאת נתוני תווים מקובץ או מזרם רשת והעברתם אל NewStringUTF
בלי לסנן אותם.
אם אתם לא יודעים שהנתונים הם בפורמט MUTF-8 תקין (או ASCII 7 סיביות, שהוא קבוצת משנה תואמת), עליכם להסיר תווים לא חוקיים או להמיר אותם לפורמט UTF-8 שעבר שינוי בצורה נכונה.
אם לא תעשו זאת, סביר להניח שההמרה ל-UTF-16 תיתן תוצאות לא צפויות.
CheckJNI – שמופעלת כברירת מחדל באמולטורים – סורק מחרוזות
ומבטלת את המכונה הווירטואלית (VM) אם היא מקבלת קלט לא חוקי.
לפני Android 8, הפעולה בדרך כלל הייתה מהירה יותר עם מחרוזות UTF-16 בתור Android
לא נדרש עותק ב-GetStringChars
, ואילו
במסגרת GetStringUTFChars
נדרשו הקצאה והמרה ל-UTF-8.
ב-Android 8, בוצע שינוי של הייצוג String
לשימוש ב-8 ביט לכל תו
למחרוזות ASCII (כדי לחסוך בזיכרון) ולהתחיל להשתמש
עובר
אוסף אשפה. התכונות האלה מצמצמות במידה משמעותית את מספר המקרים שבהם מודל ART
יכול לספק מצביע לנתונים של String
בלי ליצור עותק, אפילו
עבור GetStringCritical
. אבל אם רוב המחרוזות מעובדות על ידי הקוד
הן קצרות, וברוב המקרים ניתן להימנע מההקצאה ומהעסקה
באמצעות מאגר נתונים זמני שהוקצה במקבץ ו-GetStringRegion
או
GetStringUTFRegion
. לדוגמה:
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);
מערכים פרימיטיביים
JNI מספקת פונקציות לגישה לתוכן של אובייקטים מסוג מערך. למרות שצריך לגשת למערכי אובייקטים ברשומה אחת בכל פעם, למערכים של אפשר לקרוא ולכתוב נתונים של נייטיב ישירות כאילו הוצהרו ב-C.
כדי שהממשק יהיה יעיל ככל האפשר בלי להגביל
בהטמעה של ה-VM, השדה Get<PrimitiveType>ArrayElements
ממשפחת הקריאות מאפשרת לסביבת זמן הריצה להחזיר מצביע אל הרכיבים עצמם, או
להקצות קצת זיכרון וליצור עותק. בכל מקרה, מובטח שהצבען הגולמי המוחזר יהיה תקין עד להפעלת הקריאה המתאימה ל-Release
(כלומר, אם הנתונים לא הועתקו, אובייקט המערך יהיה מוצמד ולא ניתן יהיה להעביר אותו למיקום אחר כחלק מצמצום האשפה).
חובה Release
כל מערך שאתם Get
. כמו כן, אם Get
הקריאה תיכשל, עליך לוודא שהקוד לא מנסה Release
NULL
את הסמן בשלב מאוחר יותר.
כדי לקבוע אם הנתונים הועתקו או לא, מעבירים למשתנה ה-isCopy
הפניה שאינה NULL. זה שימושי לעיתים רחוקות.
הפונקציה Release
מקבלת ארגומנט mode
שיכול
יש אחד מתוך שלושה ערכים. הפעולות הביצוע של סביבת זמן הריצה תלויות ב-
האם הוא החזיר מצביע לנתונים בפועל או עותק שלהם:
0
- בפועל: אובייקט המערך לא מוצמד.
- העתקה: הנתונים מועתקים בחזרה. מאגר הנתונים עם העותק מתפנה.
JNI_COMMIT
- בפועל: לא עושה כלום.
- העתקה: הנתונים מועתקים בחזרה. מאגר הנתונים הזמני עם העותק לא נשלח.
JNI_ABORT
- בפועל: אובייקט המערך לא מוצמד. מוקדם יותר לא מבוטלים.
- העתקה: מאגר הנתונים עם העותק מתפנה, וכל השינויים בו יאבדו.
אחת מהסיבות לבדוק את הדגל isCopy
היא לדעת אם
עליך להתקשר אל Release
עם JNI_COMMIT
אחרי ביצוע שינויים במערך – אם אתם מחליפים בין ביצוע שינויים
והרצת קוד שמשתמש בתוכן של המערך,
יכול
לוותר על ההתחייבות. סיבה אפשרית נוספת לבדיקת הדגל היא
טיפול יעיל בJNI_ABORT
. לדוגמה, ייתכן שתרצו
לקבל מערך, לשנות אותו למקומו, להעביר חלקים לפונקציות אחרות
ומוחקים את השינויים. אם אתם יודעים ש-JNI יוצר עותק חדש עבור
אין צורך ליצור אפשרות עריכה אחרת. העתקה. אם JNI מעביר לכם את המקור, תצטרכו ליצור עותק משלכם.
טעות נפוצה (חוזרת על אותה קוד לדוגמה) היא להניח שאפשר לדלג על הקריאה Release
אם
הערך *isCopy
לא נכון. זה לא המצב. אם לא קיים מאגר נתונים זמני להעתיק
הוקצה, אז צריך להצמיד את הזיכרון המקורי ואי אפשר להעביר אותו
אוסף האשפה.
חשוב גם לציין שהדגל JNI_COMMIT
לא משחרר את המערך, ובסופו של דבר תצטרכו לבצע קריאה חוזרת ל-Release
עם דגל אחר.
שיחות אזוריות
יש חלופה לשיחות כמו Get<Type>ArrayElements
ו-GetStringChars
שיכולה להיות מאוד שימושית כשכל מה שאתם רוצים לעשות הוא להעתיק נתונים פנימה או החוצה. כדאי להביא בחשבון את הדברים הבאים:
jbyte* data = env->GetByteArrayElements(array, NULL); if (data != NULL) { memcpy(buffer, data, len); env->ReleaseByteArrayElements(array, data, JNI_ABORT); }
הפעולה הזו גורמת לצילום של המערך והעתקה של הבייט הראשון (len
)
ואז משחררים את המערך. בהתאם לתרחיש לדוגמה
הקריאה ל-Get
תצמיד או תעתיק את המערך
תוכן.
הקוד מעתיק את הנתונים (אולי בפעם השנייה), ולאחר מכן קורא ל-Release
; במקרה הזה
בעזרת JNI_ABORT
, אין סיכוי לעותק שלישי.
אפשר להשיג את אותו הדבר בצורה פשוטה יותר:
env->GetByteArrayRegion(array, 0, len, buffer);
יש לכך כמה יתרונות:
- נדרשת קריאת JNI אחת במקום 2, וכך התקורה מצטמצמת.
- לא נדרשת הצמדה או עותקי נתונים נוספים.
- מפחית את הסיכון לשגיאות מתכנתים — אין סיכון לשכוח
להתקשר אל
Release
אחרי שמשהו נכשל.
באותו אופן, אפשר להשתמש בפונקציה Set<Type>ArrayRegion
כדי להעתיק נתונים למערך, GetStringRegion
או
GetStringUTFRegion
כדי להעתיק תווים מתוך
String
חריגים
אין לקרוא לרוב הפונקציות JNI בזמן שיש חריג בהמתנה.
הקוד אמור להופיע כחריג (באמצעות הערך המוחזר של הפונקציה,
ExceptionCheck
או ExceptionOccurred
) וחוזרים,
או למחוק את החריג ולטפל בו.
פונקציות ה-JNI היחידות שאליהן ניתן להפעיל אם יש חריג חריג בהמתנה:
DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
Release<PrimitiveType>ArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
הרבה קריאות JNI יכולות להוביל לחריגה, אבל לעיתים קרובות הן מספקות דרך פשוטה יותר
בבדיקת הכשל. לדוגמה, אם הפונקציה NewString
מחזירה
ערך שאינו NULL, אין צורך לחפש חריגה. אבל אם
קוראים ל-method (באמצעות פונקציה כמו CallObjectMethod
),
תמיד צריך לבדוק אם יש יוצא מן הכלל, כי הערך המוחזר
יהיה תקף אם נרשמה חריגה.
חשוב לזכור שהחרגות שמשוחררות על ידי קוד מנוהל לא מבטלות את מסגרות ה-stack הילידיות. (וכן, אסור להפעיל חריגות של C++ מעבר לגבול המעבר של JNI מקידוד C++ לקידוד מנוהל).
ההוראות Throw
ו-ThrowNew
של JNI רק מגדירות מציין חריגה בשרשור הנוכחי. בחזרה לחשבון מנוהל
מקוד מקורי, החריג יסומן ויטופל בהתאם.
קוד מקורי יכול "לתפוס" חריג: חיוג אל ExceptionCheck
או
ExceptionOccurred
, ומנקים אותה באמצעות
ExceptionClear
. כרגיל,
ביטול חריגות מבלי לטפל בהן עלול לגרום לבעיות.
אין פונקציות מובנות לביצוע מניפולציות על האובייקט Throwable
עצמו, לכן אם רוצים (למשל) לקבל את מחרוזת החריגה, צריך
מציאת הכיתה Throwable
, חיפוש מזהה השיטה של
getMessage "()Ljava/lang/String;"
, יש להפעיל אותו, ואם התוצאה
הוא לא NULL. השתמשו ב-GetStringUTFChars
כדי לקבל משהו שאתם יכולים
בכתב יד printf(3)
או מקבילה.
בדיקה מורחבת
ב-JNI מתבצעת בדיקת שגיאות מועטה מאוד. שגיאות בדרך כלל גורמות לקריסה. ב-Android יש גם מצב בשם CheckJNI, שבו מצביעי הטבלה של JavaVM ו-JNIEnv עוברים לטבלאות של פונקציות שמבצעות סדרה מורחבת של בדיקות לפני קריאה להטמעה הרגילה.
הבדיקות הנוספות כוללות:
- מערכי: ניסיון להקצות מערך בגודל שלילי.
- סמנים לא תקינים: העברת jarray/jclass/jobject/jstring פגום אל קריאה ל-JNI, או העברת מצביע NULL לקריאה של JNI עם ארגומנט שאינו null.
- שמות של כיתות: העברה של כל דבר מלבד סגנון שם הכיתה 'java/lang/String' לקריאה ל-JNI.
- קריאות קריטיות: ביצוע קריאה ל-JNI בין קריאה מסוג 'קריטית' לבין השחרור התואם שלה.
- ByteBuffers ישירה: העברת ארגומנטים לא תקינים אל
NewDirectByteBuffer
. - חריגות: ביצוע קריאה ל-JNI בזמן שיש חריגה בהמתנה.
- JNIEnv*s: שימוש ב-JNIEnv* מהשרשור הלא נכון.
- jfieldID: שימוש ב-jfieldID NULL, או שימוש ב-jfieldID כדי להגדיר שדה לערך מסוג שגוי (בניסיון להקצות StringBuilder לשדה String (למשל), שימוש ב-jfieldID בשדה סטטי כדי להגדיר שדה מופע או להיפך, או שימוש ב-jfieldID ממחלקה אחת עם מופעים של מחלקה אחרת.
- jmethodID: שימוש ב-jmethodID מסוג שגוי כשמבצעים קריאת JNI של
Call*Method
: סוג החזרה שגוי, אי התאמה סטטית/לא סטטית, סוג שגוי ל-'this' (לשיחות שאינן סטטיות) או מחלקה שגויה (לקריאות סטטיות). - הפניות: שימוש ב-
DeleteGlobalRef
/DeleteLocalRef
בסוג שגוי של הפניה. - מצבי פרסום: העברת מצב פרסום שגוי לקריאה לפרסום (משהו שאינו
0
,JNI_ABORT
אוJNI_COMMIT
). - בטיחות סוג: החזרת סוג לא תואם מהשיטה המקורית (החזרת StringBuilder משיטה שהוצהרה להחזרת מחרוזת, למשל).
- UTF-8: העברה של רצף בייטים לא חוקי של UTF-8 שעבר שינוי לקריאה ל-JNI.
(הנגישות של שיטות ושדות עדיין לא מסומנת: הגבלות הגישה לא חלות על קוד נייטיב).
יש כמה דרכים להפעיל את CheckJNI.
אם משתמשים באמולטור, CheckJNI מופעל כברירת מחדל.
אם יש לכם מכשיר עם הרשאות בסיס, תוכלו להשתמש ברצף הפקודות הבא כדי להפעיל מחדש את סביבת זמן הריצה כש-CheckJNI מופעל:
adb shell stop adb shell setprop dalvik.vm.checkjni true adb shell start
בכל אחד מהמקרים האלה, כשתתחילו את סביבת זמן הריצה, תראו משהו כזה בפלט ה-Logcat:
D AndroidRuntime: CheckJNI is ON
אם יש לכם מכשיר רגיל, תוכלו להשתמש בפקודה הבאה:
adb shell setprop debug.checkjni 1
הפעולה הזו לא תשפיע על אפליקציות שכבר פועלות, אבל בכל אפליקציה שתופעל מאותו רגע ואילך תופעל CheckJNI. (יש לשנות את הנכס לכל ערך אחר או פשוט להפעיל מחדש ישבית שוב את CheckJNI). במקרה כזה, בפעם הבאה שאפליקציה תופעל יופיע משהו כזה בפלט של logcat:
D Late-enabling CheckJNI
אפשר גם להגדיר את המאפיין android:debuggable
במניפסט של האפליקציה כך:
הפעלת CheckJNI רק לאפליקציה שלך. שימו לב שכלי ה-build של Android יבצעו את הפעולה הזו באופן אוטומטי
סוגים מסוימים של גרסאות build.
ספריות מקוריות
אפשר לטעון קוד מקורי מספריות משותפות באמצעות הפונקציה הרגילה System.loadLibrary
.
בפועל, בגרסאות ישנות יותר של Android היו באגים ב-PackageManager שגרמו להתקנה עדכון של ספריות נייטיב באופן שלא יהיה מהימן. המקשר מחדש מציע דרכים לעקוף את הבעיה הזו ובעיות טעינה אחרות של ספריות נייטיב.
התקשרות אל System.loadLibrary
(או ReLinker.loadLibrary
) מכיתה סטטית
של האתחול. הארגומנט הוא 'לא מקושט' שם הספרייה,
לכן כדי לטעון את libfubar.so
שהיית מעביר ב-"fubar"
.
אם יש לכם רק כיתה אחת שמתבססת על שיטות נייטיב, הגיוני להפעיל את הקריאה
System.loadLibrary
כדי להיות במאתחל סטטי בשביל המחלקה הזו. אחרת ייתכן
אני רוצה לבצע את השיחה מ-Application
כדי שתדע שהספרייה תמיד נטענת,
ותמיד נטענות מוקדם.
יש שתי דרכים שבהן סביבת זמן הריצה יכולה למצוא את השיטות המקומיות. אפשר לרשום אותן באופן מפורש באמצעות RegisterNatives
, או לאפשר לסביבת זמן הריצה לחפש אותן באופן דינמי באמצעות dlsym
. היתרונות של RegisterNatives
הם שמקבלים מראש
לבדוק שהסמלים קיימים, ואפשר גם לשתף ספריות קטנות ומהירות יותר
ייצוא כל דבר מלבד JNI_OnLoad
. היתרון של מתן אפשרות לסביבת זמן הריצה לגלות את
הוא מצריך קצת פחות קוד לכתיבה.
כדי להשתמש באפליקציה RegisterNatives
:
- צריך לציין את הפונקציה
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
. - ב
JNI_OnLoad
, רושמים את כל השיטות המותאמות באמצעותRegisterNatives
. - צור באמצעות סקריפט גרסה (מועדף) או משתמשים
-fvisibility=hidden
כך שרקJNI_OnLoad
מיוצא מהספרייה שלך. כך נוצר קוד מהיר יותר וקטן יותר, התנגשויות עם ספריות אחרות שנטענות לאפליקציה שלכם (אבל הן יוצרות דוחות קריסות פחות שימושיים אם האפליקציה קורסת בקוד נייטיב).
המאתחל הסטטי אמור להיראות כך:
Kotlin
companion object { init { System.loadLibrary("fubar") } }
Java
static { System.loadLibrary("fubar"); }
הפונקציה JNI_OnLoad
אמורה להיראות כך אם היא נכתבת ב-C++:
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; }
כדי להשתמש במקום זאת ב'גילוי' של שיטות מקומיות, צריך לתת להן שם באופן ספציפי (פרטים מופיעים במפרט JNI). המשמעות היא שאם חתימת שיטה שגויה, לא תדעו עליה עד בפעם הראשונה שהשיטה מופעלת בפועל.
כל השיחות של FindClass
שיבוצעו מ-JNI_OnLoad
יטופלו ככיתות
ההקשר של טוען הכיתות ששימש לטעינת הספרייה המשותפת. כשמפעילים את FindClass
מהקשרים אחרים, הוא משתמש במטען הכיתבים שמשויך לשיטה בחלק העליון של סטאק Java. אם אין כזה (כי הקריאה מגיעה מחוט מקורי שצורף זה עתה), הוא משתמש במטען הכיתבים 'system'. טוען מחלקות המערכת לא יודע על
בכיתות האלה, לא תוכלו לחפש כיתות משלכם שמכילות את FindClass
הקשר מסוים. לכן, JNI_OnLoad
הוא מקום נוח לחיפוש כיתות ולהטמעה שלהן במטמון: אחרי שיוצרים הפניה גלובלית תקינה ל-jclass
, אפשר להשתמש בה מכל שרשור שמצורף.
שיחות מקוריות מהירות יותר עם @FastNative
ועם @CriticalNative
אפשר להוסיף הערות לשיטות מותאמות עם
@FastNative
או
@CriticalNative
(אבל לא לשניהם) כדי לזרז את המעברים בין קוד מנוהל לקוד מקורי. עם זאת, ההערות האלה מביאות לשינויים מסוימים בהתנהגות, שצריך להביא בחשבון לפני שמשתמשים בהן. אומנם אנחנו
מציינים בקצרה את השינויים האלה בהמשך, אפשר לעיין במסמכים לקבלת הפרטים.
אפשר להחיל את ההערה @CriticalNative
רק על שיטות מותאמות שלא עושות
משתמשים באובייקטים מנוהלים (בפרמטרים או בערכים מוחזרים, או ב-this
משתמע),
שמשנה את ה-ABI של המעבר מ-JNI. ההטמעה המקורית חייבת להחריג את הפרמטרים JNIEnv
ו-jclass
מחתימת הפונקציה.
בזמן הפעלה של @FastNative
או @CriticalNative
, האשפה
לא ניתן להשעות את השרשור עקב עבודות חיוניות, ויכול להיות שהוא ייחסם. אל תשתמשו בהערות האלה בשיטות ממושכות, כולל שיטות שבדומה לרוב הן מהירות אבל בדרך כלל ללא הגבלה.
במיוחד, אסור שהקוד יבצע פעולות קלט/פלט משמעותיות או יקבל מנעולים מקומיים שיכולים להישאר נעולים במשך זמן רב.
ההערות האלה הוטמעו לשימוש המערכת,
Android 8
והפך לציבורי שנבדקו באמצעות CTS
API ב-Android 14. סביר להניח שהאופטימיזציות האלה יפעלו גם במכשירי Android מגרסה 8-13 (אבל
ללא אחריות CTS חזקה), אך החיפוש הדינמי של שיטות נייטיב נתמך רק
Android מגרסה 12 ואילך, נדרש רישום מפורש ב-JNI RegisterNatives
למכשירים עם גרסאות Android 8-11. ב-Android 7- המערכת מתעלמת מההערות האלה, או חוסר ההתאמה ב-ABI
עבור @CriticalNative
יוביל לסידור שגוי של ארגומנטים, וסביר להניח לקריסות.
אם מדובר בשיטות חיוניות לשיפור הביצועים שזקוקות להערות האלה, מומלץ מאוד
לרשום באופן מפורש את השיטות עם JNI RegisterNatives
במקום להסתמך על
"Discovery" מבוסס-שם של שיטות נייטיב. כדי לקבל ביצועים אופטימליים בהפעלת האפליקציה, מומלץ לכלול בפרופיל הבסיס את הגורמים שמפעילים את השיטות @FastNative
או @CriticalNative
. החל מ-Android 12,
קריאה לשיטה נייטיב @CriticalNative
משיטה מנוהלת שעברה הידור תהיה דומה
זול כמו קריאה שאינה מוטבעת ב-C/C++ כל עוד כל הארגומנטים מתאימים לרישום (לדוגמה עד
8 ארגומנטים של אינטגרל ועד 8 ארגומנטים של נקודה צפה (floating-point) בזרוע 64).
לפעמים עדיף לפצל את השיטה המקורית לשתיים, שיטה מהירה מאוד נכשלים ועוד צוות שמטפל במקרים האיטיים. לדוגמה:
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-ביט
כדי לתמוך בארכיטקטורות שמשתמשות בסמנים של 64 ביט, צריך להשתמש בשדה long
במקום
int
כשמאחסנים מצביע למבנה מותאם בשדה Java.
תכונות שלא נתמכות/תאימות לאחור
יש תמיכה בכל התכונות של JNI 1.6, מלבד:
- ההטמעה של
DefineClass
לא מוטמעת. ב-Android לא משתמשים קטעי קוד של Java או קובצי מחלקה, כך שההעברה של נתוני מחלקה בינארית לא עובדת.
כדי לשמור על תאימות לאחור לגרסאות Android ישנות יותר, כדאי לדעת:
- חיפוש דינמי של פונקציות נייטיב
עד Android 2.0 (Eclair), התו '$' לא הומר כראוי ל-"_00024" במהלך חיפושים של שמות שיטות. כדי לעקוף את הבעיה הזו, צריך להשתמש ברישום מפורש או להעביר את השיטות הילידיות מתוך הכיתות הפנימיות.
- ניתוק שרשורים
עד ל-Android 2.0 (Eclair), לא ניתן היה להשתמש ב-
pthread_key_create
בונה שנמנעים מ"יש לנתק את ה-thread לפני יציאה" לסמן. (סביבת זמן הריצה משתמשת גם בפונקציית pthread destructor), אז זה יהיה מרוץ שצריך לראות למי להתקשר קודם.) - הפניות גלובליות חלשות
עד Android 2.2 (Froyo), לא הוטמעו הפניות גלובאליות חלשות. גרסאות ישנות יותר יידחו בתוקף ניסיונות להשתמש בהן. אפשר להשתמש בקבצים הקבועים של גרסאות פלטפורמת Android כדי לבדוק את התמיכה.
עד ל-Android 4.0 (Ice Cream סנדוויץ'), ניתן להשתמש בהפניות גלובליות חלשים רק מועבר אל
NewLocalRef
,NewGlobalRef
ו-DeleteWeakGlobalRef
. (המפרט מעודד מאוד למתכנתים ליצור התייחסויות קשות לשאלות חלשות לפני שהם עושים זאת כל דבר איתם, כך שזה לא אמור להגביל כלל).החל מ-Android 4.0 (Ice Cream Sandwich), אפשר להשתמש בהפניות גלובליות חלשות כמו בכל הפניות אחרות של JNI.
- הפניות מקומיות
עד ל-Android 4.0 (Icecream סנדוויץ'), ההפניות המקומיות היו למעשה מצביעים ישירים. גלידה הוסיפה את העקיפה שנדרשים כדי לתמוך בסוחרי אשפה טובים יותר, אבל פירוש הדבר הוא מבאגי JNI שלא ניתן לזהות בגרסאות ישנות יותר. לפרטים נוספים, ראו שינויים בהפניות המקומיות של JNI ב-ICS.
בגרסאות Android שקודמות ל-Android 8.0, מספר ההפניות המקומיות מוגבל לגרסה ספציפית. החל מגרסה 8.0 של Android, יש תמיכה בהפניות מקומיות ללא הגבלה.
- קובעים את סוג קובץ העזר עם
GetObjectRefType
עד Android 4.0 (Icecream סנדוויץ'), כתוצאה מהשימוש ב- מצביעים ישירים (ראו למעלה), לא היה אפשר להטמיע
GetObjectRefType
נכון. במקום זאת, השתמשנו בהווריסטיקה שבדקה את טבלת המשתנים הגלובליים החלשים, את הארגומנטים, את טבלת המשתנים המקומיים ואת טבלת המשתנים הגלובליים בסדר הזה. בפעם הראשונה שהאפליקציה מצאה מצביע ישיר, הוא ידווח שההפניה שלך הייתה מהסוג הזה שצריך לבחון. לדוגמה, אם התקשרת אלGetObjectRefType
ב-jclass גלובלי, יהיה זהה ל-jclass שמועבר כארגומנט מרומז מקבליםJNILocalRefType
במקוםJNIGlobalRefType
. @FastNative
ו-@CriticalNative
עד Android 7, המערכת התעלמה מהערות האופטימיזציה האלה. אי ההתאמה של ABI ל-
@CriticalNative
תוביל לסידור שגוי של הארגומנטים ולקריסות.חיפוש דינמי של פונקציות נייטיב עבור
@FastNative
ו השיטות של@CriticalNative
לא הוטמעו ב-Android 8-10, מכיל באגים ידועים ב-Android 11. שימוש באופטימיזציות האלה בלי רישום מפורש ב-JNIRegisterNatives
עלול לגרום לקריסות ב-Android 8 עד 11.FindClass
זורקתClassNotFoundException
כדי לשמור על תאימות לאחור, כשהמערכת לא מוצאת כיתה באמצעות
FindClass
, היא גורמת ל-Android להקפיץ את השגיאהClassNotFoundException
במקוםNoClassDefFoundError
. ההתנהגות הזו תואמת לממשק ה-API של Java Re Recallion.Class.forName(name)
שאלות נפוצות: למה מוצג לי UnsatisfiedLinkError
?
כשעובדים על קוד מקורי, לפעמים יש כשל כזה:
java.lang.UnsatisfiedLinkError: Library foo not found
במקרים מסוימים, המשמעות היא בדיוק מה שכתוב – הספרייה לא נמצאה. לחשבון
מקרים אחרים שבהם הספרייה קיימת אבל לא ניתן לפתוח אותה על ידי dlopen(3)
, וגם
פרטי הכשל מופיעים בהודעה המפורטת של החריגה.
סיבות נפוצות לבעיות עם הכיתוב "הספרייה לא נמצאה" חריגים:
- הספרייה לא קיימת או שאינה נגישה לאפליקציה. כדאי להשתמש
adb shell ls -l <path>
כדי לבדוק את הנוכחות שלהם והרשאות. - הספרייה לא נבנתה באמצעות ה-NDK. כתוצאה מכך ותלויות בפונקציות או בספריות שלא קיימות במכשיר.
מחלקה אחרת של UnsatisfiedLinkError
כשלים נראית כך:
java.lang.UnsatisfiedLinkError: myfunc at Foo.myfunc(Native Method) at Foo.main(Foo.java:10)
ב-Logcat יופיעו:
W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V
זה אומר שסביבת זמן הריצה ניסתה למצוא שיטה תואמת, אבל הפעולה נכשלה. בין הסיבות הנפוצות לכך:
- הספרייה לא נטענת. צריך לבדוק את פלט ה-Logcat של הודעות על טעינת הספרייה.
- השיטה לא נמצאה עקב אי-התאמה בשם או בחתימה. הזה
נגרמת בדרך כלל על ידי:
- בחיפוש שיטה מדורגת, אין הצהרה על פונקציות C++
עם
extern "C"
ומתאימים החשיפה (JNIEXPORT
). שימו לב שלפני גלידה כריך, המאקרו JNIEXPORT היה שגוי, לכן שימוש ב-GCC חדש עםjni.h
ישן לא יפעל. אפשר להשתמש ב-arm-eabi-nm
כדי לראות את הסמלים כפי שהם מופיעים בספרייה. אם הם מסתכלים מעוות (בערך כמו_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass
ולאJava_Foo_myfunc
), או אם סוג הסמל הוא 't' קטנה במקום 'T' באותיות רישיות, אז צריך צריך לתקן את ההצהרה. - ברישום מפורש, שגיאות קלות בזמן הזנת חתימה של שיטות. חשוב לוודא שהנתונים שאתם מעבירים לקריאה לרישום תואמים לחתימה בקובץ היומן.
חשוב לזכור ש-'B' הוא
byte
ו-'Z' הואboolean
. רכיבי השם של הכיתה בחתימות מתחילים ב-'L' ומסתיימים ב-';', משתמשים ב-'/' כדי להפריד בין שמות חבילות/כיתות, ומשתמשים ב-'$' כדי להפריד בין שמות של כיתות פנימיות (למשל,Ljava/util/Map$Entry;
).
- בחיפוש שיטה מדורגת, אין הצהרה על פונקציות C++
עם
שימוש ב-javah
כדי ליצור כותרות JNI באופן אוטומטי יכול לעזור למנוע בעיות מסוימות.
שאלות נפוצות: למה FindClass
לא מצאה את הכיתה שלי?
(רוב העצות האלה רלוונטיות באותה מידה גם לכשל במציאת שיטות
עם GetMethodID
או GetStaticMethodID
, או שדות
עם GetFieldID
או GetStaticFieldID
).
צריך לוודא שהמחרוזת של שם הכיתה בפורמט הנכון. שיעור JNI
השמות מתחילים בשם החבילה ומופרדים בלוכסנים,
כמו java/lang/String
. אם אתם מחפשים מחלקה של מערך,
צריך להתחיל במספר המתאים של סוגריים מרובעים
חייב להקיף את המחלקה באותיות 'L' ו-';', כלומר מערך חד-ממדי של
String
יהיה [Ljava/lang/String;
.
אם אתם מחפשים כיתה פנימית, צריך להשתמש ב-'$' במקום '.'. באופן כללי,
שימוש ב-javap
בקובץ ה- .class הוא דרך טובה לגלות
השם הפנימי של הכיתה שלכם.
אם מפעילים כיווץ קוד, צריך לוודא להגדיר איזה קוד לשמור. מתבצעת הגדרה חשוב לשמור על כללי שמירה מתאימים כי מכווץ הקוד עלול להסיר מחלקות, methods, או שדות שמשתמשים בהם רק מ-JNI.
אם שם הכיתה נראה תקין, יכול להיות שיש לך אפשרות לטעון כיתה.
בעיה. FindClass
רוצה להתחיל את חיפוש הכיתה במטען הכיתות שמשויך לקוד שלכם. הוא בוחן את ערימת הקריאות,
שנראה בערך כך:
Foo.myfunc(Native Method) Foo.main(Foo.java:10)
השיטה העליונה ביותר היא Foo.myfunc
. FindClass
מוצא את האובייקט ClassLoader
שמשויך לאובייקט Foo
והם משתמשים בו.
בדרך כלל זה עושה את מה שרוצים. אם תיצרו את השרשור בעצמכם (אולי באמצעות קריאה ל-pthread_create
ואז צירוף באמצעות AttachCurrentThread
), לא יהיו מסגרות סטאק מהאפליקציה.
אם תתקשר למספר FindClass
מהשרשור הזה,
JavaVM יתחיל ב'מערכת' הכלי לטעינת כיתות, במקום זאת המשויך
באפליקציה, ולכן ניסיונות למצוא כיתות ספציפיות לאפליקציה ייכשלו.
יש כמה דרכים לעקוף את הבעיה הזו:
- מבצעים את השאילתות של
FindClass
פעם אחת, ב-JNI_OnLoad
, ושומרים במטמון את הפניות הכיתתיות לשימוש מאוחר יותר. כל השיחות מסוגFindClass
שיבוצעו כחלק מביצועJNI_OnLoad
ישתמש בטעינת הכיתה שמשויכת אל את הפונקציה שנקראתSystem.loadLibrary
כלל מיוחד, כדי להפוך את אתחול הספרייה לנוח יותר). אם קוד האפליקציה טוען את הספרייה,FindClass
ישתמש בטוען הכיתות הנכון. - מעבירים מופע של המחלקה לפונקציות שצריכות
באמצעות הצהרה על השיטה המקורית לקחת ארגומנט Class
לאחר מכן מעבירים את
Foo.class
פנימה. - שומרים במטמון הפניה לאובייקט
ClassLoader
במקום נוח, ומפעילים קריאות ל-loadClass
ישירות. זה דורש מאמץ.
שאלות נפוצות: איך משתפים נתונים גולמיים עם קוד מקורי?
ייתכן שתיתקלו במצב שבו תצטרכו לגשת לפריט גדול מאגר של נתונים גולמיים מקוד מנוהל וגם מקוד נייטיב. דוגמאות נפוצות לכלול מניפולציה של מפות סיביות או דגימות צליל. יש שתי פלטפורמות גישות בסיסיות.
אפשר לאחסן את הנתונים בbyte[]
. כך אפשר לגשת במהירות רבה מקוד מנוהל. אבל בהיבט המקומי,
אין כל אפשרות לגשת לנתונים מבלי להעתיק אותם. בהטמעות מסוימות, הפונקציות GetByteArrayElements
ו-GetPrimitiveArrayCritical
יחזירו הפניות בפועל לנתונים הגולמיים ב-heap המנוהל, אבל בהטמעות אחרות הן יוקצו מאגר ב-heap המקורי ויעתיקו את הנתונים.
האפשרות החלופית היא לאחסן את הנתונים במאגר בייט ישיר. האלה
אפשר ליצור באמצעות java.nio.ByteBuffer.allocateDirect
, או
את הפונקציה JNI NewDirectByteBuffer
. בניגוד לרגיל
בתהליך אגירת נתונים של בייטים, האחסון לא מוקצה לערימה המנוהלת,
תמיד ניגשים אליו ישירות מקוד נייטיב (מקבלים את הכתובת
עם GetDirectBufferAddress
). בהתאם למידת הישירה
הוטמעה גישה למאגר נתונים זמני של בייטים, בגישה לנתונים מקוד מנוהל
יכול להיות איטי מאוד.
הבחירה באיזה אופן להשתמש תלויה בשני גורמים:
- האם רוב הגישות לנתונים יתרחשו מקוד שנכתב ב-Java או ב-C/C++ ?
- אם הנתונים מועברים בסופו של דבר ל-API של מערכת, באיזה פורמט הם צריכים להיות? (לדוגמה, אם הנתונים מועברים בסופו של דבר לפונקציה שמקבלת byte[], יכול להיות שלא כדאי לבצע עיבוד ישירות ב-
ByteBuffer
).
אם אין מנצחת ברורה, משתמשים במאגר נתונים זמני של בייט ישיר. תמיכה עבורם מובנה ישירות בתוך JNI, והביצועים אמורים להשתפר בגרסאות עתידיות.