JNI הוא ממשק המקור של Java. היא מגדירה דרך עבור הבייטקוד שמערכת Android מהדרת ממנו קוד מנוהל (נכתב בשפות התכנות Java או Kotlin) כדי לקיים אינטראקציה עם קוד נייטיב (נכתב ב-C/C++ ). JNI הוא ניטרלי לספק, ויש בו תמיכה בטעינת קוד משיתוף דינמי ספריות, ולפעמים הוא מסורבל ולפעמים יעיל.
הערה: כי מערכת Android מהדרת את Kotlin ל-bytecode שמתאים ל-ART, בדומה לשפת התכנות Java, אפשר ליישם את ההנחיות שבדף הזה את שפות התכנות Kotlin ו-Java במונחים של ארכיטקטורת JNI והעלויות שלהן. מידע נוסף זמין במאמר הבא: Kotlin ו-Android.
אם אתם עדיין לא מכירים, כדאי לקרוא את מפרט ממשק Java Native כדי להבין איך JNI פועלת ואילו תכונות זמינות. במידה מסוימת שלא ניתן להבחין באופן מיידי בממשק לקרוא קודם, כך שהחלקים הבאים יהיו שימושיים.
כדי לעיין בהפניות JNI גלובליות ולראות איפה נוצרות ונמחקות הפניות JNI גלובליות, צריך להשתמש ב- תצוגת ערימת JNI ב-Memory Profiler ב-Android Studio 3.2 ואילך.
טיפים כלליים
כדאי לנסות למזער את טביעת הרגל של שכבת ה-JNI שלכם. יש כאן כמה מאפיינים שצריך להביא בחשבון. פתרון ה-JNI שלכם צריך לנסות לעמוד בהנחיות האלה (מפורטות בהמשך לפי סדר החשיבות, שמתחילה בחשוב ביותר):
- לצמצם את הסידור של המשאבים בשכבת ה-JNI. ריצות עם הכדור בשכבת ה-JNI יש עלויות לא טריוויאליות. נסו לתכנן ממשק שמצמצם את כמות של הנתונים שאתם צריכים לגייס ואת התדירות שבה צריך לארגן נתונים.
- להימנע מתקשורת אסינכרונית בין קוד שנכתב בתכנות מנוהל בשפה ובקוד שנכתבים ב-C++ כשהדבר אפשרי. כך יהיה קל יותר לתחזק את ממשק ה-JNI. בדרך כלל אפשר להשתמש בפורמט אסינכרוני יותר ממשק המשתמש מתעדכן על ידי שמירה על העדכון האסינכרוני באותה שפה שבה נמצא ממשק המשתמש. לדוגמה, במקום להפעיל פונקציית C++ משרשור ממשק המשתמש בקוד Java דרך JNI, עדיף לבצע קריאה חוזרת (callback) בין שני תהליכונים בשפת Java, באחד מהם ביצוע הפעלת C++ חוסם, ולאחר מכן הודעה לשרשור בממשק המשתמש כשהקריאה לחסימה הושלם.
- צמצום מספר השרשורים שצריך לגעת בהם או לגעת בהם באמצעות 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 מאשר להגדיר אותו
קוד Native (ראו 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
אם רוצים לגשת לשדה של אובייקט מקוד Native, צריך לבצע את הפעולות הבאות:
- קבלת ההפניה לאובייקט המחלקה עבור המחלקה באמצעות
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));
כל methods של JNI מקבלות גם הפניות מקומיות וגם הפניות גלובליות כארגומנטים.
ייתכן שלהפניות לאותו אובייקט יהיו ערכים שונים.
לדוגמה, הערכים מוחזרים מקריאות עוקבות ל-
NewGlobalRef
באותו אובייקט יכול להיות שונה.
כדי לבדוק אם שתי הפניות מפנות לאותו אובייקט,
צריך להשתמש בפונקציה IsSameObject
. אף פעם לא להשוות
הפניות עם ==
בקוד מקורי.
אחת הסיבות לכך היא
אסור להניח שהפניות לאובייקטים הן קבועות או ייחודיות
בקוד מקורי. הערך שמייצג אובייקט יכול להיות שונה
מהפעלה אחת של שיטה אחת להפעלה הבאה, ויכול להיות ששניים
לאובייקטים שונים יכול להיות ערך זהה בקריאות עוקבות. לא להשתמש
jobject
ערכים כמפתחות.
מתכנתים נדרשים "לא להקצות יותר מדי" הפניות מקומיות. מבחינה מעשית,
שאם אתם יוצרים מספר גדול של הפניות מקומיות, אולי בזמן שאתם מריצים
צריך לשחרר אותם באופן ידני באמצעות
DeleteLocalRef
במקום לתת ל-JNI לעשות את זה בשבילך.
נדרשת רק כדי לשריין משבצות
16 הפניות מקומיות, כך שאם אתה זקוק ליותר הפניות, עליך למחוק אותן לפי הצורך או להשתמש
EnsureLocalCapacity
/PushLocalFrame
כדי להזמין מקומות נוספים.
חשוב לשים לב שהשדות jfieldID
ו-jmethodID
אטומים
ולא הפניות לאובייקטים, ואין להעביר אותם אל
NewGlobalRef
. הנתונים הגולמיים
מצביעים שמוחזרים על ידי פונקציות כמו GetStringUTFChars
ו-GetByteArrayElements
גם אינם אובייקטים. (יכול להיות שהם יעברו
בין שרשורים, והם תקפים עד לקריאה התואמת להשקה).
במקרה חריג אחד מגיע אזכור נפרד. אם מצרפים קובץ
שרשור עם 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
את הסמן בשלב מאוחר יותר.
ניתן לקבוע אם הנתונים הועתקו או לא על ידי העברת
מצביע שאינו NULL לארגומנט isCopy
. זה קורה לעיתים רחוקות
שימושי.
הפונקציה 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
),
תמיד צריך לבדוק אם יש יוצא מן הכלל, כי הערך המוחזר
יהיה תקף אם נרשמה חריגה.
חשוב לשים לב שחריגים שנגרמו כתוצאה מקוד מנוהל לא משחררים את סטאק התוכנות המקורי
פריימים. (וחריגים מסוג C++, שבדרך כלל לא מומלץ להשתמש בהם ב-Android, לא
חוצה את גבול המעבר של JNI מקוד C++ לקוד מנוהל.)
ההוראות של JNI Throw
ו-ThrowNew
להגדיר מצביע חריגה בשרשור הנוכחי. בחזרה לחשבון מנוהל
מקוד מקורי, החריג יסומן ויטופל בהתאם.
קוד מקורי יכול "לתפוס" חריגה באמצעות חיוג אל 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; }
כדי להשתמש ב-'Discovery' צריך לכנות אותן בצורה ספציפית (ראו מפרט JNI לקבלת פרטים נוספים). המשמעות היא שאם חתימת שיטה שגויה, לא תדעו עליה עד בפעם הראשונה שהשיטה באמת מופעלת.
כל השיחות של FindClass
שיבוצעו מ-JNI_OnLoad
יטופלו ככיתות
ההקשר של טוען הכיתות ששימש לטעינת הספרייה המשותפת. כשמתקבלת שיחה ממישהו אחר
להקשרים, FindClass
משתמש בטעינת הכיתות שמשויכת ל-method בחלק העליון של הקטע
סטאק Java, או אם אין כזה (כי השיחה היא משרשור מקורי שצורף כרגע)
הוא משתמש ב'מערכת' הכלי לטעינת סיווג. טוען מחלקות המערכת לא יודע על
בכיתות שלך, כך שלא תוכל לחפש כיתות משלך שמכילות את FindClass
הקשר מסוים. כך קל לחפש כיתות ב-JNI_OnLoad
ולשמור אותן במטמון: פעם אחת
יש לך קובץ עזר גלובלי חוקי של jclass
להשתמש בו מכל שרשור מצורף.
שיחות מותאמות מהירות יותר ב-@FastNative
וב-@CriticalNative
אפשר להוסיף הערות לשיטות מותאמות עם
@FastNative
או
@CriticalNative
(אבל לא לשניהם) כדי לזרז את המעברים בין קוד מנוהל לקוד מקורי. אבל הערות אלה
כוללים שינויים מסוימים בהתנהגות, שצריך לקחת בחשבון בזהירות לפני השימוש בהם. אומנם אנחנו
מציינים בקצרה את השינויים האלה בהמשך, אפשר לעיין במסמכים לקבלת הפרטים.
אפשר להחיל את ההערה @CriticalNative
רק על שיטות מותאמות שלא עושות
משתמשים באובייקטים מנוהלים (בפרמטרים או בערכים מוחזרים, או ב-this
משתמע),
שמשנה את ה-ABI של המעבר מ-JNI. ההטמעה של ה-Native חייבת לא לכלול את
הפרמטרים 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 (Icecream סנדוויץ'), ניתן להשתמש בהפניות גלובליות חלשים רק מועבר אל
NewLocalRef
,NewGlobalRef
ו-DeleteWeakGlobalRef
. (המפרט מעודד מאוד למתכנתים ליצור התייחסויות קשות לשאלות חלשות לפני שהם עושים זאת כל דבר איתם, כך שזה לא אמור להגביל כלל).החל מ-Android 4.0 (Ice Kram סנדוויץ'), קובצי עזר גלובליים חלשים עשויים להיות שהשתמשנו בה כמו בכל הפניה אחרת של JNI.
- הפניות מקומיות
עד ל-Android 4.0 (Icecream סנדוויץ'), ההפניות המקומיות היו למעשה מצביעים ישירים. גלידה הוסיפה את העקיפה שנדרשים כדי לתמוך בסוחרי אשפה טובים יותר, אבל פירוש הדבר הוא מבאגי JNI שלא ניתן לזהות בגרסאות ישנות יותר. צפייה פרטים נוספים על שינויים במסמכי העזר המקומיים של JNI ב-ICS.
בגרסאות Android שקודמות ל-Android 8.0, מספר ההפניות המקומיות מוגבל לגרסה ספציפית. החל מ-Android 8.0, מערכת Android תומכת בהפניות מקומיות ללא הגבלה.
- קובעים את סוג קובץ העזר עם
GetObjectRefType
עד ל-Android 4.0 (Ice Cream סנדוויץ'), כתוצאה מהשימוש ב- מצביעים ישירים (ראו למעלה), לא ניתן היה להטמיע
GetObjectRefType
נכון. במקום זאת השתמשנו בשיטה היוריסטית שבחן את טבלת ה-{0/} החלשה, את הוויכוחים, את המקומיים ואת הטבלה של 'קטגוריות Google' לפי הסדר הזה. בפעם הראשונה שהאפליקציה מצאה מצביע ישיר, הוא ידווח שההפניה שלך הייתה מהסוג הזה שצריך לבחון. לדוגמה, אם התקשרת אלGetObjectRefType
ב-jclass גלובלי, יהיה זהה ל-jclass שמועבר כארגומנט מרומז מקבליםJNILocalRefType
במקוםJNIGlobalRefType
. @FastNative
ו-@CriticalNative
עד Android 7, המערכת התעלמה מהערות האופטימיזציה האלה. ממשק ה-ABI אי-התאמה עבור
@CriticalNative
תוביל לארגומנט שגוי וגם קריסות.חיפוש דינמי של פונקציות נייטיב עבור
@FastNative
ו השיטות של@CriticalNative
לא הוטמעו ב-Android 8-10, מכיל באגים ידועים ב-Android 11. שימוש באופטימיזציות אלה בלי רישום מפורש ב-JNIRegisterNatives
צפוי מובילות לקריסות ב-Android 8-11.FindClass
זורקתClassNotFoundException
לצורך תאימות לאחור, מערכת Android מריצה
ClassNotFoundException
במקוםNoClassDefFoundError
כאשר כיתה לא נמצאת על ידיFindClass
. ההתנהגות הזו תואמת לממשק ה-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
תחזיר את המצביעים האמיתיים אל
בנתונים הגולמיים בערימה המנוהלת, אבל באחרים יוקצה מאגר נתונים זמני
בערימה המקורית ומעתיקים את הנתונים.
במקום זאת, ניתן לאחסן את הנתונים במאגר נתונים זמני של בייטים ישירים. האלה
אפשר ליצור באמצעות java.nio.ByteBuffer.allocateDirect
, או
את הפונקציה JNI NewDirectByteBuffer
. בניגוד לרגיל
בתהליך אגירת נתונים של בייטים, האחסון לא מוקצה לערימה המנוהלת,
תמיד ניגשים אליו ישירות מקוד נייטיב (מקבלים את הכתובת
עם GetDirectBufferAddress
). בהתאם למידת הישירה
הוטמעה גישה למאגר נתונים זמני של בייטים, בגישה לנתונים מקוד מנוהל
יכול להיות איטי מאוד.
הבחירה באיזה אופן להשתמש תלויה בשני גורמים:
- האם רוב הגישות לנתונים יתרחשו מקוד שנכתב ב-Java או ב-C/C++ ?
- אם הנתונים מועברים בסופו של דבר ל-API של מערכת, באיזה אופן
חייב להיות במקום? (לדוגמה, אם הנתונים מועברים בסופו של דבר אל
שלוקחת בייט[], מבצעת עיבוד באופן ישיר
אולי
ByteBuffer
הוא לא חכם).
אם אין מנצחת ברורה, משתמשים במאגר נתונים זמני של בייט ישיר. תמיכה עבורם מובנה ישירות בתוך JNI, והביצועים אמורים להשתפר בגרסאות עתידיות.