יום ראשון, דצמבר 19

Threads חלק 1: עם data messages

כשהאפליקציה נוצרת, נוצר עבורה process של מערכת ההפעלה (linux) והיא רצה  על ה-Thread הראשי שלו. רק מ-Thread זה יהיה ניתן לגשת ל-UI מכאן ואילך. נקרא לו ה-UI Thread.  (*)
ברור שאם האפליקציה תצטרך לבצע פעילויות ארוכות ב-Thread יחיד, ה-UI יסבול מזמני תגובה ארוכים למורת רוחו של המשתמש. נציין גם, שמערכת האנדרואיד בודקת בעצמה את תגובת האפליקציות, ואפליקציה שלא תגיב במשך חמש שניות תחשב ל-ANR - Application Not Responding, תתקבל הודעה מתאימה על המסך, והאפליקציה אף תחוסל.
מוסכם אם כך, שיש צורך לעיתים להשתמש ב-Threads נוספים כדי להוריד עומס מה-Thread הראשי.
נראה 2 שיטות למימוש Threads:
  1. שימוש ב-Handler.
  2. שימוש ב-AsyncTask.
(*)הערה: מקרה חריג  הוא עידכון ה-ProgressBar ווידג'ט אותו הכרנו בפוסט של ה-Dialogs. הפניה אליו בטוחה מכל Thread.
ה-Handler.
ה-Handler הוא אובייקט שרץ במסגרת ה-UI Thread, והוא יושב על ה-MessageQueue ויכול לקבל ולשלוח הודעות. ההודעות יכולות לכלול data או runnables שהם מודולים להרצה. שליחת הודעות ל-MessageQueue יכולה להעשות גם מה-Thread הראשי, אך גם מ- Threads חדשים שניצור.
ההודעות וה-runnables שנשלחים ל-Message Queue יכולים לקבל טיפול מידי או אם השהיה - בהתאם לפרמטר שניתם להם. למנגנון הנ"ל יכולים להיות מספר שימושעם כגון:
  • תזמון ביצוע פעולות תך ניצול מנגנון ההשהיה.
  • העברת נתונים וה-runnables בין Threads משניים לבין ה-Thread הראשי.
בפוסט זה ובבא אחריו נתרכז בשימוש השני - טיפול בתקשורת עם-Threads. הזכרנו שני סוגי הודעות, כך שגם בהקשר של ה-Threads ה-Handler יכול לפעול בשתי צורות:

טיפול ב- ב-Data Messages: ה-Thread מדבר עם ה-Thread הראשי ע"י שליחת הודעות הכוללות נתונים המועברים ל--Handler - שרץ ב-Thread הראשי.
שיטת ה-Runnable: ה-Thread שולח ל-MessageQueue מודולים להרצה שיופעלו ע"י ה-Handler במסגרת ה-Thread הראשי.

נפרט את השיטות הנ"ל בשלושה פוסטים נפרדים. בפוסט הזה נדגים  Handler עם Data Messages.
בפוסטים העוקבים נדגים את שתי השיטות האחרות.

Handler: טיפול ב-Data Messages




תאור הדוגמא

  • ניצור אובייקט מסוג Handler לטיפול בהודעות שנשלחות מה-Thread. האובייקט הנ"ל רץ במסגרת ה-UI Thread, כך שהוא (ה-Handler) יכול לגשת ל-UI.
  • ניצור אובייקט מסוג Thread שישלח הודעות ל-Handler הנ"ל.
  • בעצם...כדי להוסיף עניין... נדגים יצירת שני אובייקטים של ה-Thread. שניהם רצים על קוד זהה, אך עם priority שונה. נשווה את ההתנהגות שלהם.


נציג אתה תמונת ה-UI:

בתמונה הנ"ל אפשר להבחין בארבעה חלונות TextEdit ושני כפתורים. נפרט על פעולת כל אחד מהווידג'טים הנ"ל:

  • חלון הטקסט העליון - פשוט מאפשר הקלדת טקסט פנימה, כדי לתת למשתמש את התחושה שהמכשיר מגיב לפעולותחו תוך כדי פעולת ה-Threads ברקע.
  • שני חלונות הטקסט בשורה השניה - בחלון השמאלי והימני שבשורה השניה, מודפסים הערכים הרגעיים של ה-counters שנשלחים ע"י ה-Thread הראשון והשני בהתאמה. נוכל לראות שה-counter של ה-Thread עם ה-Priority הגבוהה יותר יתקדם מהר יותר, בהתאם ליחס בין ה-Priorities. כשנעצור את ה-Thread, ה-counter שלו יעצור.
  • חלון הטקסט בשורה השלישית - מציג את ה-Priorities של כל אחד מה-Threads, כאשר זו של ה-Thread הראשון ניתנת לשינוי ע"י כפתור, וה-Priority של ה-Thread השני 10תמיד . תחום  ה-Priorities של ה-Threads הוא בין 1 (נמוך) ל 10.
  • הכפתור השמאלי - משנה את ה-Priority של ה-Thread1. כל לחיצה מגדילה ב-1, בתחום של 1-10. 
    • עם שינוי ה-Priority, נאפס  את ה-counters המוצגים בחלונות השורה השלישית.
  • הכפתור הימני - מפעיל\מפסיק אתי -Thread1. ה-Thread השני ימשיך לרוץ, כפי שיומחש ע"י תצוגת ה-counter הרץ.
אזהרה: במטרה להבליט את השפעת ה-Priority, גרמתי ל-Threads לעבוד באופן אינטנסיבי  ("לטחון" עם sleep קצר בלבד) כך שה-Threads "יאבקו" בינהם על משאבי הזמן. הרצת התוכנית מעמיסה לפיכך על המכשיר עליו היא רצה או המחשב שמארח את האמולטור. ממליץ לא להריץ למשך זמן ארוך.


כאן למטה מוצג הקוד בשלמותו. כל התוכנית בנויה בתוך class יחיד מסוג Activity. התוכנית מחולקת למספר מרכיבים, כל חלק מסומן בצבע רקע שונה:
  1. Handler - צבע אפור.
  2. onCreate - צבע צהוב.
  3. ה-Thread Class - אני חושב שזהבצבע  טורקיז, אבל אני לא רואה את כל הצבעים כ"כ טוב.
  4. איתחולי הווידג'טים וה-listeners callbacks של הכפתורים.- גוון כתום.
  5. threadSetup- ורוד + כתום.
בהמשך נסקור את 3 החלקים הנ"ל. החלק הרביעי סטנדרטי ואינו קשור לנושא הליבה שלנו - Threads, כך שיתכן שלא נפרטו.

public class DrawControlActivity extends Activity {
    EditText editTextThreadOutput;
    EditText editTextThreadOutput2;
    EditText editTextPriority;
    Button buttonThreadOnOff;
    int prioIndx = 1;
    ExampleThread exampleThread1;
    ExampleThread exampleThread2;
    final int THREAD2_PRIORITY = 10;
    final int THREAD1_PRIORITY = 1;
    Handler handler = new Handler(){
        public void handleMessage(Message msg) {
            int mtThreadId = msg.getData().getInt("myThreadId",1000);
            int counter = msg.getData().getInt("counter",1000);
            if(0 == myThreadId){
                editTextThreadOutput.setText(Integer.toString(counter));
            }
            else
            {
                editTextThreadOutput2.setText(Integer.toString(counter));
            }
        }
    };
    /** Called when the activity is first created. */
    @Override public void onCreate(Bundle state) {
        super.onCreate(state);
        setContentView(R.layout.main);
        buttonSetup();
        editTextSetup();
        threadSetup();
    }
     public class ExampleThread extends Thread {
        Handler mHandler;
        boolean mState = true;
        int counter;
        int mVal;

        void mPrioritySet(int priority){
            setPriority(priority);
        }
        ExampleThread(Handler h) {
            mHandler = h;
        }
        public void run() {
            counter = 0;
            while (mState) {
                try {
                    Thread.sleep(10);
                }
                catch (InterruptedException e) {
                }
                counter++;
                Message msg = mHandler.obtainMessage();
                Bundle bundle = new Bundle();
                bundle.putInt("myThreadId", myThreadId);
                bundle.putInt("counter", counter);
                msg.setData(bundle);
                mHandler.sendMessage(msg);
            }
        }
        public void threadStateSet(boolean state) {
            mState = state;
        }
        public boolean threadStateGet(){
            return(mState);
        }
        public void threadIdSet(int val) {
            mVal = val;
        }
        public void threadCounterClear() {
            counter = 0;
        }
    }
    private void buttonSetup(){
    Button buttonSetPriority = (Button)findViewById(R.id.button_set_priority);
    buttonSetPriority.setText("Thread Priority Set");
    buttonSetPriority.setOnClickListener(
    new Button.OnClickListener() {public void onClick(View v) {
           if (0 == (prioIndx++)%10){
               prioIndx = 1;
           }
           exampleThread1.mPrioritySet(prioIndx);
        editTextPriority.setText("Thrd Priority = " +prioIndx+ "    Thrd 2 Prio= " + THREAD2_PRIORITY);

           exampleThread1.threadCounterClear();
           exampleThread2.threadCounterClear();
          
    } });
    buttonThreadOnOff = (Button)findViewById(R.id.button_thread_onoff);
    buttonThreadOnOff.setText("Thread is On");
    buttonThreadOnOff.setOnClickListener(
    new Button.OnClickListener() {public void onClick(View v) {
        if(exampleThread1.threadStateGet()){
            exampleThread1.threadStateSet(false);
            buttonThreadOnOff.setText("Thread is Off");
        }
        else
        {
            exampleThread1.threadStateSet(true);
            buttonThreadOnOff.setText("Thread is On");
            exampleThread1 = new ExampleThread(handler);
            exampleThread1.start();
        }
        exampleThread1.mPrioritySet(prioIndx);
    } });
    }
    private void  editTextSetup(){
        EditText editText1;
        editTextThreadOutput = (EditText) findViewById(R.id.edit_text2);
        editTextThreadOutput2 = (EditText) findViewById(R.id.edit_text22);
        editText1 = (EditText) findViewById(R.id.edit_text1);
        editTextPriority = (EditText) findViewById(R.id.edit_text3);
    }

    private void threadSetup(){
        exampleThread1 = new ExampleThread(handler);
        exampleThread1.start();
        exampleThread1.mPrioritySet(THREAD1_PRIORITY);
            editTextPriority.setText("Thrd 1 Prio= " +THREAD1_PRIORITY+ "    Thrd 2 Prio= " + THREAD2_PRIORITY);
        exampleThread1.threadIdSet(0);
        exampleThread2 = new ExampleThread(handler);
        exampleThread2.start();
        exampleThread2.mPrioritySet(THREAD2_PRIORITY);
        exampleThread2.threadIdSet(111111);
    }
}


נתחיל עם ה-Handler

הנה שוב הקוד של ה-handler:




  1.   Handler handler = new Handler(){
  2.         public void handleMessage(Message msg) {
  3.             int myThreadId = msg.getData().getInt("myThreadId",1000);
  4.             int counter = msg.getData().getInt("counter",1000);
  5.             if(0 == myThreadId){
  6.                 editTextThreadOutput.setText(Integer.toString(counter));
  7.             }
  8.             else
  9.             {
  10.                 editTextThreadOutput2.setText(Integer.toString(counter));
  11.             }
  12.         }
  13.     };
תפקיד ה-Handler בקיצור

ה-Handler ישלוף את ה-counter מההודעה, יזהה מי משני ה-thread שלח אותה,  ויציג אותו בחלון השמאלי או הימני בהתאם.

 

שורה 2 יוצרת אובייקט של Handler בשם handler,  ובאותה פקודה גם ממשת אותו.

ה-Handler מממש מתודה אחת בלבד:

handleMessage - זוהי מתודה אותה כל Handler Subcalss חייב לממש. היא מופעלת כשמגיעה הודעה מהתוד (מה-MessageQueueu) .

מבנה ההודעה שמימשנו:

 ה-Thread שולח הודעה ובה שני חלקים:

 ThreadId- זהוי של ה-Thread.

counter - הערך הרגעי של המונה הרץ.

כל אחד חלקי הודעה צמוד ל-key שלו (הודעות בנויות מצמדים של key,value).

תאור המתודה

שורה 3, שולפת את ה- ThreadID עפ"י ה-key לו מצפים "ThreadId". במקרה שה-key הזה לא נמצא, יוחזר ערך ה-default שהוא 0 (הפרמטר השני בשורה 3).

שורה 4 שולפת את ה-counter.

שורות 5-11 שולחות את ההודעה לאחד משני חלונות הEditText-.


נעבור ל-onCreate

כאן מתבצעות, כרגיל, פעולות האיתחול.

  1.   @Override public void onCreate(Bundle state) {

  2.         super.onCreate(state);
  3.         setContentView(R.layout.main);
  4.         buttonSetup();
  5.         editTextSetup();
  6.         threadSetup();
  7.     }

שורות 1-2 סטנדרטיות - קריאה ל-super constructor ופריסת ה-layout לתצוגה.

שורה 4 - buttonsSetup.  

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

שורה 6: threadSetup.

הנה המתודה:

  1. private void threadSetup(){

  2.         exampleThread1 = new ExampleThread(handler);
  3.         exampleThread1.start();
  4.         exampleThread1.mPrioritySet(THREAD1_PRIORITY);
  5.             editTextPriority.setText("Thrd 1 Prio= " +THREAD1_PRIORITY+ "    Thrd 2 Prio= " + THREAD2_PRIORITY);
  6.         exampleThread1.threadIdSet(0);
  7.         exampleThread2 = new ExampleThread(handler);
  8.         exampleThread2.start();
  9.         exampleThread2.mPrioritySet(THREAD2_PRIORITY);
  10.         exampleThread2.threadIdSet(111111);
  11.     }
  12. }
  • שורה 2\שורה 7: המתודה יוצרת שני אובייקטים של ה- ExampleThread

  • שורה 4\9: מקנפגת את ה-priority

  • שורה 5: מציגה את ה-priority ההתחלתי בחלון התצוגה.

  • שורה 6\10: מקנפגת את הזיהוי, שהיא בעצם חלק מההודעה אותה ישלחו ה-Threads ובעזרתה נבחין בינהם

  • שורה 3\8: מפעילה את ה-Threads ע"י המתודה start.

ה-Thread Class

ה-constructor - שורות 10-12, שומר את האובייקט של ה-handler. אובייקט זה ישמש לצורך שליחת ההודעות.

המתודה העיקרית ב-class ה-Thread היא -run. מתודה זו מופעלת כתוצאה מהפעלת start. ה-run כולל לולאת while - שורות 15-28. בתוך הלולאה ישנה פקודת ה-(sleep(100 , בה הThread ישן למשך 100 מילישניה. 

שורות 22-27 הן השורות בהן ה-Thread יוצר ושולח את ההודעה ל-handler.

  1.    public class ExampleThread extends Thread {

  2.         Handler mHandler;
  3.         boolean mState = true;
  4.         int counter;
  5.         int mVal;

  6.         void mPrioritySet(int priority){
  7.             setPriority(priority);
  8.         }
  9.         ExampleThread(Handler h) {
  10.             mHandler = h;
  11.         }
  12.         public void run() {
  13.             counter = 0;
  14.             while (mState) {
  15.                 try {
  16.                     Thread.sleep(10);
  17.                 }
  18.                 catch (InterruptedException e) {
  19.                 }
  20.                 counter++;
  21.                 Message msg = mHandler.obtainMessage();
  22.                 Bundle bundle = new Bundle();
  23.                 bundle.putInt("myThreadId", myThreadId);
  24.                 bundle.putInt("counter", counter);
  25.                 msg.setData(bundle);
  26.                 mHandler.sendMessage(msg);
  27.             }
  28.         }
  29.         public void threadStateSet(boolean state) {
  30.             mState = state;
  31.         }
  32.         public boolean threadStateGet(){
  33.             return(mState);
  34.         }
  35.         public void threadIdSet(int val) {
  36.             mVal = val;
  37.         }
  38.         public void threadCounterClear() {
  39.             counter = 0;
  40.         }
  41.     }
  42.  

נתרכז ביצירת ההודעה:
  1. Message msg = mHandler.obtainMessage();
  2.                 Bundle bundle = new Bundle();
  3.                 bundle.putInt("myThreadId", myThreadId);
  4.                 bundle.putInt("counter", counter);
  5.                 msg.setData(bundle);
  6.                 mHandler.sendMessage(msg);
שורה 1: יצירת אובייקט מסוג Message. זה האובייקט שיישלח ל MessageQueueu ומשם יגיע ל-Handler.
שורה 2-4: הכנת ההודעה. לצורך הכנת ההודעה משתמשים באובייקט מסוג Bundle שמשמש לבנייתה. כבר ציינו שההודעה בנויה מזוגות של key,value. שני זוגות כאלה מוכנסים בשורות 3 ו-4.
שורה 4 מכניסה את ה-bundle לתוך ההודעה.
שורה 6: שליחת ההודעה ל-message queue השייך ל- mHandler.

מימשנו Thread עם הודעות שטופלו ע"י ה-Handler. בפוסטים הבאים נסקור את מימוש ה-Thread עם runnable ועם AsyncTask.

2 תגובות:

  1. אחלה הסבר!
    רק חבל שבקוד המתודות לא כתובות בתבנית הרווחת - threadIdSet -> setThreadId...
    אבל באמת כל הכבוד ותודה!

    השבמחק
  2. תודה רבה!
    עוזר לי מאד!

    השבמחק