יום שבת, 6 בדצמבר 2014

Get off my main thread - חלק 1

בסדרת המאמרים הקודמים דיברנו על ה-Main Thread ועל החשיבות בלהשאיר אותו נקי ככל הניתן. אזכיר כאן את שני כללי הברזל המוכרים אשר צריכים ללוות כל מפתח אנדרואיד לגבי ה-Main Thread:
  1. אל תחסום אותו
  2. אל תנסה לגשת לקומפוננטות של ה-UI toolkit מחוץ ל-Main Thread
בסדרת המאמרים הבאה נבחן את הדרכים השונות אותם אנדרואיד מספק לנו על מנת לבצע תהליכים ארוכים ב-Worker Threads.

לשם ההדגמה נעבוד עם אפליקציה חוצת גבולות ועשירת תרבויות אשר לה ProgressBar, TextView ו-Button. 
האפליקציה שלנו תידרש לבצע 100 פעמים עבודה סתמית כלשהי ב-Worker Thread ולהציג למשתמש את ההתקדמות.
ה-Activity הראשי של האפליקציה -
public class BackgroundActivity extends Activity {
    private ProgressBar mProgress;
    private TextView mText;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.background_activity);
        mProgress = (ProgressBar) findViewById(R.id.progressBar);
        mText = (TextView) findViewById(R.id.textView);
    }

    public void onStartProgressButtonClicked(View view) { 
        // do 100 dummy works
    }

    private void doDummyWork() {
        Thread.sleep(100);
    }
 
    protected void setProgressPercent(int myProgress) {
        mText.setText(myProgress + "% percentage complete");
        mProgress.setProgress(myProgress);
    } 
}
 
ה-xml של ה-Activity -

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:indeterminate="false"
        android:max="100"
        android:padding="20dp" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onStartProgressButtonClicked"
        android:text="Start Progress"
        tools:ignore="HardcodedText" />

</LinearLayout>

אנו נבדוק דרכים שונות למלא את public void onStartProgressButtonClicked

שימוש ב-Thread חיצוני פשוט (פתרון של ג׳אווה - נמצא תחת java.lang):
 בתור התחלה נפנה לפתרון הבסיסי ביותר, ניצור Thread חדש ונשאיר לו את העבודה -

public void onStartProgressButtonClicked(View view) {
    Thread backgroundThread = new Thread(new Runnable() {

        @Override
        public void run() {
            for (int i = 1; i <= 100; i++) {
                doDummyWork(); 
                setProgressPercent(i); 
            }
        }
    });
    backgroundThread.start();
}
בפתרון הזה יצרנו Thread חדש, הגדרנו לו את העבודה - להריץ 100 פעמים doDummyWork ולדווח על ההתקדמות, אמנם בהרצה שלו מיד נקרוס ונקבל - CalledFromWrongThreadException כי עברנו על הכלל השני וניסינו לשנות את ה-TextView וה-ProgressBar שלנו מ-Thread אחר מה-UI Thread.
גם אם לא אחדש לרוב מפתחי האנדרואיד מהם השיטות הנפוצות להריץ את הקוד אשר קשור ל-UI ב-UI Thread, אנסה לחדש על העומד מאחוריהם.
א. שימוש ב-Handler:

@Override
public void run() {
    Handler uiHandler = new Handler(Looper.getMainLooper()); 
    for (int i = 1; i <= 100; i++) {
        final int myProgress = i;
        doDummyWork(); 
        uiHandler.post(new Runnable() {
 
            @Override
            public void run() { 
                setProgressPercent(myProgress);              
            }
        });
    }
} 
זו אולי הדרך הנפוצה ביותר בה נתקלתי בשליחת משימות ל-UI Thread, אמנם ניתן לשפץ אותה עוד טיפה. בצורה הנוכחית אנו יוצרים 100 אובייקטים של Runnable, לעומת זאת אנו יודעים ש-Message ממחזר את עצמו ולכן הצורה היעילה יותר תראה כך:

@Override
public void run() {
    Handler uiHandler = new Handler(Looper.getMainLooper()) {

        @Override
        public void dispatchMessage(Message msg) {
            int myProgress = msg.what; 
            setProgressPercent(myProgress);
        };
    };
 
    for (int i = 1; i <= 100; i++) {
        doDummyWork();
        uiHandler.obtainMessage(i).sendToTarget();
    }
}
במידה ונתקל בסיטואציה בה נרצה להמנע מליצור Handler נוכל להשתמש ב-Handler שנמצא בכל View ומקושר ל-Main Thread, במקרה שלנו -
mText.post(new Runnable() {...});
או לקבל את ה-Handler ישירות - 
mText.getHandler();
ב. שימוש ב-Activity.runOnUiThread:

@Override
public void run() {
    for (int i = 1; i <= 100; i++) {
        final int myProgress = i;     
        doDummyWork();
        runOnUiThread(new Runnable() {

            @Override
            public void run() {
                setProgressPercent(myProgress);
            }
        });
    }
}
בכדי להשתמש בפתרון הזה נצטרך רפרנס ל-Activity, שהרי runOnUiThread היא פונקצייה של Activity, דבר אשר אינו מובטח לנו ב-custom objects. 
ההבדל בין שתי השיטות טמון ביישום של runOnUiThread, נסתכל על הקוד המקור -

public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}
runOnUiThread יבדוק האם ה-Thread הנוכחי הוא ה-UI Thread, במידה ולא יוציא את הפקודה ל-uiHandler וזו תכנס לסוף התור של ה-MessageQueue ב-Main Looper, במידה וכן - הפקודה תרוץ באופן מיידי.

2. שימוש ב-AsyncTask (פתרון של אנדרואיד - נמצא תחת android.os):
על מנת לחסוך את הטיפול ב-Thread חיצוני ובסנכרון שלו חזרה ל-UI Thread, אנרואיד מספק לנו את  AsyncTask, מחלקה מעט שנויה במחלוקת, ישנם מפתחים שימצאו אותה שימושית מאוד ואחרים שיזהירו מפני שימוש בה. דעתי - כל עוד המפתח מבין ויודע מה יקרה בשימוש שלו במחלקה, היא יכולה להיות יעילה. המחלקה מציעה מגוון אפשרויות שימוש בה - בטור או במקביל, בהווה או בעתיד ועוד. נתחיל לחקור את השימוש הכי בסיסי והכי נפוץ של המחלקה במאמר הזה ונסיים לחקור מעבר בעוד שני מאמרים, לאחר שנחקור את ThreadPoolExecutor.

חקירה בסיסית של AsyncTask -
השימוש הנפוץ ביותר והמוכר ביותר של המחלקה מתבסס על חלקי הקוד הבאים מתוך קוד המקור שלה -
public abstract class AsyncTask<Params, Progress, Result> {

    /**
     * Runs on the UI thread before doInBackground.
     */
    protected void onPreExecute() {}

    /**
     * Runs on a background thread
     */
    protected abstract Result doInBackground(Params... params);

    /**
     * Runs on the UI thread after doInBackground.
     * The specified result is the value returned by doInBackground
     * This method won't be invoked if the task was cancelled. 
     */
    protected void onPostExecute(Result result) {}

    /**
     * Runs on the UI thread after publishProgress is invoked.
     */
    protected void onProgressUpdate(Progress... values) {}

    // A lot of code here
}
AsyncTask היא מחלקה מסוג Abstract, אין לה מימוש קיים ואם נרצה להשתמש בה נהיה חייבים לרשת ממנה, 4 רכיבים עיקריים מרכיבים אותה - onPreExecute - יופעל ב-UI Thread לפני המשימה שנבקש לבצע ב-Thread חיצוני, doInBackground - יבוצע ב-Thread חיצוני, onPostExecute - יבוצע ב-UI Thread לאחר שהעבודה של doInBackground הסתיימה, הוא יקבל את האובייקט ש-doInBackground יחזיר ו-onProgressUpdate, אליו נקרא ידנית על ידי publishProgress, גם הוא יופעל ב-UI Thread.

אפשר לתאר את התהליך כך:
original posted at http://programmerguru.com/android-tutorial/what-is-asynctask-in-android/
את כל התהליך תתחיל קריאה ל-execute, הזזתי, פישטתי ומחקתי הרבה קוד, אפשר להשיג את השימוש הבסיסי ביותר על ידי הקטע קוד הבא:

private final Handler sHandler = new Handler() {
    public void dispatchMessage(Message msg) {
        Object resultOrProgress = (Object) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                onPostExecute((Result) resultOrProgress);
                break;
            case MESSAGE_POST_PROGRESS:
                onProgressUpdate((Progress[]) resultOrProgress);
                break;
        }
    };
};
   
public final void execute(final Params... params) {
    onPreExecute();
    new Thread(new Runnable() {

        @Override
        public void run() {
            Result result = doInBackground(params);
            sHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
        }
    });
}   
   
protected final void publishProgress(Progress... values) {
    sHandler.obtainMessage(MESSAGE_POST_PROGRESS, values).sendToTarget();
}

בשביל הפעילות הבסיסית שרוב הפעמים ישתמשו ב-AsyncTask, הקוד לעיל יספיק. שלושה דברים אפשר לראות לפי הקוד -
א. execute חייב לרוץ ב-UI Thread, אחרת onPreExecute ירוץ ב-Thread הלא נכון.
ב. new AsyncTask חייב לרוץ ב-UI Thread, ה-Handler שלו מאותחל ללא Looper מוגדר.
ג. שלושת הפרמטרים הגנרים של המחלקה - Params, Progress and Result מקבלים משמעות פה, Params ישלח ל-doInBackground, בתצורת מערך, Progress ישלח ל-onProgressUpdate ו-Result ישלח ל-onPostExecute
בשביל להריץ את הקוד שלנו ב-AsyncTask נכתוב את הקוד הבא:

public void onStartProgressButtonClicked(View view) {
    Integer numOfRunTimes = 100;
    DummyWorkAsyncTask dummyWorkAsyncTask = new DummyWorkAsyncTask();
    dummyWorkAsyncTask.execute(numOfRunTimes);
}       
   
private class DummyWorkAsyncTask extends AsyncTask<Integer, Integer, Boolean> {

    @Override
    protected Boolean doInBackground(Integer... params) {
        int numOfRunTimes = params[0];
        boolean result = true;
        for (int i = 1; i <= numOfRunTimes; i++) {
            doDummyWork();
            publishProgress(i);
        }
        return result;
    }
       
    @Override
    protected void onProgressUpdate(Integer... values) {
        setProgressPercent(values[0]);
    }
       
    protected void onPostExecute(Boolean result) {
        if (result == true)
            Toast.makeText(getApplicationContext(), "Job done", 
               Toast.LENGTH_LONG).show();
    }
}

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

במאמר הבא נדון לעומק על ThreadPoolExecutor - ביצוע פעולות חיצוניות במספר Threads במקביל.

אין תגובות:

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