Android Interface Definition Language (AIDL)

שפת הגדרה לבניית ממשק Android‏ (AIDL) דומה לשפות IDL אחרות: היא מאפשרת להגדיר את ממשק התכנות שעליו הלקוח והשירות מסכימים כדי לתקשר זה עם זה באמצעות תקשורת בין תהליכים (IPC).

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

הערה: צריך להשתמש ב-AIDL רק אם אתם מאפשרים ללקוחות מאפליקציות שונות לגשת לשירות שלכם לצורך IPC, ואתם רוצים לטפל במספר תהליכים בו-זמנית בשירות. אם אתם לא צריכים לבצע IPC בו-זמנית באפליקציות שונות, תוכלו ליצור את הממשק על ידי הטמעת Binder. אם אתם רוצים לבצע IPC אבל לא צריכים לטפל במספר תהליכים, תוכלו להטמיע את הממשק באמצעות Messenger. עם זאת, חשוב להבין את השירותים המקושרים לפני שמטמיעים AIDL.

לפני שמתחילים לתכנן את ממשק ה-AIDL, חשוב לזכור שהקריאות לממשק AIDL הן קריאות ישירות לפונקציות. אל תניחו הנחות לגבי השרשור שבו מתבצעת הקריאה. מה שקורה משתנה בהתאם לשאלה אם הקריאה היא משרשור בתהליך המקומי או בתהליך מרוחק:

  • שיחות שמתבצעות מהתהליך המקומי מתבצעות באותו שרשור שמבצע את הקריאה. אם זהו ה-thread הראשי של ממשק המשתמש, ה-thread הזה ממשיך לפעול בממשק AIDL. אם זהו חוט אחר, הוא זה שיריץ את הקוד בשירות. לכן, אם רק שרשראות מקומיות ניגשות לשירות, תוכלו לשלוט באופן מלא בשרשורים שפועלים בו. אבל אם זה המצב, אל תשתמשו ב-AIDL בכלל. במקום זאת, תוכלו ליצור את הממשק על ידי הטמעת Binder.
  • קריאות מתהליך מרוחק מועברות מאוסף השרשור (thread pool) שהפלטפורמה שומרת בתוך התהליך שלכם. חשוב להיות מוכנים לקבל שיחות נכנסות משרשורים לא מוכרים, עם כמה שיחות בו-זמנית. במילים אחרות, הטמעה של ממשק AIDL חייבת להיות בטוחה לחלוטין לשימוש בכמה שרשורים (thread-safe). קריאות שנשלחות משרשור אחד באותו אובייקט מרוחק מגיעות בסדר בצד המקבל.
  • מילת המפתח oneway משנה את ההתנהגות של שיחות מרחוק. כשמשתמשים בו, שיחה מרחוק לא חסומה. הוא שולח את נתוני העסקה ומוחזר מיד. בסופו של דבר, הטמעת הממשק תקבל את ההודעה הזו כקריאה רגילה ממאגר ה-threads של Binder, כקריאה רגילה מרחוק. אם משתמשים ב-oneway בשיחה מקומית, אין השפעה והשיחה עדיין מסונכרנת.

הגדרת ממשק AIDL

מגדירים את ממשק ה-AIDL בקובץ .aidl באמצעות התחביר של שפת התכנות Java, ושומרים אותו בקוד המקור בספרייה src/, גם באפליקציה שמארחת את השירות וגם בכל אפליקציה אחרת שמקשרת לשירות.

כשמפתחים כל אפליקציה שמכילה את הקובץ .aidl, הכלים של Android SDK יוצרים ממשק IBinder על סמך הקובץ .aidl ושומרים אותו בספרייה gen/ של הפרויקט. השירות צריך להטמיע את הממשק של IBinder בהתאם לצורך. לאחר מכן אפליקציות הלקוח יכולות לקשר לשירות ול-methods של קריאה מ-IBinder כדי לבצע IPC.

כדי ליצור שירות מוגבל באמצעות AIDL, פועלים לפי השלבים הבאים, שמתוארים בסעיפים הבאים:

  1. יוצרים את הקובץ .aidl

    הקובץ הזה מגדיר את ממשק התכנות באמצעות חתימות שיטות.

  2. הטמעת הממשק

    הכלים של Android SDK יוצרים ממשק בשפת התכנות Java על סמך הקובץ .aidl. לממשק הזה יש מחלקה מופשטת פנימית בשם Stub שמרחיבה את Binder ומטמיעה שיטות מממשק AIDL. צריך להרחיב את המחלקה Stub ולהטמיע את השיטות.

  3. חשיפת הממשק ללקוחות

    מטמיעים Service ומבטלים את הגדרת ברירת המחדל של onBind() כדי להחזיר את ההטמעה שלכם של המחלקה Stub.

זהירות: כל שינוי שתבצעו בממשק AIDL אחרי הגרסה הראשונה צריך להישאר תואם לאחור, כדי למנוע תקלות באפליקציות אחרות שמשתמשות בשירות שלכם. כלומר, מכיוון שצריך להעתיק את קובץ .aidl לאפליקציות אחרות כדי שיוכלו לגשת לממשק של השירות, עליכם להמשיך לקבל תמיכה בממשק המקורי.

יוצרים את קובץ ה- .aidl

ל-AIDL יש תחביר פשוט שמאפשר להצהיר על ממשק עם method אחת או יותר שיכולה לקבל פרמטרים ולהחזיר ערכים. הפרמטרים וערכי ההחזרה יכולים להיות מכל סוג, גם ממשקים אחרים שנוצרו באמצעות AIDL.

צריך ליצור את הקובץ .aidl באמצעות שפת התכנות Java. כל קובץ .aidl צריך להגדיר ממשק יחיד וצריך רק להשתמש בו בהצהרת הממשק ובחתימות שיטה.

כברירת מחדל, AIDL תומך בסוגי הנתונים הבאים:

  • כל סוגי הפרימטיבים בשפת התכנות Java (כמו int,‏ long,‏ char,‏ boolean וכו')
  • מערך מכל סוג, כמו int[] או MyParcelable[]
  • String
  • CharSequence
  • List

    כל הרכיבים ב-List חייבים להיות מאחד מסוגי הנתונים הנתמכים ברשימה הזו, או מאחד הממשקים או ה-Parcelables האחרים שנוצרו באמצעות AIDL שהוגדרו. אפשר להשתמש ב-List כמחלקה של סוג פרמטר, כמו List<String>. המחלקה הקונקרטית בפועל שהצד השני מקבל היא תמיד ArrayList, למרות שהשיטה נוצרת לשימוש בממשק List.

  • Map

    כל הרכיבים ב-Map חייבים להיות מאחד מסוגי הנתונים הנתמכים ברשימה הזו, או מאחד הממשקים או ה-Parcelables האחרים שנוצרו באמצעות AIDL שהוגדרו. אין תמיכה במפות טיפוסים עם פרמטרים, כמו אלה בפורמט Map<String,Integer>. המחלקה הבטון בפועל שהצד השני מקבל היא תמיד HashMap, אבל השיטה נוצרת לשימוש בממשק Map. כדאי להשתמש ב-Bundle כחלופה ל-Map.

חובה לכלול משפט import לכל סוג נוסף שלא מופיע ברשימה הקודמת, גם אם הוא מוגדר באותה חבילה כמו הממשק.

כשמגדירים את ממשק השירות, חשוב לזכור:

  • ה-methods יכולות לכלול אפס פרמטרים או יותר, והן יכולות להחזיר ערך או ערך מבוטל.
  • לכל הפרמטרים הלא-פרימיטיביים נדרש תג כיוון שמציין את אופן העברת הנתונים: in, out או inout (ראו דוגמה בהמשך).

    כברירת מחדל, ממשקים פרימיטיביים, String, IBinder וממשקים שנוצרו באמצעות AIDL הם in, ואי אפשר לשנות את זה.

    אזהרה: כדאי להגביל את ההוראות למה שבאמת נדרש, כי יצירת פרמטרים היא פעולה יקרה.

  • כל התגובות לקוד שכלולות בקובץ .aidl נכללות בממשק IBinder שנוצר, פרט להערות שמופיעות לפני דפי החשבון של החבילה והייבוא.
  • אפשר להגדיר מחרוזות וקבועים אינסופיים בממשק AIDL, למשל const int VERSION = 1;.
  • קריאות ל-methods נשלחות באמצעות קוד transact(), שבדרך כלל מבוסס על אינדקס method בממשק. בגלל שזה מקשה על ניהול גרסאות, אפשר להקצות באופן ידני את קוד הטרנזקציה ל-method: void method() = 10;.
  • צריך להוסיף הערות לארגומנטים ולסוגי ההחזרה שניתנים לאפס באמצעות @nullable.

קובץ .aidl לדוגמה:

// IRemoteService.aidl
package com.example.android;

// Declare any non-default types here with import statements.

/** Example service interface */
interface IRemoteService {
    /** Request the process ID of this service. */
    int getPid();

    /** Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

שומרים את הקובץ .aidl בספרייה src/ של הפרויקט. כשמפתחים את האפליקציה, הכלים של ה-SDK יוצרים את קובץ הממשק IBinder בתיקייה gen/ של הפרויקט. שם הקובץ שנוצר זהה לשם הקובץ .aidl, אבל עם הסיומת .java. לדוגמה, IRemoteService.aidl יוצר את IRemoteService.java.

אם אתם משתמשים ב-Android Studio, ה-build המצטבר יוצר את הכיתה של ה-binder כמעט באופן מיידי. אם אתם לא משתמשים ב-Android Studio, הכלי Gradle יוצר את הכיתה של ה-binder בפעם הבאה שתיצרו את האפליקציה. בסיום כתיבת הקובץ .aidl, בונים את הפרויקט באמצעות gradle assembleDebug או gradle assembleRelease, כדי שהקוד יוכל לקשר אל המחלקה שנוצרה.

הטמעת הממשק

כשמפתחים את האפליקציה, הכלים של Android SDK יוצרים קובץ ממשק .java שנקרא על שם הקובץ .aidl. הממשק שנוצר כולל מחלקה משנית בשם Stub, שהיא יישום מופשט של ממשק ההורה שלו, כמו YourInterface.Stub, והוא כולל הצהרה על כל השיטות מהקובץ .aidl.

הערה: Stub גם מגדיר כמה שיטות מסייעות, בעיקר asInterface(), שמקבלת IBinder, בדרך כלל זו שמועברת לשיטת הקריאה החוזרת של הלקוח onServiceConnected(), ומחזירה מכונה של ממשק ה-stub. פרטים נוספים על ביצוע ההעברה הזו מופיעים בקטע קריאה לשיטת IPC.

כדי להטמיע את הממשק שנוצר מה-.aidl, מרחיבים את ממשק Binder שנוצר, כמו YourInterface.Stub, ומטמיעים את השיטות שעברו בירושה מהקובץ .aidl.

דוגמה להטמעה של ממשק שנקרא IRemoteService, שמוגדר לפי הדוגמה הקודמת של IRemoteService.aidl, באמצעות מופע אנונימי:

Kotlin

private val binder = object : IRemoteService.Stub() {

    override fun getPid(): Int =
            Process.myPid()

    override fun basicTypes(
            anInt: Int,
            aLong: Long,
            aBoolean: Boolean,
            aFloat: Float,
            aDouble: Double,
            aString: String
    ) {
        // Does nothing.
    }
}

Java

private final IRemoteService.Stub binder = new IRemoteService.Stub() {
    public int getPid(){
        return Process.myPid();
    }
    public void basicTypes(int anInt, long aLong, boolean aBoolean,
        float aFloat, double aDouble, String aString) {
        // Does nothing.
    }
};

עכשיו binder הוא מופע של הכיתה Stub (Binder), שמגדיר את ממשק ה-IPC של השירות. בשלב הבא, המכונה הזו תהיה חשופה ללקוחות כדי שיוכלו לקיים אינטראקציה עם השירות.

חשוב לשים לב למספר כללים כשמטמיעים את ממשק AIDL:

  • אין ערובה שהקריאות הנכנסות יבוצעו ב-thread הראשי, לכן צריך לחשוב על שימוש בכמה threads כבר מההתחלה ולפתח את השירות בצורה שתהיה בטוחה ל-thread.
  • כברירת מחדל, קריאות IPC הן סינכרוניות. אם אתם יודעים שלשירות נדרש יותר ממילי-שניות ספורות כדי להשלים בקשה, אל תפעילו אותו מהשרשור הראשי של הפעילות. יכול להיות שהאפליקציה תתקע, וכתוצאה מכך תופיע ב-Android תיבת הדו-שיח 'האפליקציה לא מגיבה'. קוראים לו משרשור נפרד בלקוח.
  • רק סוגי החריגים שמפורטים במסמכי העזרה של Parcel.writeException() נשלחים בחזרה למבצע הקריאה.

חשיפת הממשק ללקוחות

אחרי שמטמיעים את הממשק של השירות, צריך לחשוף אותו ללקוחות כדי שיוכלו לקשר אליו. כדי לחשוף את הממשק של השירות, מרחיבים את Service ומטמיעים את onBind() כדי להחזיר מופע של המחלקה שמיישם את Stub שנוצר, כמו שמוסבר בקטע הקודם. הנה דוגמה לשירות שחושפ את ממשק הדוגמה IRemoteService ללקוחות.

Kotlin

class RemoteService : Service() {

    override fun onCreate() {
        super.onCreate()
    }

    override fun onBind(intent: Intent): IBinder {
        // Return the interface.
        return binder
    }


    private val binder = object : IRemoteService.Stub() {
        override fun getPid(): Int {
            return Process.myPid()
        }

        override fun basicTypes(
                anInt: Int,
                aLong: Long,
                aBoolean: Boolean,
                aFloat: Float,
                aDouble: Double,
                aString: String
        ) {
            // Does nothing.
        }
    }
}

Java

public class RemoteService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // Return the interface.
        return binder;
    }

    private final IRemoteService.Stub binder = new IRemoteService.Stub() {
        public int getPid(){
            return Process.myPid();
        }
        public void basicTypes(int anInt, long aLong, boolean aBoolean,
            float aFloat, double aDouble, String aString) {
            // Does nothing.
        }
    };
}

עכשיו, כשלקוח, כמו פעילות, קורא ל-bindService() כדי להתחבר לשירות הזה, פונקציית ה-callback‏ onServiceConnected() של הלקוח מקבלת את המופע binder שהוחזר על ידי השיטה onBind() של השירות.

גם ללקוח צריכה להיות גישה לסיווג הממשק. לכן, אם הלקוח והשירות נמצאים באפליקציות נפרדות, באפליקציית הלקוח צריכה להיות עותק של הקובץ .aidl בתיקייה src/, שמניב את הממשק android.os.Binder ומספק ללקוח גישה לשיטות AIDL.

כשהלקוח מקבל את IBinder בקריאה החוזרת onServiceConnected(), הוא צריך לבצע קריאה ל-YourServiceInterface.Stub.asInterface(service) כדי להמיר את הפרמטר המוחזר לסוג YourServiceInterface:

Kotlin

var iRemoteService: IRemoteService? = null

val mConnection = object : ServiceConnection {

    // Called when the connection with the service is established.
    override fun onServiceConnected(className: ComponentName, service: IBinder) {
        // Following the preceding example for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service.
        iRemoteService = IRemoteService.Stub.asInterface(service)
    }

    // Called when the connection with the service disconnects unexpectedly.
    override fun onServiceDisconnected(className: ComponentName) {
        Log.e(TAG, "Service has unexpectedly disconnected")
        iRemoteService = null
    }
}

Java

IRemoteService iRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
    // Called when the connection with the service is established.
    public void onServiceConnected(ComponentName className, IBinder service) {
        // Following the preceding example for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service.
        iRemoteService = IRemoteService.Stub.asInterface(service);
    }

    // Called when the connection with the service disconnects unexpectedly.
    public void onServiceDisconnected(ComponentName className) {
        Log.e(TAG, "Service has unexpectedly disconnected");
        iRemoteService = null;
    }
};

דוגמאות נוספות לקוד זמינות בכיתה RemoteService.java ב- ApiDemos.

העברת אובייקטים דרך IPC

ב-Android 10 (רמת API ‏29 ואילך), אפשר להגדיר אובייקטים מסוג Parcelable ישירות ב-AIDL. בנוסף, יש תמיכה בסוגים שנתמכים כארגומנטים של ממשק AIDL וב-Parcelables אחרים. כך אפשר לחסוך את העבודה הנוספת של כתיבת קוד שרשור ושל כיתה בהתאמה אישית באופן ידני. עם זאת, הפעולה הזו גם יוצרת מבנה בסיסי. אם רוצים להשתמש ב-accessors בהתאמה אישית או בפונקציות אחרות, צריך להטמיע את Parcelable במקום זאת.

package android.graphics;

// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect {
    int left;
    int top;
    int right;
    int bottom;
}

דוגמת הקוד שלמעלה יוצרת באופן אוטומטי מחלקה של Java עם שדות מספרים שלמים left, top, right ו-bottom. כל קוד ה-marshalling הרלוונטי מוטמע באופן אוטומטי, וניתן להשתמש באובייקט ישירות בלי להוסיף הטמעה.

אפשר גם לשלוח כיתה בהתאמה אישית מתהליך אחד לאחר דרך ממשק IPC. עם זאת, ודאו שהקוד של הכיתה זמין בצד השני של ערוץ ה-IPC, ושהכיתה שלכם חייבת לתמוך בממשק Parcelable. חשוב לתמוך ב-Parcelable כי הוא מאפשר למערכת Android לפרק אובייקטים לפרימיטיבים שאפשר לארגן ברצף בתוך תהליכים.

כדי ליצור כיתה בהתאמה אישית שתומכת ב-Parcelable:

  1. מוטמע את הממשק Parcelable במחלקה.
  2. מטמיעים את writeToParcel, שלוקח את המצב הנוכחי של האובייקט וכותב אותו ב-Parcel.
  3. מוסיפים למחלקה שדה סטטי בשם CREATOR, שהוא אובייקט שמטמיע את הממשק Parcelable.Creator.
  4. לבסוף, יוצרים קובץ .aidl שמצהיר על הכיתה שלכם לחלוקה, כפי שמוצג בקובץ Rect.aidl הבא.

    אם אתם משתמשים בתהליך build בהתאמה אישית, אל תוסיפו את הקובץ .aidl ל-build. בדומה לקובץ כותרת בשפת C, קובץ .aidl לא עובר הידור.

AIDL משתמש בשיטות ובשדות האלה בקוד שהוא יוצר כדי לארגן את האובייקטים ולפרק אותם.

לדוגמה, הנה קובץ Rect.aidl כדי ליצור מחלקה Rect שניתנת לחלוקה:

package android.graphics;

// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect;

והנה דוגמה לאופן שבו מחלקה Rect מממשת את הפרוטוקול Parcelable.

Kotlin

import android.os.Parcel
import android.os.Parcelable

class Rect() : Parcelable {
    var left: Int = 0
    var top: Int = 0
    var right: Int = 0
    var bottom: Int = 0

    companion object CREATOR : Parcelable.Creator<Rect> {
        override fun createFromParcel(parcel: Parcel): Rect {
            return Rect(parcel)
        }

        override fun newArray(size: Int): Array<Rect?> {
            return Array(size) { null }
        }
    }

    private constructor(inParcel: Parcel) : this() {
        readFromParcel(inParcel)
    }

    override fun writeToParcel(outParcel: Parcel, flags: Int) {
        outParcel.writeInt(left)
        outParcel.writeInt(top)
        outParcel.writeInt(right)
        outParcel.writeInt(bottom)
    }

    private fun readFromParcel(inParcel: Parcel) {
        left = inParcel.readInt()
        top = inParcel.readInt()
        right = inParcel.readInt()
        bottom = inParcel.readInt()
    }

    override fun describeContents(): Int {
        return 0
    }
}

Java

import android.os.Parcel;
import android.os.Parcelable;

public final class Rect implements Parcelable {
    public int left;
    public int top;
    public int right;
    public int bottom;

    public static final Parcelable.Creator<Rect> CREATOR = new Parcelable.Creator<Rect>() {
        public Rect createFromParcel(Parcel in) {
            return new Rect(in);
        }

        public Rect[] newArray(int size) {
            return new Rect[size];
        }
    };

    public Rect() {
    }

    private Rect(Parcel in) {
        readFromParcel(in);
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(left);
        out.writeInt(top);
        out.writeInt(right);
        out.writeInt(bottom);
    }

    public void readFromParcel(Parcel in) {
        left = in.readInt();
        top = in.readInt();
        right = in.readInt();
        bottom = in.readInt();
    }

    public int describeContents() {
        return 0;
    }
}

ה-marshalling בכיתה Rect הוא פשוט. כדאי לבדוק את השיטות האחרות ב-Parcel כדי לראות את סוגי הערכים האחרים שאפשר לכתוב ל-Parcel.

אזהרה: חשוב לזכור את ההשלכות של קבלת נתונים מתהליכים אחרים על האבטחה. במקרה הזה, Rect קורא ארבעה מספרים מ-Parcel, אבל אתם צריכים לוודא שהם נמצאים בטווח הערכים הקבילים לכל מה שהמבצע מנסה לעשות. מידע נוסף על שמירה על אבטחת האפליקציה מפני תוכנות זדוניות זמין במאמר טיפים לאבטחה.

שיטות עם ארגומנטים בחבילות שמכילות 'פריטי מגרשים'

אם שיטת קריאה מקבלת אובייקט Bundle שצפוי להכיל אובייקטים שניתן לחלק, חשוב להגדיר את מעבד הטעינה של ה-Bundle על ידי קריאה ל-Bundle.setClassLoader(ClassLoader) לפני שמנסים לקרוא מה-Bundle. אחרת, תקבלו את השגיאה ClassNotFoundException גם אם ה-Parcelable מוגדר בצורה נכונה באפליקציה.

לדוגמה, שימו לב לקובץ .aidl לדוגמה הבא:

// IRectInsideBundle.aidl
package com.example.android;

/** Example service interface */
interface IRectInsideBundle {
    /** Rect parcelable is stored in the bundle with key "rect". */
    void saveRect(in Bundle bundle);
}
כפי שמוצג בהטמעה הבאה, הערך של ClassLoader מוגדר במפורש ב-Bundle לפני קריאת Rect:

Kotlin

private val binder = object : IRectInsideBundle.Stub() {
    override fun saveRect(bundle: Bundle) {
      bundle.classLoader = classLoader
      val rect = bundle.getParcelable<Rect>("rect")
      process(rect) // Do more with the parcelable.
    }
}

Java

private final IRectInsideBundle.Stub binder = new IRectInsideBundle.Stub() {
    public void saveRect(Bundle bundle){
        bundle.setClassLoader(getClass().getClassLoader());
        Rect rect = bundle.getParcelable("rect");
        process(rect); // Do more with the parcelable.
    }
};

קריאה לשיטת IPC

כדי לקרוא לממשק מרוחק שהוגדר באמצעות AIDL, מבצעים את השלבים הבאים בכיתה שבה מתבצעת הקריאה:

  1. כוללים את הקובץ .aidl בספרייה src/ של הפרויקט.
  2. מגדירים מופע של הממשק IBinder שנוצר על סמך ה-AIDL.
  3. מטמיעים את ServiceConnection.
  4. קוראים ל-Context.bindService() ומעבירים את ההטמעה של ServiceConnection.
  5. בהטמעה של onServiceConnected(), מקבלים מכונה של IBinder שנקראת service. קוראים ל-YourInterfaceName.Stub.asInterface((IBinder)service) כדי להמיר את הפרמטר המוחזר לסוג YourInterface.
  6. קריאה ל-methods שהגדרתם בממשק. תמיד צריך לזהות חריגות מסוג DeadObjectException, שמתרחשות כשהחיבור נקטע. בנוסף, צריך לזהות חריגות מסוג SecurityException, שמתרחשות כשיש הגדרות AIDL סותרות בשני התהליכים שמעורבים בקריאה לשיטת ה-IPC.
  7. כדי להתנתק, צריך להפעיל Context.unbindService() עם המכונה של הממשק.

חשוב לזכור את הנקודות הבאות כשקוראים לשירות IPC:

  • האובייקטים נספרים לפי הפניות בכל התהליכים.
  • אפשר לשלוח אובייקטים אנונימיים בתור ארגומנטים של שיטות.

מידע נוסף על קישור לשירות זמין בסקירה הכללית על שירותים מקושרים.

הנה קוד לדוגמה שמדגים קריאה לשירות שנוצר על ידי AIDL, שנלקח מדוגמת השירות המרוחק בפרויקט ApiDemos.

Kotlin

private const val BUMP_MSG = 1

class Binding : Activity() {

    /** The primary interface you call on the service.  */
    private var mService: IRemoteService? = null

    /** Another interface you use on the service.  */
    internal var secondaryService: ISecondary? = null

    private lateinit var killButton: Button
    private lateinit var callbackText: TextView
    private lateinit var handler: InternalHandler

    private var isBound: Boolean = false

    /**
     * Class for interacting with the main interface of the service.
     */
    private val mConnection = object : ServiceConnection {

        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            // This is called when the connection with the service is
            // established, giving us the service object we can use to
            // interact with the service.  We are communicating with our
            // service through an IDL interface, so get a client-side
            // representation of that from the raw service object.
            mService = IRemoteService.Stub.asInterface(service)
            killButton.isEnabled = true
            callbackText.text = "Attached."

            // We want to monitor the service for as long as we are
            // connected to it.
            try {
                mService?.registerCallback(mCallback)
            } catch (e: RemoteException) {
                // In this case, the service crashes before we can
                // do anything with it. We can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
            }

            // As part of the sample, tell the user what happened.
            Toast.makeText(
                    this@Binding,
                    R.string.remote_service_connected,
                    Toast.LENGTH_SHORT
            ).show()
        }

        override fun onServiceDisconnected(className: ComponentName) {
            // This is called when the connection with the service is
            // unexpectedly disconnected&mdash;that is, its process crashed.
            mService = null
            killButton.isEnabled = false
            callbackText.text = "Disconnected."

            // As part of the sample, tell the user what happened.
            Toast.makeText(
                    this@Binding,
                    R.string.remote_service_disconnected,
                    Toast.LENGTH_SHORT
            ).show()
        }
    }

    /**
     * Class for interacting with the secondary interface of the service.
     */
    private val secondaryConnection = object : ServiceConnection {

        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            // Connecting to a secondary interface is the same as any
            // other interface.
            secondaryService = ISecondary.Stub.asInterface(service)
            killButton.isEnabled = true
        }

        override fun onServiceDisconnected(className: ComponentName) {
            secondaryService = null
            killButton.isEnabled = false
        }
    }

    private val mBindListener = View.OnClickListener {
        // Establish a couple connections with the service, binding
        // by interface names. This lets other applications be
        // installed that replace the remote service by implementing
        // the same interface.
        val intent = Intent(this@Binding, RemoteService::class.java)
        intent.action = IRemoteService::class.java.name
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
        intent.action = ISecondary::class.java.name
        bindService(intent, secondaryConnection, Context.BIND_AUTO_CREATE)
        isBound = true
        callbackText.text = "Binding."
    }

    private val unbindListener = View.OnClickListener {
        if (isBound) {
            // If we have received the service, and hence registered with
            // it, then now is the time to unregister.
            try {
                mService?.unregisterCallback(mCallback)
            } catch (e: RemoteException) {
                // There is nothing special we need to do if the service
                // crashes.
            }

            // Detach our existing connection.
            unbindService(mConnection)
            unbindService(secondaryConnection)
            killButton.isEnabled = false
            isBound = false
            callbackText.text = "Unbinding."
        }
    }

    private val killListener = View.OnClickListener {
        // To kill the process hosting the service, we need to know its
        // PID.  Conveniently, the service has a call that returns
        // that information.
        try {
            secondaryService?.pid?.also { pid ->
                // Note that, though this API lets us request to
                // kill any process based on its PID, the kernel
                // still imposes standard restrictions on which PIDs you
                // can actually kill. Typically this means only
                // the process running your application and any additional
                // processes created by that app, as shown here. Packages
                // sharing a common UID are also able to kill each
                // other's processes.
                Process.killProcess(pid)
                callbackText.text = "Killed service process."
            }
        } catch (ex: RemoteException) {
            // Recover gracefully from the process hosting the
            // server dying.
            // For purposes of this sample, put up a notification.
            Toast.makeText(this@Binding, R.string.remote_call_failed, Toast.LENGTH_SHORT).show()
        }
    }

    // ----------------------------------------------------------------------
    // Code showing how to deal with callbacks.
    // ----------------------------------------------------------------------

    /**
     * This implementation is used to receive callbacks from the remote
     * service.
     */
    private val mCallback = object : IRemoteServiceCallback.Stub() {
        /**
         * This is called by the remote service regularly to tell us about
         * new values.  Note that IPC calls are dispatched through a thread
         * pool running in each process, so the code executing here is
         * NOT running in our main thread like most other things. So,
         * to update the UI, we need to use a Handler to hop over there.
         */
        override fun valueChanged(value: Int) {
            handler.sendMessage(handler.obtainMessage(BUMP_MSG, value, 0))
        }
    }

    /**
     * Standard initialization of this activity.  Set up the UI, then wait
     * for the user to interact with it before doing anything.
     */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.remote_service_binding)

        // Watch for button taps.
        var button: Button = findViewById(R.id.bind)
        button.setOnClickListener(mBindListener)
        button = findViewById(R.id.unbind)
        button.setOnClickListener(unbindListener)
        killButton = findViewById(R.id.kill)
        killButton.setOnClickListener(killListener)
        killButton.isEnabled = false

        callbackText = findViewById(R.id.callback)
        callbackText.text = "Not attached."
        handler = InternalHandler(callbackText)
    }

    private class InternalHandler(
            textView: TextView,
            private val weakTextView: WeakReference<TextView> = WeakReference(textView)
    ) : Handler() {
        override fun handleMessage(msg: Message) {
            when (msg.what) {
                BUMP_MSG -> weakTextView.get()?.text = "Received from service: ${msg.arg1}"
                else -> super.handleMessage(msg)
            }
        }
    }
}

Java

public static class Binding extends Activity {
    /** The primary interface we are calling on the service. */
    IRemoteService mService = null;
    /** Another interface we use on the service. */
    ISecondary secondaryService = null;

    Button killButton;
    TextView callbackText;

    private InternalHandler handler;
    private boolean isBound;

    /**
     * Standard initialization of this activity. Set up the UI, then wait
     * for the user to interact with it before doing anything.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.remote_service_binding);

        // Watch for button taps.
        Button button = (Button)findViewById(R.id.bind);
        button.setOnClickListener(mBindListener);
        button = (Button)findViewById(R.id.unbind);
        button.setOnClickListener(unbindListener);
        killButton = (Button)findViewById(R.id.kill);
        killButton.setOnClickListener(killListener);
        killButton.setEnabled(false);

        callbackText = (TextView)findViewById(R.id.callback);
        callbackText.setText("Not attached.");
        handler = new InternalHandler(callbackText);
    }

    /**
     * Class for interacting with the main interface of the service.
     */
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // This is called when the connection with the service is
            // established, giving us the service object we can use to
            // interact with the service.  We are communicating with our
            // service through an IDL interface, so get a client-side
            // representation of that from the raw service object.
            mService = IRemoteService.Stub.asInterface(service);
            killButton.setEnabled(true);
            callbackText.setText("Attached.");

            // We want to monitor the service for as long as we are
            // connected to it.
            try {
                mService.registerCallback(mCallback);
            } catch (RemoteException e) {
                // In this case the service crashes before we can even
                // do anything with it. We can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
            }

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_connected,
                    Toast.LENGTH_SHORT).show();
        }

        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service is
            // unexpectedly disconnected&mdash;that is, its process crashed.
            mService = null;
            killButton.setEnabled(false);
            callbackText.setText("Disconnected.");

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_disconnected,
                    Toast.LENGTH_SHORT).show();
        }
    };

    /**
     * Class for interacting with the secondary interface of the service.
     */
    private ServiceConnection secondaryConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // Connecting to a secondary interface is the same as any
            // other interface.
            secondaryService = ISecondary.Stub.asInterface(service);
            killButton.setEnabled(true);
        }

        public void onServiceDisconnected(ComponentName className) {
            secondaryService = null;
            killButton.setEnabled(false);
        }
    };

    private OnClickListener mBindListener = new OnClickListener() {
        public void onClick(View v) {
            // Establish a couple connections with the service, binding
            // by interface names. This lets other applications be
            // installed that replace the remote service by implementing
            // the same interface.
            Intent intent = new Intent(Binding.this, RemoteService.class);
            intent.setAction(IRemoteService.class.getName());
            bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
            intent.setAction(ISecondary.class.getName());
            bindService(intent, secondaryConnection, Context.BIND_AUTO_CREATE);
            isBound = true;
            callbackText.setText("Binding.");
        }
    };

    private OnClickListener unbindListener = new OnClickListener() {
        public void onClick(View v) {
            if (isBound) {
                // If we have received the service, and hence registered with
                // it, then now is the time to unregister.
                if (mService != null) {
                    try {
                        mService.unregisterCallback(mCallback);
                    } catch (RemoteException e) {
                        // There is nothing special we need to do if the service
                        // crashes.
                    }
                }

                // Detach our existing connection.
                unbindService(mConnection);
                unbindService(secondaryConnection);
                killButton.setEnabled(false);
                isBound = false;
                callbackText.setText("Unbinding.");
            }
        }
    };

    private OnClickListener killListener = new OnClickListener() {
        public void onClick(View v) {
            // To kill the process hosting our service, we need to know its
            // PID.  Conveniently, our service has a call that returns
            // that information.
            if (secondaryService != null) {
                try {
                    int pid = secondaryService.getPid();
                    // Note that, though this API lets us request to
                    // kill any process based on its PID, the kernel
                    // still imposes standard restrictions on which PIDs you
                    // can actually kill.  Typically this means only
                    // the process running your application and any additional
                    // processes created by that app as shown here. Packages
                    // sharing a common UID are also able to kill each
                    // other's processes.
                    Process.killProcess(pid);
                    callbackText.setText("Killed service process.");
                } catch (RemoteException ex) {
                    // Recover gracefully from the process hosting the
                    // server dying.
                    // For purposes of this sample, put up a notification.
                    Toast.makeText(Binding.this,
                            R.string.remote_call_failed,
                            Toast.LENGTH_SHORT).show();
                }
            }
        }
    };

    // ----------------------------------------------------------------------
    // Code showing how to deal with callbacks.
    // ----------------------------------------------------------------------

    /**
     * This implementation is used to receive callbacks from the remote
     * service.
     */
    private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {
        /**
         * This is called by the remote service regularly to tell us about
         * new values.  Note that IPC calls are dispatched through a thread
         * pool running in each process, so the code executing here is
         * NOT running in our main thread like most other things. So,
         * to update the UI, we need to use a Handler to hop over there.
         */
        public void valueChanged(int value) {
            handler.sendMessage(handler.obtainMessage(BUMP_MSG, value, 0));
        }
    };

    private static final int BUMP_MSG = 1;

    private static class InternalHandler extends Handler {
        private final WeakReference<TextView> weakTextView;

        InternalHandler(TextView textView) {
            weakTextView = new WeakReference<>(textView);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case BUMP_MSG:
                    TextView textView = weakTextView.get();
                    if (textView != null) {
                        textView.setText("Received from service: " + msg.arg1);
                    }
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }
}