כשמתחיל רכיב באפליקציה ולאפליקציה אין רכיבים אחרים שפועלים, מערכת Android מתחילה תהליך חדש של Linux עבור האפליקציה עם שרשור יחיד של ביצוע. כברירת מחדל, כל הרכיבים של אותה אפליקציה פועלים באותו תהליך ובאותו שרשור, שנקרא שרשור הראשי.
אם רכיב באפליקציה מופעל וכבר יש תהליך לאותה אפליקציה, כי רכיב אחר מהאפליקציה כבר הופעל, הרכיב מופעל בתוך התהליך הזה ומשתמש באותו Thread של הרצה. עם זאת, אתם יכולים להגדיר שרכיבים שונים באפליקציה יפעלו בתהליכים נפרדים, וליצור שרשורים נוספים לכל תהליך.
במאמר הזה מוסבר איך פועלים תהליכים ו-Thread-ים באפליקציה ל-Android.
תהליכים
כברירת מחדל, כל הרכיבים של אפליקציה פועלים באותו תהליך, ורוב האפליקציות לא משנות את זה. עם זאת, אם אתם רוצים לקבוע לאיזה תהליך רכיב מסוים שייך, אתם יכולים לעשות זאת בקובץ המניפסט.
רשומת המניפסט לכל סוג של רכיב – <activity>, <service>, <receiver> ו-<provider> – תומכת במאפיין android:process שבאמצעותו אפשר לציין תהליך שהרכיב פועל בו. אתם יכולים להגדיר את המאפיין הזה כך שכל רכיב יפעל בתהליך משלו, או כך שחלק מהרכיבים ישתפו תהליך ואחרים לא.
אפשר גם להגדיר את android:process כך שרכיבים של אפליקציות שונות יפעלו באותו תהליך, בתנאי שהאפליקציות חולקות את אותו מזהה משתמש ב-Linux וחתומות באותם אישורים.
רכיב <application>
תומך גם במאפיין android:process, שבו אפשר להגדיר ערך ברירת מחדל שחל על כל הרכיבים.
יכול להיות שמערכת Android תחליט להפסיק תהליך בשלב מסוים, כשמשאבים נדרשים לתהליכים אחרים שמשרתים את המשתמש באופן מיידי יותר. רכיבי האפליקציה שפועלים בתהליך שנסגר נהרסים כתוצאה מכך. התהליך מתחיל מחדש עבור הרכיבים האלה כשיש להם עבודה לעשות.
כדי להחליט אילו תהליכים להפסיק, מערכת Android בודקת את החשיבות היחסית שלהם למשתמש. לדוגמה, המערכת סוגרת בקלות רבה יותר תהליך שמארח פעילויות שכבר לא מוצגות במסך, בהשוואה לתהליך שמארח פעילויות שמוצגות במסך. לכן, ההחלטה אם להפסיק תהליך תלויה במצב הרכיבים שפועלים בתהליך הזה.
פרטים על מחזור החיים של התהליך והקשר שלו למצבי האפליקציה מפורטים במאמר תהליכים ומחזור החיים של האפליקציה.
שרשורים
כשמפעילים אפליקציה, המערכת יוצרת שרשור ביצוע בשביל האפליקציה, שנקרא השרשור הראשי. השרשור הזה חשוב מאוד, כי הוא אחראי לשליחת אירועים לווידג'טים המתאימים בממשק המשתמש, כולל אירועי ציור. בנוסף, זה כמעט תמיד השרשור שבו האפליקציה שלכם יוצרת אינטראקציה עם רכיבים מחבילות android.widget ו-android.view של ערכת הכלים לבניית ממשק המשתמש ב-Android.
לכן, ה-thread הראשי נקרא לפעמים שרשור UI. עם זאת, בנסיבות מיוחדות, יכול להיות שה-thread הראשי של האפליקציה לא יהיה שרשור UI. מידע נוסף זמין במאמר בנושא הערות בשרשור.
המערכת לא יוצרת שרשור נפרד לכל מופע של רכיב. כל הרכיבים שפועלים באותו תהליך מופעלים בשרשור UI, והקריאות למערכת לכל רכיב נשלחות מהשרשור הזה. לכן, שיטות שמגיבות לקריאות חוזרות (callback) של המערכת – כמו onKeyDown()לדיווח על פעולות משתמש או שיטת קריאה חוזרת במחזור חיים – תמיד פועלות בשרשור UI של התהליך.
לדוגמה, כשמשתמש נוגע בלחצן במסך, השרשור של ממשק המשתמש של האפליקציה שולח את אירוע המגע לווידג'ט, שבתורו מגדיר את מצב הלחיצה שלו ושולח בקשת ביטול תוקף לתור האירועים. הבקשה מוצאת מהתור בשרשור UI והווידג'ט מקבל הודעה לצייר את עצמו מחדש.
אם לא מטמיעים את האפליקציה בצורה נכונה, מודל חד-הליכי כזה עלול להניב ביצועים ירודים כשהאפליקציה מבצעת עבודה אינטנסיבית בתגובה לאינטראקציה של המשתמש. ביצוע פעולות ארוכות בשרשור v-UI, כמו גישה לרשת או שאילתות במסד נתונים, חוסם את כל ממשק המשתמש. כשחוסמים את השרשור, אי אפשר לשלוח אירועים, כולל אירועי ציור.
מנקודת המבט של המשתמש, נראה שהאפליקציה מפסיקה להגיב. גרוע מכך, אם שרשור ה-UI נחסם למשך כמה שניות, מוצג למשתמש הדו-שיח האפליקציה לא מגיבה (ANR). במקרה כזה, המשתמש עשוי להחליט לצאת מהאפליקציה או אפילו להסיר אותה.
חשוב לזכור שערכת הכלים לבניית ממשק משתמש ב-Android לא בטוחה לשימוש עם שרשורים. לכן, אל תבצעו מניפולציות בממשק המשתמש מ-Thread עובד. כל השינויים בממשק המשתמש צריכים להתבצע בשרשור ממשק המשתמש. יש שני כללים למודל החד-ניבי של Android:
- אל תחסמו את שרשור ה-UI.
- אל תיגשו לערכת הכלים לבניית ממשק משתמש ב-Android מחוץ לשרשור UI.
Thread עובד
בגלל מודל השרשור היחיד הזה, חשוב מאוד לא לחסום את שרשור UI כדי שממשק המשתמש של האפליקציה יגיב במהירות. אם יש לכם פעולות לבצע שלא מתבצעות באופן מיידי, הקפידו לבצע אותן בנפרד בשרשורי רקע או בשרשורי worker. חשוב לזכור שאי אפשר לעדכן את ממשק המשתמש מכל שרשור שהוא, מלבד שרשור ממשק המשתמש או השרשור הראשי.
כדי לעזור לכם לפעול בהתאם לכללים האלה, מערכת Android מציעה כמה דרכים לגשת לשרשור UI מ-threads אחרים. ריכזנו כאן כמה שיטות שיכולות לעזור:
בדוגמאות הבאות מוסבר איך להעביר משימה לשרשור ברקע ולעדכן את שרשור UI אחרי שהמשימה מסתיימת:
Kotlin
// Kotlin coroutines implementation. fun onClick(v: View) { // Launch a coroutine in the lifecycle scope (e.g., in an Activity or Fragment). lifecycleScope.launch { // Run the blocking task on the IO dispatcher. val bitmap = withContext(Dispatchers.IO) { BitmapFactory.decodeFile("image.png") } // Back on the main thread, update the UI. imageView.setImageBitmap(bitmap) } }
Java
// Java Executor implementation. // (executorService is assumed to be defined elsewhere). public void onClick(View v) { executorService.execute(() -> { // Run the heavy task on a background thread. Bitmap bitmap = BitmapFactory.decodeFile("image.png"); // Update the View on the UI thread. imageView.post(() -> imageView.setImageBitmap(bitmap)); }); }
ההטמעה הזו בטוחה לשימוש בשרשור, כי הפעולה ברקע מתבצעת משרשור נפרד, בעוד שהמניפולציה של ImageView מתבצעת תמיד משרשור UI.
עם זאת, ככל שהפעולה מורכבת יותר, קוד כזה יכול להיות מסובך וקשה לתחזוקה. כדי לטפל באינטראקציות מורכבות יותר עם Thread עובד, אפשר להשתמש ב-Handler ב-Thread עובד כדי לעבד הודעות שנשלחות מ-שרשור UI. הסבר מלא על תזמון עבודה בשרשורים ברקע ועל תקשורת חזרה לשרשור ממשק המשתמש זמין במאמר סקירה כללית על עבודה ברקע.
שיטות thread-safe
במקרים מסוימים, השיטות שאתם מטמיעים נקראות מכמה שרשורים, ולכן צריך לכתוב אותן כך שהן יהיו בטוחות לשימוש בשרשורים.
זה נכון בעיקר לגבי שיטות שאפשר להפעיל מרחוק, כמו שיטות בשירות מאוגד. כשמתבצעת קריאה למתודה שהוטמעה ב-IBinder ומקורה באותו תהליך שבו IBinder פועל, המתודה מופעלת בשרשור של המתקשר.
עם זאת, אם הקריאה מגיעה מתהליך אחר, המתודה מופעלת בשרשור שנבחר מתוך מאגר של שרשורים שהמערכת מתחזקת באותו תהליך כמו IBinder.
היא לא מופעלת בשרשור ה-UI של התהליך.
לדוגמה, בעוד שהשיטה onBind() של שירות מופעלת משרשור UI של תהליך השירות, שיטות שמוטמעות באובייקט שמוחזר על ידי onBind(), כמו מחלקה משנית שמטמיעה שיטות של קריאה לשירות מרוחק (RPC), מופעלות משרשורים במאגר. יכול להיות שלשירות יהיו כמה לקוחות, ולכן יכול להיות שכמה שרשורים במאגר יפעילו את אותה שיטת IBinder בו-זמנית. לכן, צריך להטמיע את שיטות IBinder כך שיהיו בטוחות לשימוש בשרשורים.
באופן דומה, ספק תוכן יכול לקבל בקשות לנתונים שמקורן בתהליכים אחרים.
המחלקות ContentResolver ו-ContentProvider מסתירות את הפרטים של אופן הניהול של התקשורת בין התהליכים (IPC), אבל השיטות ContentProvider שמגיבות לבקשות האלה – השיטות query(), insert(), delete(), update() ו-getType() – נקראות ממאגר של שרשורים בתהליך של ספק התוכן, ולא מהשרשור של ממשק המשתמש של התהליך. יכול להיות שהקריאות לשיטות האלה יתבצעו ממספר רב של שרשורים בו-זמנית, ולכן גם הן צריכות להיות מיושמות בצורה שתבטיח בטיחות שרשורים.
תקשורת בין תהליכים (IPC)
מערכת Android מציעה מנגנון ל-IPC באמצעות RPC, שבו שיטה מופעלת על ידי פעילות או רכיב אחר של האפליקציה, אבל מבוצעת מרחוק בתהליך אחר, וכל תוצאה מוחזרת אל הקריאה החוזרת (caller). הפעולה הזו כוללת פירוק של קריאה למתודה והנתונים שלה לרמה שמערכת ההפעלה יכולה להבין, העברה שלה מהתהליך המקומי ומרחב הכתובות לתהליך המרוחק ומרחב הכתובות, ואז הרכבה מחדש ושחזור של הקריאה שם.
הערכים המוחזרים מועברים בכיוון ההפוך. Android מספקת את כל הקוד לביצוע עסקאות ה-IPC האלה, כך שאתם יכולים להתמקד בהגדרה ובהטמעה של ממשק התכנות של ה-RPC.
כדי לבצע IPC, האפליקציה צריכה להתחבר לשירות באמצעות bindService(). מידע נוסף זמין במאמר סקירה כללית על השירותים.