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

Get off my main thread - חלק 3

לאחר שהתחלנו לפני שני מאמרים לסקור את AsyncTask ובמאמר הקודם לסקור את ThreadPoolExecutor, נחבר בין שניהם ונמשיך לצלול לקוד של AsyncTask ולהבין איך ThreadPoolExecutor שייך לשם.


הפעם נחבר קוד טיפה שונה ל-AsyncTask שלנו. בפעם הקודמת שהשתמשנו בו, הרצנו AsyncTask אחד, ב-Thread חיצוני אחד, שביצע 100 פעולות ברקע. כעת נבקש להריץ מספר AsyncTask במקביל. נתחיל בקוד הבא:

public void onStartProgressButtonClicked(View view) {
    AtomicInteger numOfRunTimes = new AtomicInteger(0);
    for (int i = 1; i <= 100; i++) {
        DummyWorkAsyncTask dummyWorkAsyncTask = new DummyWorkAsyncTask();
        dummyWorkAsyncTask.execute(numOfRunTimes);
    }
}  
 
private class DummyWorkAsyncTask extends AsyncTask<AtomicInteger, Integer, Void> {

    @Override
    protected Void doInBackground(AtomicInteger... params) {
        doDummyWork();
        AtomicInteger numOfRunTimes = params[0];
        int myProgress = numOfRunTimes.incrementAndGet();
        publishProgress(myProgress);
        return null;
    }
    
    @Override
    protected void onProgressUpdate(Integer... values) {
        setProgressPercent(values[0]);
    }
}
רק שהפעם נקבל משהו מוזר, אם נריץ את הקוד הנ״ל ב-Gingerbread נקבל קריאות במקביל למספר AsyncTask והעבודה שלנו תתבצע מהר כפי שהייתה לו היינו משתמשים ב-ThreadPoolExecutor, אך אם נריץ את הקוד ב-KitKat למשל (או כל Honeycomb ומעלה) נגלה שהקוד רץ פחות או יותר באותה המהירות של הקוד מלפני שני מאמרים, זה שרץ ב-Thread אחד.

אז מה קורה פה? נפנה לקוד המקור לבדוק.
הקטע הבא לקוח מתוך של AsyncTask ב-KitKat.

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;

/**
 * An {@link Executor} that can be used to execute tasks in parallel.
 */
public static final Executor THREAD_POOL_EXECUTOR
        = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

/**
 * An {@link Executor} that executes tasks one at a time in serial
 * order.  This serialization is global to a particular process.
 */
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    return executeOnExecutor(sDefaultExecutor, params);
}
ב-AsyncTask יש לנו שני משתנים סטטים, האחד TREAD_POOL_EXECUTOR שנבנה עם כמות Threads מותאמת למספר הליבות במכשיר ויכול להריץ משימות במקביל, והשני SERIAL_EXECUTOR המריץ פעולות בטור. ניתן גם לראות שהברירת מחדל כאשר אנו קוראים ל-execute היא להריץ בטור. הברירת מחדל היא ההבדל לשוני בין הפצות אנדרואיד שונות, בשלב מסויים רצו בגוגל להמנע מבעיות שיכולות לנבוע מהרצת קוד במקביל והפכו את ברירת המחדל של AsyncTask מהרצה במקביל להרצה בטור.

דבר נוסף שאני לוקח ממבט בקוד המקור הוא לא לקרוא יותר ל-execute, אלא רק ל-exeuteOnExecutor, כך הקוד יהיה ברור יותר ותינתן לי שליטה מלאה על הקוד שלי. 
אם ארצה להריץ בטור אקרא ל-

dummyWorkAsyncTask.executeOnExecutor( 
                 AsyncTask.SERIAL_EXECUTOR, numOfRunTimes);

ואם ארצה במקביל אוכל לקרוא ל-

dummyWorkAsyncTask.executeOnExecutor(
                AsyncTask.THREAD_POOL_EXECUTOR, numOfRunTimes);

או במקרה שלנו, כפי שגילינו במאמר הקודם עדיף יהיה לקרוא ל-

ExecutorService ex = Executors.newCachedThreadPool();
for (int i = 1; i <= 100; i++) {
    DummyWorkAsyncTask dummyWorkAsyncTask = new DummyWorkAsyncTask();
    dummyWorkAsyncTask.executeOnExecutor(ex, numOfRunTimes);
}

עד עכשיו דיברנו על Thread חיצוני, AsyncTask ו-ThreadPoolExecutor, והזכרנו שכולם סובלים מאותה נקודת התורפה שעשויה להתרחש במקרה הבא - המשתמש יסובב את המסך, ה-Activity יווצר מחדש, Threads חיצוניים חדשים יוקמו בעוד הקודמים ממשיכים ומנסים לקרוא ל-
setProgressPercent של ה-Activity הלא נכון והאפליקציה תהיה במצב בלתי צפוי.
 
ישנן מספר דרכים להתגבר על הבעיה, אפרט את חלקם:
 
1) שליטה על סיבובי מסך על ידי הוספת configChanges=orientation ל-manifest, בשיטה זה המערכת תתן למפתח את כל השליטה על הקורה בסיבוב המסך, ולא יופעל ה-LifeCycle הטבעי של -Activity, ההורס את ה-Activity הישן, משנה את ה-Configuration ויוצר Activity חדש. 
למי זה טוב? אם ה-Activity שלכם לא משתמשת ב-Resources שונים בשינוי אוריינטציה, אם לא אכפת לנו מקוד שעשוי להכשל בעתיד, כאשר נוסיף קוד ל-Activity או אם אנחנו רוצים להיות מפוטרים. אצטט את הדוקמנטציה של אנדרואיד - ״הטכניקה הזו צריכה להיות בשימוש רק כמוצא אחרון ואינה מתאימה למרבית האפליקציות״ 

2) שימוש ב-onSaveInstanceState ו-onRestoreInstacneState על מנת לשמור את המצב האחרון בו ה-Activity היה לפני שינוי האוריינטציה ולעלות אותו מחדש לאחר השינוי.
למי זה טוב? במקרים בהם נרצה לשמור מידע על המצב הנוכחי של ה-Activity, למשל - איזה Views חבואים ואיזה נראים, באיזה מיקום ב-ListView או RecyclerView נמצא המשתמש, על מנת להחזיר אותו לאותו מיקום (זה למעשה קורה אוטומטי על ידי הטכניקה הזו), ובמקרים רבים אחרים בו נרצה מידע גולמי על ה-Activity או ה-Views בו, פתרון זה יהיה מספיק טוב ופשוט. במידה ונרצה לשמור אובייקטים שלמים, כמו תמונות (על מנת להמנע מעלייה איטית שלהם מחדש) או כמו במקרה שלנו, בו נרצה לשמור את ה-AsyncTask, זה לא מספיק. 

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

4) שימוש ב-Loaders, יורחב במאמר הבא.

5) אחד הפתרונות הטובים ביותר למצב שלנו ולמצבים רבים אחרים יהיה שימוש ב-Fragments -
בסיבוב מסך, כאשר ה-Activity נהרס ומאותחל, fragments אשר יסומנו עם (setRetainInstance(true לא יהרסו וישמרו על האובייקטים שלהם, אתן פה את הדוגמא מ-Handling Runtime Changes של http://developer.android.com.

נבנה את הפרגמנט הבא:
public class RetainedFragment extends Fragment {

    // data object we want to retain
    private MyDataObject data;

    // this method is only called once for this fragment
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // retain this fragment
        setRetainInstance(true);
    }

    public void setData(MyDataObject data) {
        this.data = data;
    }

    public MyDataObject getData() {
        return data;
    }
}
ונשתמש בו כך:
public class MyActivity extends Activity {

    private RetainedFragment dataFragment;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // find the retained fragment on activity restarts
        FragmentManager fm = getFragmentManager();
        dataFragment = (DataFragment) fm.findFragmentByTag(data);

        // create the fragment and data the first time
        if (dataFragment == null) {
            // add the fragment
            dataFragment = new DataFragment();
            fm.beginTransaction().add(dataFragment, data).commit();
            // load the data from the web
            dataFragment.setData(loadMyData());
        }

        // the data is available in dataFragment.getData()
        ...
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // store the data in the fragment
        dataFragment.setData(collectMyLoadedData());
    }
}


הערה:
איך פרגמנטים שומרים על עצמם בסיבובי מסך? ה-ActivityThread שומר מצביע לאובייקטים שישמרו לפני הריסת ה-Activity והבנייה שלה מחדש, בעבר יכלנו להוסיף אובייקטים לתהליך הזה על ידי getLastNonConfigurationInstance, מרגע הוספת הפרגמנטים פונקצייה זו ב-Deprecated, מלבד פרגמנטים גם LoaderManager יודע לשמור את ה-Loaders שלו בסיבובי מסך ועליו נרחיב במאמר הבא.

לקריאה נוספת:

אין תגובות:

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