טיפים בנושא JNI

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. שימוש באופטימיזציות אלה בלי רישום מפורש ב-JNI RegisterNatives צפוי מובילות לקריסות ב-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;, נניח).

שימוש ב-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). בהתאם למידת הישירה הוטמעה גישה למאגר נתונים זמני של בייטים, בגישה לנתונים מקוד מנוהל יכול להיות איטי מאוד.

הבחירה באיזה אופן להשתמש תלויה בשני גורמים:

  1. האם רוב הגישות לנתונים יתרחשו מקוד שנכתב ב-Java או ב-C/C++ ?
  2. אם הנתונים מועברים בסופו של דבר ל-API של מערכת, באיזה אופן חייב להיות במקום? (לדוגמה, אם הנתונים מועברים בסופו של דבר אל שלוקחת בייט[], מבצעת עיבוד באופן ישיר אולי ByteBuffer הוא לא חכם).

אם אין מנצחת ברורה, משתמשים במאגר נתונים זמני של בייט ישיר. תמיכה עבורם מובנה ישירות בתוך JNI, והביצועים אמורים להשתפר בגרסאות עתידיות.