ANRs

כש-thread של ממשק המשתמש של אפליקציה ל-Android חסום למשך זמן רב מדי, מופיעה לא מתקבלת תגובה" (ANR) מופעלת. אם האפליקציה פועלת בחזית, מציגה למשתמש תיבת דו-שיח, כפי שמוצג באיור 1. בתיבת הדו-שיח של ה-ANR מוצגת למשתמש יש הזדמנות לצאת ידנית מהאפליקציה.

איור 1. תיבת דו-שיח בנושא ANR תוצג למשתמש

איור 1. תיבת דו-שיח בנושא ANR תוצג למשתמש

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

שגיאת ANR מופעלת באפליקציה כשמתקיים אחד מהתנאים הבאים:

  • תם הזמן הקצוב לתפוגה לשליחת קלט: אם האפליקציה לא הגיבה לקלט אירוע (כמו לחיצה על מקש או מגע במסך) תוך 5 שניות.
  • ביצוע השירות: אם שירות שהוצהר על ידי האפליקציה לא יכול לסיים ביצוע של Service.onCreate() ו Service.onStartCommand() לService.onBind() תוך כמה שניות.
  • לא מתבצעת קריאה ל-Service.startForeground() : אם האפליקציה משתמשת Context.startForegroundService() להפעלת שירות חדש בחזית, אבל השירות לא יתקשר ל-startForeground() תוך 5 שניות.
  • שידור של כוונה: אם BroadcastReceiver לא סיימו להפעיל אותו בפרק זמן מוגדר. אם יש באפליקציה פעילות בחזית. הזמן הקצוב לתפוגה הוא 5 שניות.
  • אינטראקציות של מתזמן המשימות: אם JobService לא מוחזר מ-JobService.onStartJob() או JobService.onStopJob() בטווח של שניות, או אם משימה שבוצעה ביוזמת המשתמש מתחילה והאפליקציה לא מתקשרת אל JobService.setNotification() תוך כמה זמן שניות אחרי הקריאה ל-JobService.onStartJob(). לגבי טירגוט לאפליקציות ב-Android מגרסה 13 ומטה, מקרי ה-ANR הם במצב שקט ולא ידווחו לאפליקציה. באפליקציות שמטרגטות ל-Android 14 ואילך, מקרי ה-ANR כוללים במפורש דווחו לאפליקציה.

אם זוהו מקרי ANR באפליקציה, אפשר להיעזר בהנחיות שבמאמר הזה כדי לאבחן ולפתור את הבעיה.

זיהוי הבעיה

אם כבר פרסמת את האפליקציה, אפשר להשתמש ב- תפקוד האפליקציה כדי לראות מידע על מקרי ANR באפליקציה שלך. אפשר להשתמש בהגדרות אחרות כלים לזיהוי מקרי ANR בשטח, אבל שימו לב שכלים של צד שלישי לא יכולים לדווח על מקרי ANR גרסאות ישנות יותר של Android (Android 10 ומטה), בניגוד לתפקוד האפליקציה.

תפקוד האפליקציה

תפקוד האפליקציה יכול לעזור לך לעקוב אחר שיעור מקרי ה-ANR של האפליקציה ולשפר אותו. תפקוד האפליקציה מודד כמה שיעורים של מקרי ANR:

  • שיעור ANR: אחוז המשתמשים הפעילים ביום (DAU) למקרי ANR מכל סוג שהוא.
  • שיעור מקרי ה-ANR שהשפיעו על המשתמשים: אחוז המשתמשים הפעילים ביום (DAU) שחוו לפחות מקרה ANR אחד שהשפיעו על המשתמשים. כרגע רק מקרי ANR של סוגים Input dispatching timed out נחשבים להשפיעו על המשתמשים.
  • שיעור מקרי ה-ANR מרובים: אחוז המשתמשים הפעילים ביום (DAU) חוו לפחות שני מקרי ANR.

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

שיעור מקרי ה-ANR שהשפיעו על המשתמשים הוא תפקוד ליבה, כלומר הוא משפיע על יכולת הגילוי של האפליקציה ב-Google Play. זה חשוב כי במקרי ANR ספירות מתרחשות תמיד כאשר המשתמש יוצר אינטראקציה עם האפליקציה, וכתוצאה מכך שיבוש.

מערכת Play הגדירה שני ערכי סף של התנהגות לא תקינה במדד הזה:

  • סף התנהגות לא תקינה: לפחות 0.47% מהמשתמשים הפעילים ביום ומקרי ANR שהשפיעו על המשתמשים בכל דגמי המכשירים.
  • סף התנהגות לא תקינה לפי מכשיר: לפחות 8% מהמשתמשים היומיים מקרה ANR שהשפיעו על המשתמשים, בדגם מכשיר אחד.

אם האפליקציה חורגת מסף ההתנהגות הלא תקינה הכולל, סביר להניח שהיא פחות גלוי בכל המכשירים. אם האפליקציה שלך חורגת מהמגבלה לכל מכשיר את סף ההתנהגות במכשירים מסוימים, סביר להניח שהדבר יפגע ביכולת הגילוי שלו מכשירים כאלה, ויכול להיות שתוצג אזהרה בדף האפליקציה בחנות.

התכונה 'תפקוד האפליקציה' יכולה לשלוח לך התראה דרך Play Console, כשבאפליקציה יש כמות גדולה של מקרי ANR.

כדי לקבל מידע על אופן האיסוף של נתוני תפקוד האפליקציה ב-Google Play, אפשר לעיין במאמר Play Console התיעוד.

אבחון מקרי ANR

יש כמה דפוסים נפוצים שכדאי לחפש בעת אבחון מקרי ANR:

  • האפליקציה מבצעת פעולות איטיות שכוללות קלט/פלט (I/O) ב-thread הראשי.
  • האפליקציה מבצעת חישוב ארוך ב-thread הראשי.
  • ה-thread הראשי מבצע קריאה סינכרונית של קישור (Binder) לתהליך אחר, לוקח הרבה זמן עד שתהליך מסוים חוזר.
  • ה-thread הראשי חסום בהמתנה לבלוק מסונכרן למשך זמן ארוך שמתרחשת בשרשור אחר.
  • ה-thread הראשי נמצא בקיפאון עם שרשור אחר, או באמצעות קריאה ל-binder. ה-thread הראשי לא רק ממתין למשך זמן כדי לסיים, אבל הוא במצב של מבוי סתום. לקבלת מידע נוסף, אפשר לקרוא את Deadlock בוויקיפדיה.

תוכלו להיעזר בשיטות הבאות כדי לקבוע את הגורם למקרי ה-ANR.

נתונים סטטיסטיים

HealthStats מספק מדדים לגבי את התקינות של אפליקציה באמצעות תיעוד הזמן הכולל של המשתמש והמערכת, זמן המעבד (CPU), הרשת, נתונים סטטיסטיים של הרדיו, זמן ההפעלה/כיבוי של המסך והשעון המעורר. מי יכול עזרה במדידה של השימוש הכולל במעבד וצריכת הסוללה.

ניפוי באגים

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

נתוני יציאה של אפליקציה

הדומיין ApplicationExitInfo זמין ב-Android 11 (API ברמה 30) ואילך, ומספק מידע על הסיבה ליציאה מהאפליקציה. דוגמאות למקרי ANR, מחסור בזיכרון, קריסות של אפליקציות שימוש מוגזם במעבד (CPU), הפרעות למשתמשים, הפרעות במערכת או זמן ריצה שינויים בהרשאות.

מצב קפדני

השימוש ב-StrictMode עוזר לך למצוא פעולות קלט/פלט מקריות ב-thread הראשי בזמן פיתוח האפליקציה. אפשר להשתמש ב-StrictMode ברמת האפליקציה או הפעילות.

הפעלת תיבות דו-שיח ברקע בנושא ANR

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

Traceview

אפשר להשתמש ב-Traceview כדי לקבל תיעוד של האפליקציה הפעילה בזמן שעוברים תרחישים לדוגמה ולזהות את המקומות שבהם ה-thread הראשי עמוס. למידע לקבלת מידע נוסף על השימוש ב-Traceview, אפשר לעיין במאמר יצירת פרופילים באמצעות Traceview dmtracedump.

אחזור של קובץ מעקב

מערכת Android שומרת את פרטי המעקב כשהיא מזהה מקרה ANR. במערכות הפעלה ישנות יותר בגרסאות השונות, יש קובץ /data/anr/traces.txt אחד במכשיר. בגרסאות חדשות יותר של מערכת ההפעלה יש מספר קבצים מסוג /data/anr/anr_*. אפשר לגשת למעקבי ANR ממכשיר או מאמולטור באמצעות Android Debug Bridge (adb) ברמה הבסיסית (root):

adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>

אפשר לתעד דוח על באג ממכשיר פיזי באמצעות שתי הפעולות הבאות: לדווח על אפשרות למפתחים במכשיר, או על הפקודה adb report report ב- מכונה של פיתוח. מידע נוסף זמין במאמר תיעוד וקריאה של באג .

תיקון הבעיות

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

הזנת קוד איטי בשרשור הראשי

צריך לזהות את המקומות בקוד שבהם ה-thread הראשי של האפליקציה עמוס, מ-5 שניות. צריך לחפש את התרחישים החשודים לדוגמה באפליקציה ולנסות לשחזר את שגיאת ה-ANR.

לדוגמה, באיור 2 מוצג ציר זמן של Traceview שבו ה-thread הראשי עמוס. למשך יותר מ-5 שניות.

איור 2. ציר הזמן של Traceview שבו מוצג אזור ראשי עמוס
שרשור

איור 2. ציר הזמן של Traceview שבו מוצג שרשור ראשי עמוס

איור 2 מראה שרוב הקוד הפוגעני מופיע onClick(View) handler, כפי שמוצג בדוגמת הקוד הבאה:

Kotlin

override fun onClick(v: View) {
    // This task runs on the main thread.
    BubbleSort.sort(data)
}

Java

@Override
public void onClick(View view) {
    // This task runs on the main thread.
    BubbleSort.sort(data);
}

במקרה כזה, צריך להעביר לעובד אחר את העבודה שרצתה ב-thread הראשי של שרשור. Android Framework כולל מחלקות שיכולות לעזור בהעברת המשימה לשרשור של עובדים. הצגת עובד שרשורים למידע נוסף מידע.

IO ב-thread הראשי

ביצוע פעולות IO ב-thread הראשי היא סיבה נפוצה לפעולות איטיות ב-thread הראשי, שעלול לגרום לשגיאות ANR. מומלץ להעביר את כל הקלט (IO) לשרשורים של עובדים, כפי שמתואר בקטע הקודם.

דוגמאות לפעולות IO הן פעולות רשת ואחסון. לקבלת מידע נוסף מידע נוסף, ראו ביצוע רשת פעולות ושמירה .

תחרות על נעילה

בתרחישים מסוימים, העבודה שגורמת ל-ANR לא מבוצעת ישירות בשרשור הראשי של האפליקציה. אם שרשור של Worker שומר נעילה על משאב שעיקר נדרש ל-thread כדי להשלים את העבודה, אז יכול להיות שמתרחש שגיאת ANR.

לדוגמה, איור 3 מציג ציר זמן של Traceview שבו רוב העבודה בוצע בשרשור של עובדים.

איור 3. ציר זמן של Traceview שבו רואים את הביצוע של העבודה על עובד
שרשור

איור 3. ציר זמן של Traceview שבו רואים את הביצוע של העבודה על עובד שרשור

אבל אם המשתמשים שלך עדיין חווים מקרי ANR, צריך לבדוק את הסטטוס של ה-thread הראשי ב-Android Device Monitor. בדרך כלל, ה-thread הראשי נמצא RUNNABLE הוא מוכן לעדכון ממשק המשתמש והוא מגיב בדרך כלל.

אבל אם לא ניתן להמשיך את ההפעלה של ה-thread הראשי, הוא נמצא מצב BLOCKED ולא יכולים להגיב לאירועים. המצב מופיע ב-Android Device Monitor בתור עוקבים או ממתינים, כפי שמוצג באיור 5.

איור 4. ה-thread הראשי בצג
סטטוס

איור 4. ה-thread הראשי בסטטוס 'מעקב'

במעקב הבא מוצג ה-thread הראשי של האפליקציה שנחסם בזמן ההמתנה resource:

...
AsyncTask #2" prio=5 tid=18 Runnable
  | group="main" sCount=0 dsCount=0 obj=0x12c333a0 self=0x94c87100
  | sysTid=25287 nice=10 cgrp=default sched=0/0 handle=0x94b80920
  | state=R schedstat=( 0 0 0 ) utm=757 stm=0 core=3 HZ=100
  | stack=0x94a7e000-0x94a80000 stackSize=1038KB
  | held mutexes= "mutator lock"(shared held)
  at com.android.developer.anrsample.BubbleSort.sort(BubbleSort.java:8)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:147)
  - locked <0x083105ee> (a java.lang.Boolean)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:135)
  at android.os.AsyncTask$2.call(AsyncTask.java:305)
  at java.util.concurrent.FutureTask.run(FutureTask.java:237)
  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
  at java.lang.Thread.run(Thread.java:761)
...

כדאי לבדוק את המעקב כדי לאתר את הקוד שחוסם את ה-thread הראשי. הקוד הבא אחראי להחזיק את המנעול שחוסם את שרשור במעקב הקודם:

Kotlin

override fun onClick(v: View) {
    // The worker thread holds a lock on lockedResource
    LockTask().execute(data)

    synchronized(lockedResource) {
        // The main thread requires lockedResource here
        // but it has to wait until LockTask finishes using it.
    }
}

class LockTask : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? =
            synchronized(lockedResource) {
                // This is a long-running operation, which makes
                // the lock last for a long time
                BubbleSort.sort(params[0])
            }
}

Java

@Override
public void onClick(View v) {
    // The worker thread holds a lock on lockedResource
   new LockTask().execute(data);

   synchronized (lockedResource) {
       // The main thread requires lockedResource here
       // but it has to wait until LockTask finishes using it.
   }
}

public class LockTask extends AsyncTask<Integer[], Integer, Long> {
   @Override
   protected Long doInBackground(Integer[]... params) {
       synchronized (lockedResource) {
           // This is a long-running operation, which makes
           // the lock last for a long time
           BubbleSort.sort(params[0]);
       }
   }
}

דוגמה נוספת היא ה-thread הראשי של אפליקציה שממתין לתוצאה בפרוטוקול worker, כפי שמוצג בקוד הבא. הערה: שימוש ב-wait() וב- לא מומלץ להשתמש בדפוס notify() ב-Kotlin, ויש לו מנגנונים משלו לטיפול בו-זמניות (concurrency). כשמשתמשים ב-Kotlin יש להשתמש ב-Kotlin ומנגנונים, אם אפשר.

Kotlin

fun onClick(v: View) {
    val lock = java.lang.Object()
    val waitTask = WaitTask(lock)
    synchronized(lock) {
        try {
            waitTask.execute(data)
            // Wait for this worker thread’s notification
            lock.wait()
        } catch (e: InterruptedException) {
        }
    }
}

internal class WaitTask(private val lock: java.lang.Object) : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        synchronized(lock) {
            BubbleSort.sort(params[0])
            // Finished, notify the main thread
            lock.notify()
        }
    }
}

Java

public void onClick(View v) {
   WaitTask waitTask = new WaitTask();
   synchronized (waitTask) {
       try {
           waitTask.execute(data);
           // Wait for this worker thread’s notification
           waitTask.wait();
       } catch (InterruptedException e) {}
   }
}

class WaitTask extends AsyncTask<Integer[], Integer, Long> {
   @Override
   protected Long doInBackground(Integer[]... params) {
       synchronized (this) {
           BubbleSort.sort(params[0]);
           // Finished, notify the main thread
           notify();
       }
   }
}

יש מצבים אחרים שבהם אפשר לחסום את ה-thread הראשי, כולל שרשורים שמשתמשים ב-Lock, Semaphore, וגם מאגר משאבים (כמו מאגר של חיבורי מסד נתונים) או מנגנונים אחרים של הדרה (mutex) הדדיים.

באופן כללי, כדאי להעריך את הנעילות שהאפליקציה שלך מחזיקה במשאבים, אבל אם רוצים למנוע מקרי ANR, צריך לבדוק את המנעולים שמוחזקים למשאבים שנדרשת ל-thread הראשי.

ודאו שהנעילות מוחזקות למשך הזמן הקצר ביותר, או אפילו טוב יותר, להעריך אם האפליקציה צריכה את ההשהיה מלכתחילה. אם משתמשים כדי לקבוע מתי לעדכן את ממשק המשתמש בהתבסס על עיבוד של שרשור עובד, משתמשים במנגנונים כמו onProgressUpdate() וגם onPostExecute() לצורך תקשורת בין העובד לבין השרשורים הראשיים.

מנעולים

מצב קיפאון מתרחש כששרשור נכנס למצב המתנה עקב דרישה משאב מוחזק על ידי שרשור אחר, שממתין גם הוא למשאב בשרשור הראשון. אם במצב הזה נמצא ה-thread הראשי של האפליקציה, סביר להניח שמקרי ANR עד שזה יקרה.

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

אפשר לקרוא מידע נוסף בקטע נעילה וגם מניעת מבוי סתום אלגוריתמים מופעלים ויקיפדיה.

מקלטי שידורים איטיים

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

שגיאת ANR מתרחשת במקרים הבאים:

  • מקלט השידורים עדיין לא סיים להפעיל onReceive() בשיטה הזו בתוך פרק זמן לא מבוטל.
  • מקלט שידורים מתקשר goAsync() ולא ניתן לקרוא finish() ב PendingResult לאובייקט.

האפליקציה צריכה לבצע פעולות קצרות בלבד onReceive() שיטה BroadcastReceiver עם זאת, אם האפליקציה מחייבת כתוצאה מהודעת שידור, עליך לדחות את המשימה IntentService

אפשר להשתמש בכלים כמו Traceview כדי לזהות אם מקלט השידור פועל פעולות ממושכות ב-thread הראשי של האפליקציה. לדוגמה, איור מספר 6 ציר הזמן של מקלט שידורים שמעבד הודעות בשרשור הראשי למשך כ-100 שניות.

איור 5. ציר הזמן של Traceview שמציג את העבודה &#39;broadcastReceiver&#39; בערוץ הראשי
שרשור

איור 5. ציר הזמן של Traceview שמציג את העבודה של BroadcastReceiver ה-thread הראשי

ההתנהגות הזו עשויה לנבוע מביצוע פעולות ממושכות onReceive() ה-method של BroadcastReceiver, כפי שאפשר לראות בדוגמה הבאה:

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    // This is a long-running operation
    BubbleSort.sort(data)
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    // This is a long-running operation
    BubbleSort.sort(data);
}

במצבים כאלה, מומלץ להעביר את הפעולה הממושכת IntentService כי נעשה בו שימוש שרשור עובד לביצוע העבודה. הקוד הבא מראה איך להשתמש IntentService כדי לעבד פעולה ממושכת:

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    Intent(context, MyIntentService::class.java).also { intentService ->
        // The task now runs on a worker thread.
        context.startService(intentService)
    }
}

class MyIntentService : IntentService("MyIntentService") {
    override fun onHandleIntent(intent: Intent?) {
        BubbleSort.sort(data)
    }
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    // The task now runs on a worker thread.
    Intent intentService = new Intent(context, MyIntentService.class);
    context.startService(intentService);
}

public class MyIntentService extends IntentService {
   @Override
   protected void onHandleIntent(@Nullable Intent intent) {
       BubbleSort.sort(data);
   }
}

כתוצאה מהשימוש ב- IntentService, מתבצעת פעולה בשרשור של עובד במקום בשרשור הראשי. איור 7 מציגה את העבודה נדחתה ל-thread של העובד בציר הזמן של Traceview.

איור 6. ציר הזמן של Traceview שמציג את הודעת השידור שעובדה
שרשור עובד

איור 6. ציר הזמן של Traceview שמציג את הודעת השידור שעובדה שרשור עובד

מקלט השידור שלך יכול להשתמש goAsync() כדי לאותת למערכת שנדרש זמן נוסף לעיבוד ההודעה. אבל, לפעמים כדאי להתקשר finish() ב PendingResult לאובייקט. הדוגמה הבאה מראה איך לקרוא ל-Finish() כדי לאפשר למערכת מיחזור של מקלט השידור ומניעת ANR:

Kotlin

val pendingResult = goAsync()

object : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        // This is a long-running operation
        BubbleSort.sort(params[0])
        pendingResult.finish()
        return 0L
    }
}.execute(data)

Java

final PendingResult pendingResult = goAsync();
new AsyncTask<Integer[], Integer, Long>() {
   @Override
   protected Long doInBackground(Integer[]... params) {
       // This is a long-running operation
       BubbleSort.sort(params[0]);
       pendingResult.finish();
   }
}.execute(data);

עם זאת, העברת הקוד ממקלט של שידור איטי לשרשור אחר באמצעות goAsync() לא תתקן את שגיאת ה-ANR אם השידור מתבצע ברקע. הזמן הקצוב לתפוגה של שגיאת ה-ANR עדיין חל.

פעילות משחק

בספרייה GameActivity צמצמו מקרי ANR ב- מקרים לדוגמה של משחקים ואפליקציות שנכתבו ב-C או ב-C++. אם מחליפים את הפעילות המותאמת הקיימת בפעילות GameActivity, אפשר לצמצם את החסימה של השרשורים בממשק המשתמש ולמנוע מקרי ANR מסוימים.

מידע נוסף על מקרי ANR זמין במאמר הקפדה על כך שהאפליקציה תהיה רספונסיבית. לקבלת מידע נוסף על שרשורים, ביצועי השרשורים.