יום שבת, נובמבר 20

Service

הגדרנו את המושג Service באחד הפוסטים הראשונים כפעילות ללא ממשק למשתמש. ל-Service יש מחזור חיים שונה משל ה-Activity, כך שגם אם המסך של ה-Activity שהפעיל את ה-Service יורד מהתצוגה, ה-Service יכול להמשיך לפעול. זאת בשונה מה-Activity שכידוע, עלול להיות מחוסל אם אינו מופיע ב-Foreground של התצוגה. דוגמא לשימוש אפשרי ב-service: נגן. נרצה שהנגן ימשיך לפעול גם אם המסך שהפעיל אותו כבר ירד מהתצוגה. אחת המתודות של ה-Service Class בה רצוי להשתמש במצב כזה היא ה-startForeground. המתודה הזו מפעילה דגל שמודיע שמסמן לא להרוג את ה-service אחרת תפגע פעולתו.
ה-Service למעשה יכול לשמש אמצעי למענה על שני צרכים של המערכת:
הצורך הראשון, ביצוע פעילות רקע של המערכת (background) שאינה קשורה ישירות לתצוגה שב-UI. 
הצורך השני, תקשורת  בין  Processes שונים במערכת 
(IPC - Inter Process Communication או בשם אחר שנפוץ בהרבה מערכות הפעלה RPC - Remote Process Communication) .
מה זה IPC ולשם מה בכלל הוא נחוץ? על זה נדון בפוסט  שייוחד ל-IPC.
בפוסט הנוכחי נתרכז בשימוש הראשון של ה-service עליו דיברנו: הרצת פעילויות ברקע.
 
אחת הנקודות החשובות אותה הייתי רוצה לדגיש היא שה- Service במהותו אינו "background task" במובן שהוא באופן אוטומטי רץ ב-Thread שונה מזה של ה-Activity שהפעיל אותו. בכלל לא - ה-service ירוץ באותו Thread שהפעיל אותו, בד"כ ה- UI Thread, ולפיכך הוא לא יוריד את העומס מה- Thread הראשי,  (offload).
מצד שני, ה-Service  אכן יכול להריץ את פעילותו  ב-Thread עצמאי, אותו יש לצור, ובכך להוריד עומס (= בעברית לעשות Offload ), מה- UI Thread. 
 
נציג דוגמא מעניינת לשימוש ב-service.
האפליקציה מכילה שני classes: ה- myActivator שהוא subclass של  Activity , המטפל ב-UI ו myService שהוא subclass של Service שיופעל ע"י ה- Activity. ה-Service  יבצע ברקע פעילות אינטנסיבית -  Loop באורך מליון, תוך כדי חיבור את כל המספרים מ1 עד מיליון.הפעילות הזו תתבצע ב-Thread עצמאי כך שה-UI יהיה זמין למשתמש כל הזמן. כל זה יומחש בזמן הריצה בעזרת הודעות על המסך, כולל נושא ה-LifeCycle של ה-Service.
למרות שמיותר, אזכיר את הקישור להורדת קבצי הפרויקט שנמצא בתחתית העמוד.
אני מציע שנעיף מבט על התוכנית, ונתחיל עם ה-Service. בתוך ה-class ממומשות סה"כ 4 מתודות + class קטן מסוג Thread. תוכן הקובץ של ה-Service מובא כאן ומיד אחריו הסברים:


  1. public class myService extends Service{
  2.     Integer sum = 0;
  3.     boolean isThreadOn = false;
  4.     public final String TAG = "myService";
  5.    
  6.     @Override
  7.         public void onCreate() {
  8.               super.onCreate();
  9.                 Toast.makeText(this,"onCreate", Toast.LENGTH_LONG).show();
  10.                Log.d(TAG," onCreate");
  11.         }
  12.  
  13.     @Override
  14.     public int onStartCommand(Intent intent, int flags, int startId) {
  15.          if(!isThreadOn)
  16.          {
  17.              isThreadOn = true;
  18.              SumCalc sumCalc = new SumCalc();
  19.              sumCalc.start();
  20.                  Toast.makeText(this,"onStartCommand. Run New Thread", Toast.LENGTH_LONG).show();
  21.         }
  22.          else
  23.               Toast.makeText(this,"onStartCommand. sum is:" + sum, Toast.LENGTH_LONG).show();
  24.       
  25.         return START_STICKY;
  26.     }
  27.     @Override
  28.     public void onDestroy() {
  29.           super.onDestroy();
  30.           Toast.makeText(this, "The Service was destroyed ...", Toast.LENGTH_LONG).show();
  31.             Log.d(TAG," onDestroy");
  32.     }
  33.    
  34.     @Override
  35.     public IBinder onBind(Intent arg0) {
  36.           return null;
  37.     }
  38.    
  39.     public class SumCalc extends Thread {
  40.         public void run() {
  41.             sum = 0;
  42.             for(Integer idx = 0; idx< 1000000; idx ++)
  43.             {
  44.                  sum++;
  45.             }
  46.             isThreadOn = false;
  47.         }
  48.     }
  49.    
  50. }




3 מתודות הן callbacks הקשורות ל-Lifecycle של ה- Service:
  1. onCreate-מופעלת בפעם אחת בלבד עם יצירת האובייקט, כתוצאה מהפעלת startService. הפעלות נספות של startService לא יפעילו את onCreate אלא אם ה-Servcie נעצר, למשל עקב stopService.
  2. onStartCommand- מופעלת עבור כל הפעלה  של- startServce. 
  3.  onDestroy - מופעלת כ-callback של stopService.
 כל אחת מ-3 ה-callback הנ"ל מדפיס הודעות למסך, בעזרתן ניתן לעקוב אחרי ה-LifeCycle של ה-Service.
ה-Service אמנם לא קשור ל-UI אך ניתן להציג הודעות תוך שימוש ב-Toast class. ה-Toast Notification היא הודעה שקופצת על המסך לזמן מוגבל ונעלמת. היא יכולה להשלח מ- Service או מ-Activity. 

ב-onCreate (שורה 7) אין שום דבר מיוחד. כאמור יש Toast Notification אותה נראה רק בהפעלה הראשונה של ה-Service או אחרי שנהרוס אותו. אגב, שורה 10 מכילה הדפסת הודעה ל- Logger. אפשר לראות הודעות אלה ב- IDE תוך שימוש ב- LogCat.

ה- onDestroy גם הוא טריוויאלי. גם הוא כולל הדפסת Toast Notification.
 
 נעבור ל-callBack השלישי והאחרון : onStartCommand, שהוא המקבילה של onStart ב- Activity, ומופעל בכל פעם שמפעילים את startService. המימוש אצלנו כולל הפעלת ה- Thread שמבצע את ה-Loop, והדפסת הודעות למסך. נציג את המתודה שוב:
  1. public int onStartCommand(Intent intent, int flags, int startId) {
  2.          if(!isThreadOn)
  3.          {
  4.              isThreadOn = true;
  5.              SumCalc sumCalc = new SumCalc();
  6.              sumCalc.start();
  7.                  Toast.makeText(this,"onStartCommand. Run New Thread", Toast.LENGTH_LONG).show();
  8.         }
  9.          else
  10.               Toast.makeText(this,"onStartCommand. sum is:" + sum, Toast.LENGTH_LONG).show();
  11.       
  12.         return START_STICKY;
  13.     }
בשורה 2 בודקים את הדגל isThreadOn. אם הוא במצב true, ה-Thread רץ ברקע ולא ניצור instance נוסף שלו. אחרת: ניצור instance חדש של ה-Thread ונריץ אותו (שורות 5,6). שורה 4 משנה את מצב הדגל לסמן שה-Thread רץ (כאמןר, כדי לא להריץ אותו יותר מפעם בו זמנית), ושורה 8 מדפיסה למסך הודעה מתאימה.
אם הדגל isThreadOn היה במצב true, לא היינו מבצעים דבר מלבד הדפסת המשתנה sum: ערכו הרגעי של הסכום המחושב ע"י ה-Thread. (שורה 10).


את המתודה הרביעית, onBind, חייבים לממש בכל Service. היא קשורה לנושא ה-IPC - Inter Process Communication, כך שנסביר את תפקידה בפוסט על ה-IPC. בכל אופן, היות שאצלינו אין שימוש ב- IPC היא מחזירה false תמיד.
בנוסף ל4 המתודות, שורה 39, class ה- -Thread בו מתבצע ה-loop.

 שורה 13: סיום המתודה.   return START_STICKY; משמעות ה START_STICKY היא, שבמקרה שה-service יחוסל (killed) אחרי הפעלתו, הוא ישאר במצב "started". מאוחר יותר שה-service יוחזר לתפקוד, ו-onCreate תופעל שוב, יופעל גם ה-onStartCommand. לעומת זאת קיימים ערכים אחרים לחזרה למשל:

START_NOT_STICK: במקרה שה-service יופעל לאחר שהרגו אותו, ה- onStartCommand לא יופעל שוב, אלה אם יש בתור פקודות onStartCommand ממתינות.


ולסיום, ה-Thread: לופ פשוט שמתבצע מיליון פעם (42-45). בסוף הביצוע הדגל isThreadOn מועבר מצב כדי לאפשר הפעלה נוספת של ה-Thread.
  1. public class SumCalc extends Thread {
  2.         public void run() {
  3.             sum = 0;
  4.             for(Integer idx = 0; idx< 1000000; idx ++)
  5.             {
  6.                  sum++;
  7.             }
  8.             isThreadOn = false;
  9.         }
  10.     }
  11.    
  12. }
סיימנו עם ה-service subclass ונעבור ל- Activity subclass האחראי על ה-UI, וממנו מופעל ה- Service ומכאן שמו: myActivator. הוא באמת טריויאלי - בטח למי שעיין לפחות במקצת מהפוסטים הקודמים.
מוגדרים בו שני כפתורים + callback לכל אחד מהם.
עבור כפתור Service Start מבצע ה-callback של הכפתור הפעלה של ה-service (שורה 3):
  1.             buttonServiceStart.setOnClickListener(new OnClickListener() {
  2.                 public void onClick(View v){
  3.                      startService(new Intent(myActivator.this,myService.class));
  4.                 }               
  5.             });
ההפעלה מתבצעת ע"י Intent שנשלח ל - myService.class.
 
הפעולות שמתבצעות עם לחיצה על ה- Stop Service מתוארות בשורות הבאות:
  1.             buttonServiceDestroy.setOnClickListener(new OnClickListener() {
  2.                 public void onClick(View v){
  3.                     stopService(new Intent(myActivator.this,myService.class));
  4.                 }               
  5.             });


ושוב, קריאה ל- stopService ששולח Intent (שורה 5).
הריצו את התוכנית, ועיקבו אחרי ההודעות שנשלחות מהמתודות של ה- Service:
 
  1. לחצו על כפתור ה- Start. התתקבל הודעה על המסך שנשלחה מה- onCreate ואחריה מה- onStart.
  2. לחצו שוב על ה-Start. הפעם רק הודעת ה-onStart תופיע. המשיכו ללחוץ על 1 וצפו בהגדלת הערך של sum מלחיצה ללחיצה. אם ה-Thread סיים עבודתו, תתקבל הודעה על הפעלת Thread חדש.
  3. לחצו על כפתור ה- Stop וצפו בהודעה מה- onDestroy.
  4. לחצו שוב על ה- start.יתקבלו הודעות כמו בשלב 1.
 קישור לקבצים.
נ.ב.
גם service חייבים לרשום ב- manifest.XML: 

           <service android:name=".myService">
            </service>

בהצלחה!

 
 

4 תגובות:

  1. היי,

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

    השבמחק
    תשובות
    1. תמיד אפשר לגרום לדברים להיות אוטונומים על ידי שימוש ב-synchronized

      מחק
  2. תודה! מאוד מועיל! רציתי לשאול אם אני יוצר service שמפעיל GPS וכולל בתוכה פונ' שמחזירה את המיקום הנוכחי. איך אפשר לקרוא לפונ' שבתוך הservice?
    תודה

    השבמחק