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

ההארה של ה-MainThread חלק 2


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

public class MyThread extends Thread {

    @Override
    public void run() {
        // do stuff here
        // and another stuff here
    }
}
ה-Thread שלנו יעבור ב-run, יעשה דבר ועוד דבר ויצא. על מנת להשאיר Thread פעיל ושיעבוד עבורנו על פי דרישה, נחפש משהו יותר דומה ל-

MessageQueue queue;
 
@Override
public void run() {
    while (true) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return; 
        }
        msg.target.dispatchMessage(msg); 
        msg.recycle();  
    }

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


public class Looper {

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    final MessageQueue mQueue;
 
    public static void prepare() {
        sThreadLocal.set(new Looper());
    }
    
    public static void loop() {
        final MessageQueue queue = myLooper().mQueue; 
        while (true) {
            Message msg = queue.next(); // might block
            msg.target.dispatchMessage(msg);
            msg.recycleUnchecked();
        }
    }

    public static Looper myLooper() {
        return sThreadLocal.get();
    }
 
    public void quit() {
        mQueue.quit();
    } 
}
 
מי שעקב באדיקות אחרי המאמר הקודם יבחין שה-Looper נשמר ב-ThreadLocal, דבר המבטיח מקסימום של Looper אחד ל-Thread וזמינות של כל Looper ל-Thread שלו בלבד.

public static void prepare()
ידאג ליצור את ה-Looper ולשמור אותו ב-ThreadLocal של ה-Thread הנוכחי.
public static void loop()
ידאג להריץ את כל המשימות ברשימה שלנו.
public static Looper myLooper()
יחזיר לנו את ה-Looper של ה-Thread אשר אנו נמצאים בו
public void quit()
על מנת לאפשר ל-Thread להשתחרר בסיום העבודה יש לקרוא לסיום ה-Looper ובכך 
להודיע ל-Thread שמשימותיו תמו.

Handler הוא השותף של ה-Looper, הוא ה-target ב-msg.target ב-void loop של ה-Looper,
לו שתי יכולות עיקריות:
  • שליחת משימות ל-Looper (יכול להיעשות מכל Thread)
  • ביצוע המשימות אשר ה-Looper שולף (יבוצע ב-Thread אשר מקושר ל-Looper)
המתווך בין ה-Handler ל-Looper יהיה ה-Message, מחלקה גנרית ויעילה מאוד, מרבית הפעילות של המשתמש עם האפליקציה יתורגם ל-Message ל-Looper.
כל Handler יהיה מקושר ל-Looper אחד בלבד אשר יהיה מקושר ל-Thread אחד בלבד, מספר ה-Handler אשר יהיו מקושרים לאותו ה-Looper אינו מוגבל, סדר הפעילויות בפשטות יראה כך -
 
הדיאגרמה לקוחה מתוך הבלוג של rxwen - just do IT

האיור לעיל מציג את שקורה ב-Thread אחד בלבד, לו Looper אחד ושני Handler. במקרה הזה Handler1 שלח את Message1 וכלשיגיע תורו ה-Looper יחזיר אותו ל-Handler להמשך טיפול, כמו כן Handler2 אחראי לשליחת משימות 2 ו-3.

הגנריות של Message באה לידי ביטוי בכך שהשימוש ב-Message נתון כמעט באופן בלעדי לידי 
המפתח -

/**
 * User-defined message code so that the recipient can identify
 */ 
public int what;
long when;

/**
 * if you only need to store a few integer values.
 */
public int arg1; 
public int arg2;
/**
 * An arbitrary object to send to the recipient.  
 */
public Object obj;

המפתח יחליט מה להעביר ב-what על מנת לזהות בהמשך את ה-Message כאשר זה יחזור מה-Looper ל-Handler, המפתח יחליט מתי ה-Message צריך להשלח ב-when, המפתח יחליט אם ומה להכיל ב-arg1 ו-arg2 והאם להוסיף אובייקט ל-obj. 

היעילות של Message -

Handler target;
Runnable callback;
Message next;
private static final Object sPoolSync = new Object();
private static Message sPool;
 
/**
 * Return a new Message instance from the global pool. Allows us to
 * avoid allocating new objects in many cases.
 */
public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            return m;
        }
    }
    return new Message();
}

/**
 * Recycles a Message 
 * Used internally by the MessageQueue and Looper when disposing of queued Messages.
 */
void recycle() {
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    when = 0;
    target = null;
    callback = null;

    synchronized (sPoolSync) {
        next = sPool;
        sPool = this;
    }
}
 
כשבחנו את ה-Looper ראינו שהוא מכיל MessageQueue, הוא ידאג לשלוף עבורו כל Message בתורו ובתנאי שה-when שלו הגיע, בסיום ה-Looper ימחזר את ה-Message. 

היצירה של ה-Message היא אחת מ-2 המשימות שנותרו ל-Handler.
דוגמא אחת לשימוש ב-Handler -
 // Each handler is associated to one looper.
Handler handler = new Handler(myLooper) {
    public void handleMessage(Message message) {
        switch (message.what) {
            case SOMETHING:
                // do something on the thread of the looper
                break;
            case SOMETHING_ELSE:
                // do something else on the thread of the looper
                break;
        }
    }
};

// Create a new message associated to that handler.
Message message = handler.obtainMessage();
message.what = SOMETHING_ELSE;

// Add the message to the looper queue, can be called from any thread.
handler.sendMessage(message);
 
בדוגמא הנוכחית יצרנו Handler, קישרנו אותו ל-Looper והגדרנו לו פעולות לבצע כתלות במשימה, לאחר מכן יצרנו משימה אשר מקושרת ל-Handler שלנו ושלחנו אותה להמתין בתור עד שה-Looper יחזיר לנו אותה. הטיפול במשימה יתבצע ב-Thread המקושר ל-Looper, אך שליחת המשימה תוכל להתבצע מכל
Thread.

דרך נוספת ונפוצה לשלוח משימות בעזרת Handler תהיה

handler.post(new Runnable() {
    public void run() {
        // do something on the thread of the looper
    }
});
 
כאן יצרנו Handler עם wait של 0 מילישניות (במידה והיינו משתמשים ב-postDelayed זה הערך אשר היה
משתנה), וה-Runnable שלנו ישמר ב-callback של ה-Message ובבוא העת ירוץ ב-Thread של 
ה-Looper.

Handler אשר יאותחל כך -
Handler handler = new Handler();
יקושר ל-Thread בו הוא נוצר, במהלך האיתחול שלו הוא יבצע
mLooper = Looper.myLooper();
ככלל ביצירת Handler חדש כדאי למפתח לדעת לאיזה Looper ו-Thread ה-Handler הנוצר מקושר 
ולכן לא מומלץ לאתחל Handler מבלי לספק לו Looper.

HandlerThread
 
HandlerThread - מחלקה אשר יורשת מ-Thread ומחברת בין על האמור לעיל
 
public class HandlerThread extends Thread {

    Looper mLooper;

    @Override
    public void run() {
        Looper.prepare();
        mLooper = Looper.myLooper();
        Looper.loop();
    }

    public Looper getLooper() {
        return mLooper;
    }

    public void quit() {
        mLooper.quit();
    }
}
 השימוש בה יתבצע כך -
HandlerThread thread = new HandlerThread("MyCoolThreadName");
thread.start(); // starts the thread.
Handler myThreadHandler = new Handler(thread.getLooper());
// do a lot of stuff with my handler
// finally let the thread quit
thread.quit(); 

במאמר הבא נסיים להבין את ה-MainThread ונענה על החידה מהמאמר הראשון, לבינתיים אציג חידה נוספת אשר הוצגה ב-(DroidCon Tel-Aviv (2014 - מה הבעיה בקטע קוד הבא?

public class MainActivity extends Activity {

    protected static final String TAG = "my_tag";
    private Handler handler = new Handler();
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handler.postDelayed(new Runnable() {
   
            @Override
            public void run() {
                Log.d(TAG, "3 minutes passed");
            }
        }, TimeUnit.MINUTES.toMillis(3));
    }
}

   
לקריאה נוספת: 
http://rxwen.blogspot.co.il/2010/08/looper-and-handler-in-android.html
http://corner.squareup.com/2013/10/android-main-thread-1.html

אין תגובות:

הוסף רשומת תגובה