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

ההארה של ה-MainThread חלק 3 ואחרון

מאמר זו נסמך ומסכם את שלושת המאמרים הקודמים ומומלץ לקרוא אותם תחילה 1, 2, 3.
במאמר הראשון בסדרה עצרנו בנקודה בה הפעלנו ב-ActivityThread את הפונקציה הראשונה אשר תרוץ בהפעלת אפליקצייה חדשה, היא public static void main, הצגנו חידה והתפנינו להבין איך Lopper עובד.

לאחר שעברנו על Looper ו-Handler נחזור כעת לקוד המקור ונבחן את public static void  main. לאחר ניקוי מסיבי (מדובר במחלקה כבדה), סידור והזזות קטנות, נראה שהמצב דומה מאוד ל-

public class ActivityThread {
 
    static Handler sMainThreadHandler;  // set once in main()
    final H mH = new H();

    public static void main(String... args) {
        // Save the main looper as a static variable of the Looper class
        Looper.prepareMainLooper();
        // Get a reference to the main handler
        ActivityThread thread = new ActivityThread();
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        // Posting some messages
        Looper.loop();
    }
 
    private class H extends Handler {
        ...
        ...
    }
}
ברגע שה-Thread אשר שועתק (Fork) מהזיגוטה הפך פעיל, מתבצעות הפעולות הבאות בפונקציית ה-main (מקור שמם של ה-Looper וה-Thread הנוכחיים): 
  1. נקראת הפונקצייה Looper.prepareMainLooper, היא זהה ל-Looper.prepare עם תוספת של שמירת ה-Lopper הנוכחי, ה-MainLooper, כמשתנה סטטי של מחלקת Looper - הוא יהיה נגיש מכל מקום בקריאת Looper.getMainLooper.
  2. ה-Handler של ה-Thread הנוכחי, ה-MainThread, ישמר כמשתנה סטטי של מחלקת ActivityThread.
  3. ישלחו פקודות ראשונות ל-MainThreadHandler.
  4. Looper.loop ידאג להמשך חיי ה-Thread וליכולתו לקבל פקודות.
ל-MainThread חשיבות עצומה בכל הנוגע לאפליקצייה שלנו וכאן אצטט את הדוקומנטיצה של אנדרואיד - הוא אחראי לתפיסה ושליחה של events כמו לחיצה, מגע, וציור לממשק המשתמש, הוא ה-Thread היחידי בו האפליקציה תתקשר עם רכיבים מה-Android UI toolkit - ולכן לפעמים יכונה בנוסף ל-Main Thread - ה-UI Thread והוא ה-Thread אשר יהיה אחראי לכל הקריאות של פונקציות ה-LifeCycle במערכת (Recievers, Services, Activities, Providers).

לדוגמא, בלחיצת המשתמש על כפתור, ה-UI Thread ישלח הודעה (Handler.post) אשר תודיע לכפתור כי עליו לצייר עצמו מחדש במצב לחוץ. 

כפי שכולנו למדנו על בשרנו, במידה והאפליקצייה תבצע פעולות אינטנסיביות על ה-UI Thread, הביצועים שלו יפגמו והמשתמש ירגיש שה״אפליקציה תקועה״, חמור מכך - אם ה-UI Thread יהיה חסום למשך 5 שניות יוצג למשתמש הדיאלוג המושמץ - "application not responding״ (ANR dialog) והמשתמש יקבל את האופציה להרוג את האפליקציה. 
על כן, פעולות אשר לוקחות זמן רב כמו חישובים מסובכים, network events, אינטרקציה עם קבצים או עם ה-database וכיוצא בזאת, יופנו ל-Thread אחר. 
הסיבה לכך שכל הפעולות הקשורות לממשק המשתמש מתבצעות ב-Thread יחיד היא מכיוון שה-Android UI toolkit אינו Thread-safe. לסיכום שני חוקים מוכרים וחשובים לגבי ה-Main Thread:
  1. אל תחסום אותו
  2. אל תנסה לגשת לקומפוננטות של ה-UI toolkit מחוץ ל-Main Thread
ובמילים אחרות - Get off my main thread - כותרת אשר תלווה את סדרת המאמרים הבאה, בהם יבחנו (ברמת הקוד) השיטות השונות אשר אנדרואיד מספק לנו על מנת לבצע פעולות ב- Worker Thread בהם נמצא בין היתר את המחלקות Thread, AsyncTask, Loaders, ThreadPoolExecutors. 
ספוילר - כנראה שאת חלקם עד רובם כל מפתח אנדרואיד מכיר ועם זאת שמסתכלים ברמת הקוד דברים יכולים להפתיע ולחדש.

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

public class MainActivity extends Activity {

    private static final String TAG = "my_tag";
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (savedInstanceState == null) {
            Handler handler = new Handler();    
            handler.post(new Runnable() {    post1 נקרא לזה
                public void run() {
                    Log.d(TAG, "Posted before requesting orientation change");
                }
            });
            Log.d(TAG, "Requesting orientation change");
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            handler.post(new Runnable() {    post2 נקרא לזה
                public void run() {
                    Log.d(TAG, "Posted after requesting orientation change");
                }
            });
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG, "onStart");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }
}

“Stress and nervous tension are now serious social problems in all parts of the galaxy and it is in order that this situation should not be in any way exacerbated that the following facts will now be revealed in advanced”
על פי המדריך לטרמפיסט לגלקסיה ומנסיון אישי, מסתבר שעודף סקרנות עשוי להוביל למשבר חברתי עמוק ולכן אתחיל בתשובה -
  1. Requesting orientation change
  2. onStart
  3. Posted before requesting orientation change
  4. onStop
  5. onDestroy
  6. onStart
  7. Posted after requesting orientation change
אלה מכם שידעו את התשובה יכולים לעבור לפתרון החידה הבאה או לכבות כעת את המחשב, המסוקרנים מהסיבה לתשובה ימשיכו איתי הלאה.
כדי לפתור את התשובה, נברר מה ב-MessageQueue של ה-MainLooper, וזאת נעשה כך-
public class MyApplication extends Application {
 
    private static final String TAG = "main_looper"; 
    private Printer printer;

    @Override
    public void onCreate() {
        super.onCreate();
        printer = new Printer() {

            @Override
            public void println(String x) {
                Log.d(TAG, x);    
            }
        };

        Looper.getMainLooper().setMessageLogging(printer);
    }
}
לפני שה-Activity הראשון שלי נוצר, אוסיף לוג ל-MainLooper שלי וכך אקבל את כל המשימות אשר הוא ישלוף. והתוצאה (אחרי לא מעט סינונים) -

Dispatching to Handler (android.app.ActivityThread$H): 100
Dispatching to Handler (android.app.ActivityThread$H): 118
Dispatching to Handler (android.app.ActivityThread$H): 126

מחלקת ActivityThread$H לא חדשה לנו - היא מתוארת בתחילת הכתבה הזו, החלקים בקוד של H המעניינים אותנו -
private class H extends Handler {
    public static final int LAUNCH_ACTIVITY                    = 100;
    public static final int CONFIGURATION_CHANGED   = 118;
    public static final int RELAUNCH_ACTIVITY               = 126;

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case LAUNCH_ACTIVITY:
                ActivityClientRecord r = (ActivityClientRecord)msg.obj;
                handleLaunchActivity(r, null);
                break;
            case RELAUNCH_ACTIVITY:
                ActivityClientRecord r = (ActivityClientRecord)msg.obj;
                handleRelaunchActivity(r);
                break;
            case CONFIGURATION_CHANGED:
                mCurDefaultDisplayDpi = ((Configuration)msg.obj).densityDpi;
                handleConfigurationChanged((Configuration)msg.obj, null);
                break;
        }        
    }
}
100 - ידאג לאתחל את ה-Actvity, לעניינינו הוא יקרא ל-
  1. onCreate
  2. onStart
118 - ידאג לשינוי האוריינטציה, מהקוד המוצג כאן ניתן לנחש שכל נושא בחירת ה-Resources יהיה באחראיותו
126 - ידאג להריץ מחדש את ה-Activity לאחר שהאוריינטציה התחלפה - לעניינינו הוא יקרא ב-Activity שלנו ל-
  1. onStop
  2. onDestroy
ייצור Activity חדש ויקרא אצלו ל-
  1. onCreate
  2. onStart
מלבד זאת הוא גם ידאג לשלוף את המידע של ה-Activity הישן, לפני שזה יהרס ולשלוח אותו ל-Activity החדש - הוא ה-savedInstanceState. סדר הפעולות המפורט מסביר מדוע אם נבדוק את האוריינטציה של המכשיר ב-onDestroy נקבל landscape על אף שה-Activity שנהרס פעל ב-Portrait.

נסכם -
תחילה לסדר הפעולות יכנס onCreate → onStart, ב-onCreate שלנו נוסיף למשימות את post1, לאחריו 118, 126 ולסיום post2, כך שסדר המשימות שלנו הוא
onCreate → onStart → post1 → onStop → onDestroy → onCreate → onStart → post2
אם נקרא את הלוגים המפוזרים במשימות לעיל נקבל בדיוק את התשובה.

הערה חשובה:
post2 נוצר ב-Activity הראשון אך נקרא רק כאשר ה-Activity השני מוצג והראשון אחרי onDestroy, במידה והיינו עושים בו משהו עם שדה של ה-Activity הראשון היינו יוצרים באג. בכל post שאנו שולחים יש לקחת בחשבון את המצב בו אנו עשויים להיות ברגע שהמשימה תחזור ל-handler.

לגבי החידה אשר הוצגה במאמר הקודם, נשאלה השאלה מה הבעיה בקטע קוד הבא -

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));
    }
}
הבעיה הגדולה הטמונה בקטע הקוד הזה נובעת מהעובדה שבג׳אווה, non-static inner and anonymous class שומרת מצביע ל-outer class שלה, כך שאוכל להגיע ל-MainActivity מתוך ה-Runnable שלי בעזרת MainActivity.this. 
ה-Runnable שלי נשמר ב-Message בתוך ה-MainLooper וכעת אם אסובב את המסך מספר פעמים, ה-Activities שלי לא ישתחררו עד ששלושת הדקות יעברו וה-Message יתמחזר. במצב זה הזכרון עשוי להתנפח והאפליקציה עלולה להיאט ואף לקרוס. בנוסף קיימת אותה הבעיה שדיברנו עליה בהיערה החשובה מהפסקה הקודמת.
ישנם מספר פתרונות לבעיה, החל מ-Handler\Runnable סטטים ועד לשחרור כל המשימות של ה-Handler ב-onDestory. הדרך האופטימלית לפתרון הבעיה היא ביד המפתח - כל עוד הוא יודע את שקורה מאחורי הקוד שלו ומבין את הבעיות העשויות לצוץ.

 לקריאה נוספת -
http://developer.android.com/guide/components/processes-and-threads.html 
http://corner.squareup.com/2013/12/android-main-thread-2.html  
https://techblog.badoo.com/blog/2014/08/28/android-handler-memory-leaks/
http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html 
 
 

אין תגובות:

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