יום שני, נובמבר 22

Service חלק 2: IPC

בפוסט שעסק בנושא ה-Service צויין שה-service משמש לשתי מטרות: האחת ביצוע פעולות ב-background והשניה ביצוע תיקשורת בין Processes או בקיצור IPC או בעברית:   IPC - Inter Process Communication. אותו מושג ידוע במערכות אחרות גם בשם RPC - Remote Process Calls.

באותו פוסט קודם על ה-Service, התרכזנו בעניין הראשון (הפעולה ב- background), והדגמנו  Service שמבצע פעולות ארוכות. ה-service בשונה מ-Activity יכול להמשיך לבצע עבודתו ברקע גם אם ה-Activity יתחלף.
כעת נעסוק ביעוד השני אותו משמש ה-Service:  ה- IPC. 
מה זה IPC ולמלה בכלל הוא נחוץ? נתחיל קודם בציון העובדה שכל אפליקציה של האנדרואיד רצה ב- Process נפרד של מערכת ההפעלה.
בעצם, למען הבהירות, אולי נלך עוד צעד אחורה ונזכיר שמדובר במערכת ההפעלה לינוקס, שבה כל Process מהווה יחידה נפרדת עם משאבי מערכת מופרדים (המושג "משאבים" מתכוון למרחב הזיכרון, Threads,  Semphores, וכיו"ב). החציצה בין ה-Process חוסמת בין שהשאר את הגישה לזיכרון של Process אחר וגם את הגישה ל-API של המתודות מה-Process האחר. להפרדה זו יתרונות רבים שעיקרם ביטול השפעות שליליות בין ה-Process כך שבמקרה שאפליקציה אחת "תתברבר" האפליקציות האחרות יהיו מוגנות. 
יחד עם זאת, קיים בהרבה מקרים הצורך לאינטראקציה בין אפליקציות. תיאורטית היה ניתן לאפשר אינטראקציה כזו ע"י מעקף של החציצה, למשל העברת מידע דרך קובץ משותף. מצד שני, שיטות "לא נקיות" מעין אלה הן מקור לבעיות. במקום זה, אנדרואיד מספק מנגנון למימוש IPC ה-AIDL. מכאן ועד סוף הפוסט נסביר ונדגים את המנגנון הזה. אציין שהמנגנון הזה מתאים גם לתקשורת בין Activities ל- Service של אותה אפליקציה = אותו Process.
AIDL - הכלי של Android לביצוע התקשורת הוא ה- AIDL - Android Interface Definition Language, בעזרתו בונים ממשק (Interface) ל- Service. ע"י שימוש בממשק זה, יכולים אובייקטים מכל אפליקציה להתחבר (פעולת Bind). כדי לתת מושג ראשוני על הממשק, נציג דוגמא לקובץ ה- AIDL שעל המפתח לבנות. הנה קובץ לדוגמא:

שם הקובץ: IAidlFileDemo.aidl

interface IAidlFileDemo {
     int appSet(int v1, in String v2);
     int appGet(int v1, int v2);
}

הקובץ הנ"ל מגדיר Interface לשתי מתודות:
appSet ו-appGet.
 
 
של ה-Service אותן יהיה ניתן להפעיל מכל אפליקציה שתעשה פעולת Bind לאותו Service כפי שראה בהמשך. הפרמטרים שהועברו בפונקציות הנ"ל הם מסוג Integer ו- String. סוגי הפרמטרים בהם מותר להשתמש כוללים את כל ה- Java primitives כלומר Integer, Boolean, Byte, Char, Float, Long וכו', String, CharSequence, List, Map. כמוכ אפשר להעביר Parcelable ,Objects (נעסוק ב-Parcelable Objects בפוסט נפרד). פרמטרים שאינם משתנים פרימיטיביים חייבים את הסימון in, out או inout בהתאם לשימושם כ-input, output או גם וגם. משתנים פרימיטיביים לא צריכים את הסימון, ובכל אופן הם יכולים להיות אך ורק Input.

עם בניית הקובץ xxx.aidl ושמירתו תחת src עם קבצי ה-java, המערכת תייצר קובץ java באותו שם תחת הספריה gen. הקובץ ייוצר באופן אוטומטי בסביבת Eclipse, אחרת צריך להפעיל את  הפקודה aidl.

קובץ ה-java שנוצר בנה את ה-Interface הדרוש בעצמו! לא נכנס בפוסט זה לתוכן קובץ ה-Interface שנוצר, ונתרכז בהוראות  שמטרתן לצור את ממשק ה-IPC בקלות - עד כמה שזה ניתן. אכן יש לתהליך הזה קצת overhead אבל ננסה להבהיר אותו תוך כדי שימוש בדוגמא פשוטה.
הדוגמא מתבססת על האפליקציה של הפוסט הקודם על ה-service, עליו מומלץ לעבור קודם בכל מקרה.
מדובר בשני c: myMainActivity ו- myIpcService. ה- Service מבצע Loop ארוך, תוך שימוש ב-Thread עצמאי. בפוסט הקודם הדגמנו הפסקה והפעלה של ה-Service ע"י ה- Activity תוך שימוש בכפתורים. הפעם למסך של ה- Activity יהיו שני כפתורים להפעלת פונקציות של ה-Service שיופעלו ע"י ה- IPC. מעתה נשתדל להתרכז רק בפעולות הדרושות לבניית ה- AIDL, בהנחה ששאר הקוד כבר מוכר מההסבר הקודם.
 
 
 
נתחיל במלאכת הבישול:
שלב 1: בניית קובץ AIDL.
הקובץ שלנו: ImyIpcServiceInterface.aidl
package com.ahs.androheb.ipc;
interface ImyIpcServiceInterface {
       void runLoop(int loopCount);
       int indexGet();
      }
הקובץ מכיל שתי מתודות הממומשות באובייקט ה-service אותן ברצוננו לחשוף. הראשונה מפעילה את המתודה שעושה loop .  ה-פרמטר  loopCount קובע את מספר הפעמים שה-loop יתבצע. המתודה השניה מקבלת את הערך הרגעי של ה-loop counter. (הערך שיתקבל ישלח לתצוגה ב-UI).
ואם כבר מדברים על UI הנה  צילום של התצוגה:





שלב 2: טיפול בקובץ של ה- Service.
2.א. צריך לשתול שם instance שם ה-stub שנוצר אוטומטית בקובץ הממשק ImyIpcServiceInterface.java, ובתוכו לממש את הפונקציות אותן הגדרנו בקובץ ה-AIDL. בקיצור, נוסיף לקובץ myIpcService.java בתוך  myIpcService class  את השורות:
 
  1. private final ImyIpcServiceInterface.Stub binder = new                    ImyIpcServiceInterface.Stub() {
  2.         public int indexGet() {
  3.             return idx;
  4.        }
  5.         public void runLoop(int loopCount){
  6.               if(!isThreadOn)
  7.               {
  8.                Toast.makeText(myIpcService.this, "Starting Loop ...", Toast.LENGTH_LONG).show();
  9.                loopSize = loopCount;
  10.                   isThreadOn = true;
  11.                  SumCalc sumCalc = new SumCalc();
  12.                   sumCalc.start();
  13.              }
  14.               else
  15.                Toast.makeText(myIpcService.this, "Loop already running ...", Toast.LENGTH_LONG).show();
  16.          }
  17.      };
הסבר:
שורה 1: יצירת אובייקט binder מסוג  ImyIpcServiceInterface.Stub. כש ImyIpcServiceInterface הוא שם קובץ ה-AIDL שבנינו בשלב 1.
שורות 2 ו-5: מימוש המתודות של ה- Interface.


 שלב 2.ב: שינוי מתודת ה-onBind (שאותה כידוע חייבים לממש בכל service). שתלו את המתודה הבאה בקובץ:
  @Override
    public IBinder onBind(Intent intent) {
        return this.binder;
    }
המתודה הבאה היא האמצעי דרכו יוכלו האובייקטים החיצוניים להתחבר, כשהאובייקט binder - ראה 2.א למעלה - יושתל בתוכם. סיימנו עם הטיפול באובייקט ה-service. נעבור לשלב השלישי, אותו צריך לבצע עבור כל אובייקט שיתחבר ל-service כלומר ל-clients.
 
שלב 3: טיפול ב- clients: האובייקטים שמעוניינים להתממשק ל-service:
3 א צור אובייקט ממשק מהסוג שנוצר עפ"י ה-aidl. עשה זאת ע"י הכנסת השורה הבאה ל-client, במקרה שלנו ל-myMainActivity:
public ImyIpcServiceInterface remoteService;
3.ב שתול את האובייקט הבא בתוך האובייקט. במקרה שלנו לתוך myMainActivity.

  1.        ServiceConnection connection = new ServiceConnection() {
  2.     public void onServiceConnected(ComponentName className, IBinder iservice) {
  3.         remoteService = ImyIpcServiceInterface.Stub.asInterface(iservice);
  4.     }
  5.     public void onServiceDisconnected(ComponentName className) {
  6.         remoteService = null;
  7.     }};

קטע הקוד הנ"ל יוצר אובייקט בשם connection (זה השם שבחרנו) של ה ServiceConnection class.
ה-class הנ"ל מכיל שני callbacks שמופעלים האחד עם יצירת החיבור ל- service (שורה 2) והשני עם ניתוקו (שורה 5). השמות בכחול - הוגדרו על ידנו.
 
3.ג.  הוסף את המתודה bindService להתחברות ו-unBindService להתנתקות מה-service. באוביקט שלנו הן הוספו במתודות ה-onStart ו-onPause בהתאמה (שני callback עם override).
נציג כאן את שתי המתודות:
  1.     @Override
  2.     public void onStart() {
  3.          super.onStart();
  4.           bindService(new Intent(myMainActivity.this, myIpcService.class),
  5.             this.connection, Context.BIND_AUTO_CREATE);  
  6.      }
  7.     @Override
  8.      public void onPause() {
  9.          super.onPause();
  10.          unbindService(this.connection);
  11.     }
3 הפרמטרים של bindService (שורה 5): 

1. ה-Intent ל-Service.
2. ה- conn- זה האוביקט שיצרנו, שכולל את ה-callbacks ביצירת והתרת הקשר ל-service.
3. דגלים: יכול לקבל את הערך 0 או BIND_AUTO_CREATE שמשמעותו: אם ה-Service לא מופעל, הפעל את onCreate שלו אוטומטית.

bindService מחזיר true אם יצירת הקשר הצליחה - אחרת מחזיר false.
הפרמטר של unbindService (שורה 11): ה- conn בלבד.
 

התבשיל מוכן. לגבי ה-bindService: אין הגבלה על מספר האובייקטים שיכולים לעשות bind ל-service. מצד שני, כשכל האובייקטים שעשו bind יפסיקו לפעול וה-service ישאר ללא קליינטים, מערכת ההפעלה תהרוג אותו. אם רוצים  למנוע זאת, יש להפעיל גם את startService. באפליקציה הנ"ל startService מופעל ב-onCreate. גורם נוסף שיש לקחת בחשבון הוא העומס על משאבי המערכת שנגרם ע"י ה- service, כך שאם אין צורך בו, צריך לעצור אותו.
...קישור להורדת קבצי הפרויקט.

2 תגובות:

  1. רונן שלום,

    לא הבנתי לגמרי מתי יש לעשות שימוש ב-IPC ו-AIDL.
    נתקלתי בשתי פרוייקטים באותו תחום המיישמים SERVICE וACTIVITIES. האחד מיישם מממשקי AIDL ו-Parcelable (שלא הבנתי למה צריך) והשני משתמש ב-callbacks להעביר events מה-SERVICE ל-ACTIVITY הרלוונטי.
    שיטת ה-Callbacks נראית יותר פשוטה וברורה.
    אם כך למה צריך את ה-IPC ו-AIDL.

    תודה,
    ירון

    השבמחק
  2. היי,

    לפי הבנתי לא נהוג להשתמש במגנון ה-BIND עבור IPC/RPC אלה במנגנון ה-INTENTS. דווקא ה-BIND מתאים (גם לפי ההיגיון שלי) לדוגמא עבור שימוש בשירות שהוגדר "פרטי" לאפליקציה שלך - כמובן שאין מן הנמנע לעשות זאת אבל יכול להיות שזאת א הפרקטיקה הנכונה ?

    השבמחק