רינדור של ממשק המשתמש הוא פעולה של יצירת פריים מהאפליקציה והצגתו במסך. כדי להבטיח שהאינטראקציה של המשתמש עם האפליקציה תהיה חלקה, האפליקציה צריכה להציג פריימים בפחות מ-16 אלפיות השנייה כדי להגיע ל-60 פריימים לשנייה (fps). כדי להבין למה עדיף להשתמש ב-60 fps, אפשר לעיין במאמר Android Performance Patterns: Why 60fps?. אם מנסים להגיע ל-90 fps, אז החלון הזה יורד ל-11ms, ואם מנסים להגיע ל-120 fps, הוא יורד ל-8ms.
אם חורגים מהחלון הזה במילי-שנייה אחת, זה לא אומר שהפריים מוצג באיחור של מילי-שנייה אחת, אלא ש-Choreographer משמיט את הפריים לחלוטין. אם העיבוד של ממשק המשתמש באפליקציה איטי, המערכת נאלצת לדלג על פריימים והמשתמשים חווים גמגום באפליקציה. התופעה הזו נקראת jank. בדף הזה מוסבר איך לאבחן ולתקן בעיות של jank.
אם אתם מפתחים משחקים שלא משתמשים במערכת View, אתם לא צריכים להשתמש ב-Choreographer. במקרה כזה, ספריית Frame Pacing עוזרת למשחקי OpenGL ו-Vulkan להשיג רינדור חלק וקצב פריימים נכון ב-Android.
זיהוי בעיות בממשק (jank)
יכול להיות שיהיה קשה למצוא באפליקציה את הקוד שגורם לבעיות בממשק (jank). בקטע הזה מתוארות שלוש שיטות לזיהוי בעיות בממשק (jank):
בדיקה ויזואלית מאפשרת לעבור על כל תרחישי השימוש באפליקציה בכמה דקות, אבל היא לא מספקת פרטים כמו Systrace. Systrace מספק פרטים נוספים, אבל אם מריצים את Systrace לכל תרחישי השימוש באפליקציה, יכול להיות שיוצגו לכם כל כך הרבה נתונים שיהיה קשה לנתח אותם. גם בדיקה ויזואלית וגם Systrace מזהות ג'אנק במכשיר המקומי. אם אתם לא מצליחים לשחזר את הבעיה במכשירים מקומיים, אתם יכולים ליצור מעקב ביצועים בהתאמה אישית כדי למדוד חלקים ספציפיים באפליקציה במכשירים שפועלים בשטח.
בדיקה ויזואלית
הבדיקה החזותית עוזרת לזהות את תרחישי השימוש שגורמים לבעיות בממשק (jank). כדי לבצע בדיקה ויזואלית, פותחים את האפליקציה ועוברים באופן ידני על החלקים השונים שלה כדי לחפש תנודות בממשק המשתמש.
ריכזנו כאן כמה טיפים לביצוע בדיקות ויזואליות:
- מריצים גרסת הפצה של האפליקציה – או לפחות גרסה שלא ניתן לבצע בה ניפוי באגים. סביבת הריצה של ART משביתה כמה אופטימיזציות חשובות כדי לתמוך בתכונות של ניפוי באגים, לכן חשוב לוודא שאתם רואים משהו דומה למה שמשתמש רואה.
- מפעילים את האפשרות 'עיבוד פרופיל ב-GPU'. הכלי Profile GPU Rendering מציג עמודות במסך שנותנות ייצוג ויזואלי של הזמן שלוקח לעבד את הפריים של חלון ממשק המשתמש ביחס לנקודת ההשוואה של 16 אלפיות השנייה לכל פריים. לכל עמודה יש רכיבים צבעוניים שמייצגים שלב בצינור העיבוד, כך שאפשר לראות איזה חלק לוקח הכי הרבה זמן. לדוגמה, אם הפריים מבלה הרבה זמן בטיפול בקלט, כדאי לבדוק את קוד האפליקציה שמטפל בקלט של המשתמשים.
- כדאי לעבור על רכיבים שהם מקורות נפוצים לבעיות בממשק (jank), כמו
RecyclerView. - מפעילים את האפליקציה מהתחלה קרה.
- מריצים את האפליקציה במכשיר איטי יותר כדי להחמיר את הבעיה.
כשמזהים תרחישי שימוש שגורמים ל-jank, אפשר להבין מה גורם ל-jank באפליקציה. אם אתם צריכים מידע נוסף, אתם יכולים להשתמש ב-Systrace כדי לבדוק את הסיבה לעומק.
Systrace
למרות ש-Systrace הוא כלי שמציג את הפעילות של המכשיר כולו, הוא יכול להיות שימושי לזיהוי גמגום באפליקציה. ל-Systrace יש תקורה מינימלית של המערכת, כך שתוכלו לחוות גמגום ריאליסטי במהלך המדידה.
מבצעים הקלטה של נתוני מעקב באמצעות Systrace בזמן שמבצעים את תרחיש השימוש הבעייתי במכשיר. הוראות לשימוש ב-Systrace מופיעות במאמר תיעוד עקבות המערכת בשורת הפקודה. הנתונים ב-Systrace מחולקים לפי תהליכים ושרשורים. מחפשים את התהליך של האפליקציה ב-Systrace, שדומה למה שמוצג באיור 1.
הדוגמה של Systrace באיור 1 מכילה את הפרטים הבאים לזיהוי של בעיות בממשק (jank):
- ב-Systrace מוצג מתי כל פריים מצויר, וכל פריים מקודד בצבע כדי להדגיש זמני רינדור איטיים. כך תוכלו למצוא מסגרות ספציפיות עם בעיות בצורה מדויקת יותר מאשר בבדיקה ויזואלית. מידע נוסף זמין במאמר בדיקת מסגרות והתראות בממשק המשתמש.
- Systrace מזהה בעיות באפליקציה ומציג התרעות גם בפריים בודד וגם בחלונית ההתרעות. מומלץ לפעול לפי ההוראות שמופיעות בהתראה.
- חלקים מה-framework והספריות של Android, כמו
RecyclerView, מכילים סמני מעקב. לכן, ציר הזמן של systrace מראה מתי השיטות האלה מופעלות בשרשור של ממשק המשתמש וכמה זמן לוקח להן לפעול.
אחרי שמעיינים בפלט של Systrace, יכול להיות שיהיו באפליקציה שיטות שמעוררות חשד לגבי גורם אפשרי לבעיה. לדוגמה, אם ציר הזמן מראה שפריים איטי נגרם בגלל RecyclerView שלוקח לו הרבה זמן, אפשר להוסיף אירועי מעקב מותאמים אישית לקוד הרלוונטי ולהריץ מחדש את Systrace כדי לקבל מידע נוסף. ב-Systrace החדש, ציר הזמן מראה מתי נקראות השיטות של האפליקציה וכמה זמן לוקח להן לפעול.
אם Systrace לא מציג פרטים לגבי הסיבה לכך שעבודה בשרשור UI נמשכת זמן רב, אפשר להשתמש ב-Android CPU Profiler כדי להקליט תיעוד method עם דגימה או עם אינסטרומנטציה. בדרך כלל, עקבות של שיטות לא מתאימות לזיהוי של jank כי הן יוצרות jank חיובי כוזב בגלל תקורה כבדה, והן לא יכולות לראות מתי השרשורים פועלים ומתי הם חסומים. אבל, אפשר להשתמש במעקב אחר שיטות כדי לזהות את השיטות באפליקציה שלוקחות הכי הרבה זמן. אחרי שמזהים את השיטות האלה, מוסיפים סמני מעקב ומריצים מחדש את Systrace כדי לבדוק אם השיטות האלה גורמות לבעיות בממשק (jank).
מידע נוסף זמין במאמר בנושא הסבר על Systrace.
מעקב מותאם אישית אחרי הביצועים
אם אתם לא מצליחים לשחזר את הבעיה במכשיר מקומי, אתם יכולים להוסיף לאפליקציה כלי מותאם אישית למעקב אחרי הביצועים, כדי לזהות את מקור הבעיה במכשירים בשטח.
כדי לעשות את זה, אוספים את זמני העיבוד של הפריים מחלקים ספציפיים באפליקציה באמצעות
FrameMetricsAggregator
ומתעדים ומנתחים את הנתונים באמצעות Firebase Performance Monitoring.
מידע נוסף מופיע במאמר תחילת השימוש ב-Performance Monitoring ל-Android.
פריימים קפואים
פריימים קפואים הם פריימים של ממשק המשתמש שזמן הרינדור שלהם ארוך מ-700 אלפיות השנייה. זו בעיה כי נראה שהאפליקציה נתקעת ולא מגיבה לקלט של המשתמש כמעט למשך שנייה שלמה בזמן שהפריים מוצג. מומלץ לבצע אופטימיזציה של אפליקציות כדי להציג פריים תוך 16 אלפיות השנייה, וכך להבטיח ממשק משתמש חלק. עם זאת, במהלך הפעלת האפליקציה או המעבר למסך אחר, נורמלי שהפריימים הראשוניים יימשכו יותר מ-16 אלפיות השנייה, כי האפליקציה צריכה להרחיב את התצוגות, להגדיר את המסך ולבצע את הציור הראשוני מאפס. לכן, ב-Android, פריימים קפואים נספרים בנפרד מרינדור איטי. הרינדור של אף פריים באפליקציה לא צריך להימשך יותר מ-700 אלפיות השנייה.
פריים קפוא הוא צורה קיצונית של רינדור איטי, ולכן התהליך לאבחון ולתיקון הבעיה זהה.
מעקב אחרי בעיות בממשק (jank)
FrameTimeline ב-Perfetto יכול לעזור לעקוב אחרי פריימים איטיים או קפואים.
הקשר בין פריימים איטיים, פריימים קפואים ומקרי ANR
פריימים איטיים, פריימים קפואים ו-ANR הם סוגים שונים של באגים שעלולים להתרחש באפליקציה. הטבלה שלמטה עוזרת להבין את ההבדל.
| פריימים איטיים | פריימים קפואים | מקרי ANR | |
|---|---|---|---|
| זמן עיבוד | בין 16 אלפיות השנייה ל-700 אלפיות השנייה | בין 700 אלפיות השנייה ל-5 שניות | יותר מ-5 שניות |
| אזור ההשפעה הגלוי למשתמשים |
|
|
|
מעקב נפרד אחרי פריימים איטיים ופריימים קפואים
במהלך הפעלת האפליקציה או המעבר למסך אחר, בדרך כלל לוקח יותר מ-16 אלפיות השנייה לשרטוט הפריימים הראשוניים, כי האפליקציה צריכה להרחיב את התצוגות, לסדר את המסך ולבצע את השרטוט הראשוני מאפס.
שיטות מומלצות לקביעת סדר עדיפויות ולפתרון של בעיות ג'אנק
כדי לפתור בעיות של גמגום באפליקציה, כדאי לפעול לפי השיטות המומלצות הבאות:
- לזהות ולפתור את המקרים הכי קלים לשחזור של ג'אנק.
- קובעים סדרי עדיפויות למקרי ANR. מסגרות איטיות או קפואות עשויות לגרום לאפליקציה להיראות איטית, אבל שגיאות ANR גורמות לאפליקציה להפסיק להגיב.
- קשה לשחזר רינדור איטי, אבל אפשר להתחיל בהפסקת פריימים קפואים של 700ms. הבעיה הזו נפוצה בעיקר בזמן הפעלת האפליקציה או בזמן מעבר בין מסכים.
תיקון בעיות בממשק (jank)
כדי לתקן את הבעיה, בודקים אילו פריימים לא הושלמו תוך 16 אלפיות השנייה ומחפשים את הבעיה. בודקים אם Record View#draw או Layout נמשכים זמן ארוך באופן חריג בחלק מהפריימים. במאמר מקורות נפוצים לבעיות בממשק (jank) מפורטות הבעיות האלה ובעיות נוספות.
כדי להימנע מבעיות בממשק (jank), מריצים משימות ארוכות באופן אסינכרוני מחוץ לשרשור UI. חשוב תמיד לדעת באיזה שרשור הקוד פועל, ולהיזהר כשמפרסמים משימות לא טריוויאליות בשרשור הראשי.
אם יש לאפליקציה ממשק משתמש ראשי מורכב וחשוב – כמו רשימת גלילה מרכזית – כדאי לכתוב בדיקות מכשור שיכולות לזהות באופן אוטומטי זמני עיבוד ארוכים ולהריץ את הבדיקות לעיתים קרובות כדי למנוע רגרסיות.
מקורות נפוצים של בעיות בממשק (jank)
בקטעים הבאים מוסבר על מקורות נפוצים של תופעת ה-jank באפליקציות שמשתמשות במערכת Viewומוצגות שיטות מומלצות לטיפול בהם. מידע על פתרון בעיות שקשורות לביצועים ב-Jetpack Compose זמין במאמר ביצועים ב-Jetpack Compose.
רשימות עם אפשרות גלילה
ListView – ובמיוחד RecyclerView – משמשים בדרך כלל לרשימות גלילה מורכבות שמועדות במיוחד ל-jank. שניהם מכילים סמני Systrace, כך שאפשר להשתמש ב-Systrace כדי לראות אם הם תורמים לבעיות בביצועים באפליקציה. מעבירים את ארגומנט שורת הפקודה -a
<your-package-name> כדי שקטעי המעקב יופיעו ב-RecyclerView, וגם כל סמני המעקב שהוספתם. אם יש התראות, פועלים לפי ההנחיות שמופיעות בפלט של Systrace. ב-Systrace, אפשר ללחוץ על הקטעים המסומנים RecyclerViewכדי לראות הסבר על העבודה ש-RecyclerView מבצעת.
RecyclerView: notifyDataSetChanged()
אם אתם רואים שכל פריט ב-RecyclerView עובר איגוד מחדש – ולכן נפרס מחדש ומצויר מחדש בפריים אחד – ודאו שאתם לא קוראים ל-notifyDataSetChanged(), ל-setAdapter(Adapter) או ל-swapAdapter(Adapter,
boolean) לעדכונים קטנים. השיטות האלה מציינות שיש שינויים בתוכן של כל הרשימה, והן מופיעות ב-Systrace כ-RV FullInvalidate. במקום זאת, אפשר להשתמש ב-SortedList או ב-DiffUtil כדי ליצור עדכונים מינימליים כשתוכן משתנה או מתווסף.
לדוגמה, נניח שיש אפליקציה שמקבלת גרסה חדשה של רשימת תוכן חדשותי משרת. כשמפרסמים את המידע הזה במתאם, אפשר לקרוא ל-notifyDataSetChanged(), כמו שמוצג בדוגמה הבאה:
Kotlin
fun onNewDataArrived(news: List<News>) { myAdapter.news = news myAdapter.notifyDataSetChanged() }
Java
void onNewDataArrived(List<News> news) { myAdapter.setNews(news); myAdapter.notifyDataSetChanged(); }
החיסרון בגישה הזו הוא שאם יש שינוי קל, כמו הוספה של פריט אחד לראש הרשימה, RecyclerView לא מודע לכך. לכן, הוא מקבל הוראה להפסיק את כל המידע מהמצב הסמוי של הפריט, ולכן צריך לקשר מחדש את הכול.
מומלץ להשתמש ב-DiffUtil, שמחשב ושולח עדכונים מינימליים בשבילכם:
Kotlin
fun onNewDataArrived(news: List<News>) { val oldNews = myAdapter.items val result = DiffUtil.calculateDiff(MyCallback(oldNews, news)) myAdapter.news = news result.dispatchUpdatesTo(myAdapter) }
Java
void onNewDataArrived(List<News> news) { List<News> oldNews = myAdapter.getItems(); DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news)); myAdapter.setNews(news); result.dispatchUpdatesTo(myAdapter); }
כדי להסביר ל-DiffUtil איך לבדוק את הרשימות, צריך להגדיר את MyCallback כהטמעה של Callback.
RecyclerView: Nested RecyclerViews
מקובל להשתמש בכמה מקרים של RecyclerView בתוך RecyclerView, במיוחד כשמדובר ברשימה אנכית של רשימות אופקיות לגלילה. דוגמה לכך היא רשתות האפליקציות בדף הראשי של חנות Play. השיטה הזו יכולה לעבוד מצוין, אבל היא גם כוללת הרבה תצוגות שמועברות ממקום למקום.
אם אתם רואים הרבה פריטים פנימיים שמתרחבים כשאתם גוללים למטה בדף בפעם הראשונה,
כדאי לבדוק שאתם משתפים את
RecyclerView.RecycledViewPool
בין מופעים פנימיים (אופקיים) של RecyclerView. כברירת מחדל, לכל RecyclerView יש מאגר פריטים משלו. אבל אם יש תריסר itemViews על המסך בו-זמנית, זה בעייתי אם אי אפשר לשתף את itemViews בין הרשימות האופקיות השונות, אם בכל השורות מוצגים סוגים דומים של תצוגות.
Kotlin
class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() { ... override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { // Inflate inner item, find innerRecyclerView by ID. val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false) innerRv.apply { layoutManager = innerLLM recycledViewPool = sharedPool } return OuterAdapter.ViewHolder(innerRv) } ...
Java
class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> { RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool(); ... @Override public void onCreateViewHolder(ViewGroup parent, int viewType) { // Inflate inner item, find innerRecyclerView by ID. LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL); innerRv.setLayoutManager(innerLLM); innerRv.setRecycledViewPool(sharedPool); return new OuterAdapter.ViewHolder(innerRv); } ...
כדי לבצע אופטימיזציה נוספת, אפשר גם להתקשר אל
setInitialPrefetchItemCount(int)
בLinearLayoutManager
של RecyclerView הפנימי. לדוגמה, אם תמיד מוצגים 3.5 פריטים בשורה, צריך להפעיל את הפונקציה innerLLM.setInitialItemPrefetchCount(4). האות הזה מציין ל-RecyclerView שכאשר שורה אופקית עומדת להופיע במסך, המערכת צריכה לנסות לאחזר מראש את הפריטים שבתוכה אם יש זמן פנוי בשרשור של ממשק המשתמש.
RecyclerView: פעולת ה-inflation אורכת זמן רב מדי או שפעולת ה-Create אורכת זמן רב מדי
ברוב המקרים, התכונה 'טעינה מראש' ב-RecyclerView יכולה לעזור לעקוף את העלות של האינפלציה על ידי ביצוע העבודה מראש בזמן ששרשור ממשק המשתמש לא פעיל.
אם אתם רואים אינפלציה במהלך פריימים ולא בקטע עם התווית RV
Prefetch, ודאו שאתם מבצעים את הבדיקה במכשיר נתמך ומשתמשים בגרסה עדכנית של ספריית התמיכה.
טעינה מראש נתמכת רק ב-Android מגרסה 5.0 (רמת API 21) ואילך.
אם אתם רואים לעיתים קרובות שהאינפלציה גורמת לבעיות בגלל פריטים חדשים שמופיעים במסך, כדאי לוודא שאין לכם יותר סוגי תצוגה ממה שאתם צריכים. ככל שיש פחות סוגי תצוגה בתוכן של RecyclerView, כך צריך לבצע פחות ניפוח כשסוגים חדשים של פריטים מופיעים על המסך. אם אפשר, כדאי למזג סוגי תצוגות כשזה הגיוני. אם רק סמל, צבע או קטע טקסט משתנים בין סוגים, אפשר לבצע את השינוי הזה בזמן הקישור ולמנוע ניפוח, וכך להקטין את נפח הזיכרון של האפליקציה.
אם סוגי התצוגה נראים טוב, כדאי לנסות להפחית את העלות של האינפלציה.
אפשר לצמצם את התצוגות המיותרות של מאגרי התגים והמבנים. מומלץ לבנות itemViews באמצעות ConstraintLayout, כדי לצמצם את הצפיות במבנה.
אם אתם רוצים לבצע אופטימיזציה נוספת של הביצועים, וההיררכיות של הפריטים פשוטות ולא נדרשות לכם תכונות מורכבות של עיצוב וסגנון, כדאי להפעיל את הפונקציות ליצירת אובייקטים בעצמכם. עם זאת, לרוב לא כדאי לוותר על הפשטות והתכונות של XML.
RecyclerView: Bind taking too long
הקישור – כלומר, onBindViewHolder(VH,
int) – צריך להיות פשוט ולהימשך הרבה פחות ממילי-שנייה, למעט פריטים מורכבים במיוחד. היא צריכה לקחת פריטים מסוג Plain Old Java Object (POJO) מנתוני הפריטים הפנימיים של המתאם ולהפעיל פונקציות setter בתצוגות ב-ViewHolder. אם RV OnBindView נמשך זמן רב, צריך לוודא שאתם מבצעים עבודה מינימלית בקוד הקישור.
אם אתם משתמשים באובייקטים בסיסיים של POJO כדי לאחסן נתונים במתאם, אתם יכולים להימנע לחלוטין מכתיבת קוד הקישור ב-onBindViewHolder באמצעות ספריית Data Binding.
RecyclerView או ListView: הפריסה או הציור נמשכים יותר מדי זמן
אם אתם נתקלים בבעיות שקשורות לציור ולפריסה, כדאי לעיין בקטעים ביצועי הפריסה וביצועי העיבוד.
ListView: Inflation
אם לא נזהרים, אפשר להשבית בטעות את המיחזור ב-ListView. אם אתם רואים ניפוח בכל פעם שפריט מופיע במסך, בדקו שההטמעה של Adapter.getView() מבצעת חשיבה, קשירה מחדש והחזרה של הפרמטר convertView. אם ההטמעה של getView() תמיד מתרחבת, האפליקציה לא נהנית מהיתרונות של שימוש חוזר ב-ListView. המבנה של getView() צריך להיות כמעט תמיד דומה להטמעה הבאה:
Kotlin
fun getView(position: Int, convertView: View?, parent: ViewGroup): View { return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply { // Bind content from position to convertView. } }
Java
View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { // Only inflate if no convertView passed. convertView = layoutInflater.inflate(R.layout.my_layout, parent, false) } // Bind content from position to convertView. return convertView; }
ביצועי הפריסה
אם ב-Systrace מופיע שהקטע Layout של Choreographer#doFrame פועל יותר מדי או בתדירות גבוהה מדי, זה אומר שאתם נתקלים בבעיות בביצועים של הפריסה. ביצועי הפריסה של האפליקציה תלויים בחלק בהיררכיית התצוגה שבה משתנים פרמטרים או קלטים של הפריסה.
ביצועי פריסה: עלות
אם הפלחים ארוכים מכמה אלפיות השנייה, יכול להיות שאתם נתקלים בביצועים הכי גרועים של הקינון ב-RelativeLayouts או ב-weighted-LinearLayouts. כל אחד מהפריסות האלה יכול להפעיל כמה מעברים של מדידה ופריסה של רכיבי הצאצא שלו, ולכן קינון שלהם יכול להוביל להתנהגות O(n^2) בעומק הקינון.
כדאי להימנע משימוש ב-RelativeLayout או בתכונת המשקל שלLinearLayout בכל הצמתים מלבד הצמתים הנמוכים ביותר בהיררכיה. אלה הדרכים שבהן אפשר לעשות את זה:
- לארגן מחדש את התצוגות המבניות.
- הגדרת לוגיקת פריסה בהתאמה אישית. דוגמה ספציפית מופיעה במאמר בנושא אופטימיזציה של היררכיות פריסות. אפשר לנסות להמיר ל-
ConstraintLayout, שמספק תכונות דומות בלי החסרונות בביצועים.
ביצועי פריסה: תדירות
הפריסה אמורה להתרחש כשתוכן חדש מופיע במסך, למשל כשפריט חדש נגלל לתצוגה ב-RecyclerView. אם מתבצע פריסה משמעותית בכל פריים, יכול להיות שאתם מנפישים פריסה, וזה כנראה יגרום להשמטת פריימים.
באופן כללי, אנימציות צריכות לפעול במאפייני הציור של View, כמו המאפיינים הבאים:
אפשר לשנות את כל ההגדרות האלה בעלות נמוכה בהרבה מאשר מאפייני פריסה, כמו
padding או margins. בדרך כלל, גם הרבה יותר זול לשנות את מאפייני הציור של תצוגה על ידי קריאה לפונקציית setter שמופעלת invalidate(), ואחריה draw(Canvas) בפריים הבא. הוא מקליט מחדש פעולות ציור של התצוגה שבוטלה, ובדרך כלל הוא גם זול יותר מפריסה.
ביצועי רינדור
ממשק המשתמש של Android פועל בשני שלבים:
- Record View#draw בשרשור UI, שמופעל
draw(Canvas)בכל תצוגה לא חוקית, ויכול להפעיל קריאות לתצוגות מותאמות אישית או לקוד שלכם. - DrawFrame ב-
RenderThread, שפועל ב-RenderThreadהמקורי, אבל מבוסס על עבודה שנוצרה בשלב Record View#draw.
ביצועי עיבוד: שרשור ממשק המשתמש
אם Record View#draw נמשך זמן רב, בדרך כלל מדובר בציור של מפת סיביות בשרשור UI. ציור למפת סיביות משתמש בעיבוד CPU, ולכן בדרך כלל מומלץ להימנע מכך ככל האפשר. כדי לבדוק אם זו הבעיה, אפשר להשתמש ב-Android CPU Profiler למעקב אחר שיטות.
הציור על מפת סיביות מתבצע בדרך כלל כשאפליקציה רוצה לקשט מפת סיביות לפני שהיא מוצגת – לפעמים זה קישוט כמו הוספת פינות מעוגלות:
Kotlin
val paint = Paint().apply { isAntiAlias = true } Canvas(roundedOutputBitmap).apply { // Draw a round rect to define the shape: drawRoundRect( 0f, 0f, roundedOutputBitmap.width.toFloat(), roundedOutputBitmap.height.toFloat(), 20f, 20f, paint ) paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY) // Multiply content on top to make it rounded. drawBitmap(sourceBitmap, 0f, 0f, paint) setBitmap(null) // Now roundedOutputBitmap has sourceBitmap inside, but as a circle. }
Java
Canvas bitmapCanvas = new Canvas(roundedOutputBitmap); Paint paint = new Paint(); paint.setAntiAlias(true); // Draw a round rect to define the shape: bitmapCanvas.drawRoundRect(0, 0, roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)); // Multiply content on top to make it rounded. bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint); bitmapCanvas.setBitmap(null); // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
אם זה סוג העבודה שאתם מבצעים בשרשור UI, אתם יכולים לבצע את הפעולה הזו בשרשור הפענוח ברקע. במקרים מסוימים, כמו בדוגמה הקודמת, אפשר לבצע את העבודה גם בזמן השליפה. לכן, אם הקוד של Drawable או View נראה בערך כך:
Kotlin
fun setBitmap(bitmap: Bitmap) { mBitmap = bitmap invalidate() } override fun onDraw(canvas: Canvas) { canvas.drawBitmap(mBitmap, null, paint) }
Java
void setBitmap(Bitmap bitmap) { mBitmap = bitmap; invalidate(); } void onDraw(Canvas canvas) { canvas.drawBitmap(mBitmap, null, paint); }
אפשר להחליף אותו בטקסט הבא:
Kotlin
fun setBitmap(bitmap: Bitmap) { shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) invalidate() } override fun onDraw(canvas: Canvas) { canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint) }
Java
void setBitmap(Bitmap bitmap) { shaderPaint.setShader( new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP)); invalidate(); } void onDraw(Canvas canvas) { canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint); }
אפשר לעשות את זה גם להגנה על הרקע, למשל כשמציירים מעבר צבע על מפת הסיביות, וגם לסינון תמונות באמצעות ColorMatrixColorFilter – שתי פעולות נפוצות נוספות שמבצעים כשמשנים מפות סיביות.
אם אתם מציירים על מפת סיביות מסיבה אחרת – יכול להיות שאתם משתמשים בה כמטמון – נסו לצייר ישירות על Canvas עם האצת חומרה שמועבר אל View או אל Drawable. במקרה הצורך, אפשר גם להתקשר אל
setLayerType()
עם
LAYER_TYPE_HARDWARE
כדי לשמור במטמון פלט עיבוד מורכב ועדיין ליהנות מהיתרונות של עיבוד ב-GPU.
ביצועי רינדור: RenderThread
חלק מהפעולות של Canvas זולות לתיעוד, אבל מפעילות חישוב יקר ב-RenderThread. בדרך כלל, Systrace מציין את הבעיות האלה באמצעות התראות.
הנפשה של נתיבים גדולים
כשקוראים ל-Canvas.drawPath() ב-Canvas שעבר עם האצת חומרה ל-View, מערכת Android מציירת את הנתיבים האלה קודם ב-CPU ומעלה אותם ל-GPU.
אם יש לכם נתיבים גדולים, אל תערכו אותם מפריים לפריים כדי שניתן יהיה לשמור אותם במטמון ולצייר אותם ביעילות.
drawPoints(), drawLines() ו-drawRect/Circle/Oval/RoundRect() יעילים יותר ומומלץ להשתמש בהם גם אם משתמשים ביותר קריאות לציור.
Canvas.clipPath
clipPath(Path)
מפעיל התנהגות חיתוך יקרה, ובדרך כלל צריך להימנע ממנו. אם אפשר, כדאי לצייר צורות במקום לחתוך לצורות לא מלבניות. הוא פועל טוב יותר ותומך בהחלקת קצוות. לדוגמה, אפשר לבטא את הקריאה הבאה clipPath באופן שונה:
Kotlin
canvas.apply { save() clipPath(circlePath) drawBitmap(bitmap, 0f, 0f, paint) restore() }
Java
canvas.save(); canvas.clipPath(circlePath); canvas.drawBitmap(bitmap, 0f, 0f, paint); canvas.restore();
במקום זאת, כדאי לנסח את הדוגמה הקודמת כך:
Kotlin
paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) // At draw time: canvas.drawPath(circlePath, mPaint)
Java
// One time init: paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP)); // At draw time: canvas.drawPath(circlePath, mPaint);
העלאות של מפות סיביות
מערכת Android מציגה מפות סיביות כמרקמי OpenGL, ובפעם הראשונה שמפת סיביות מוצגת בפריים, היא מועלית ל-GPU. אפשר לראות את זה ב-Systrace בתור Texture upload(id) width x height. התהליך הזה יכול להימשך כמה אלפיות השנייה, כמו שמוצג באיור 2, אבל הוא הכרחי כדי להציג את התמונה באמצעות ה-GPU.
אם התהליך נמשך זמן רב, כדאי קודם לבדוק את המספרים של הרוחב והגובה בנתוני המעקב. מוודאים שהמפת סיביות שמוצגת לא גדולה משמעותית מהאזור במסך שבו היא מוצגת. אם כן, זה מבזבז זמן העלאה וזיכרון. בדרך כלל, ספריות לטעינת מפות סיביות מספקות אמצעי לבקשת מפה סיבית בגודל מתאים.
ב-Android 7.0, קוד טעינת מפת הסיביות – שבדרך כלל מבוצע על ידי ספריות – יכול לקרוא ל-prepareToDraw() כדי להפעיל העלאה מוקדמת לפני שהיא נדרשת. כך ההעלאה מתבצעת מוקדם יותר, בזמן שהמכשיר RenderThread לא פעיל. אפשר לעשות את זה אחרי פענוח או כשמבצעים קישור של מפת סיביות לתצוגה, כל עוד יודעים את מפת הסיביות. באופן אידיאלי, ספריית טעינת מפת הסיביות עושה זאת בשבילכם, אבל אם אתם מנהלים את הספרייה בעצמכם או רוצים לוודא שלא תגיעו למגבלות ההעלאה במכשירים חדשים יותר, אתם יכולים לקרוא ל-prepareToDraw() בקוד שלכם.
prepareToDraw().עיכובים בתזמון שרשורים
מתזמן השרשורים הוא החלק במערכת ההפעלה של Android שאחראי להחליט אילו שרשורים במערכת צריכים לפעול, מתי הם פועלים ולכמה זמן.
לפעמים, בעיות בממשק (jank) מתרחשות כי שרשור UI של האפליקציה חסום או לא פועל. ב-Systrace נעשה שימוש בצבעים שונים, כמו שמוצג באיור 3, כדי לציין מתי השרשור במצב שינה (אפור), ניתן להרצה (כחול: אפשר להריץ אותו, אבל המתזמן עדיין לא בחר בו להרצה), פועל באופן פעיל (ירוק) או במצב שינה שלא ניתן להפריע לו (אדום או כתום). האפשרות הזו שימושית מאוד לניפוי באגים בבעיות בממשק (jank) שנגרמות בגלל עיכובים בתזמון של שרשורים.
הרבה פעמים קריאות ל-Binder – מנגנון התקשורת בין תהליכים (IPC) ב-Android – גורמות להפסקות ארוכות בהרצת האפליקציה. בגרסאות מאוחרות יותר של Android, זו אחת הסיבות הנפוצות ביותר להפסקת הפעולה של שרשור UI. בדרך כלל, הפתרון הוא להימנע מהפעלת פונקציות שמבצעות קריאות ל-binder. אם אי אפשר להימנע מכך, כדאי לשמור את הערך במטמון או להעביר את העבודה לשרשורים ברקע. ככל שבסיסי הקוד גדלים, יכול להיות שבטעות תוסיפו קריאה ל-Binder אם תפעילו שיטה ברמה נמוכה בלי לשים לב. אבל אפשר למצוא ולתקן אותן באמצעות מעקב.
אם יש לכם עסקאות של binder, אתם יכולים לתעד את מחסנית הקריאות שלהן באמצעות הפקודות הבאות של adb:
$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt
לפעמים קריאות שנראות תמימות, כמו getRefreshRate(), יכולות להפעיל עסקאות של binder ולגרום לבעיות גדולות אם הן מופעלות בתדירות גבוהה. מעקב תקופתי יכול לעזור לכם למצוא ולפתור את הבעיות האלה כשהן מתרחשות.
trace-ipc כדי לאתר ולהסיר קריאות לקישור.אם אתם לא רואים פעילות של binder אבל עדיין לא רואים את שרשור ה-UI שלכם פועל, ודאו שאתם לא מחכים לנעילה או לפעולה אחרת משרשור אחר. בדרך כלל, שרשור ה-UI לא צריך לחכות לתוצאות משרשורים אחרים. שרשורים אחרים צריכים לפרסם בו מידע.
הקצאת אובייקטים ומנגנון איסוף זבל
הקצאת אובייקטים ומנגנון איסוף זבל (GC) הם בעיה פחות משמעותית מאז ש-ART הושק כזמן הריצה שמוגדר כברירת מחדל ב-Android 5.0, אבל עדיין אפשר להעמיס על השרשורים את העבודה הנוספת הזו. אפשר להקצות זיכרון בתגובה לאירוע נדיר שלא קורה הרבה פעמים בשנייה – כמו הקשה של משתמש על לחצן – אבל חשוב לזכור שלכל הקצאה יש עלות. אם הפונקציה נמצאת בלולאה צפופה שמופעלת לעיתים קרובות, כדאי להימנע מהקצאה כדי להפחית את העומס על איסוף האשפה.
ב-Systrace אפשר לראות אם איסוף האשפה פועל לעיתים קרובות, וב-Android Memory Profiler אפשר לראות מאיפה מגיעות ההקצאות. אם אפשר, כדאי להימנע מהקצאות, במיוחד בלולאות צפופות, כדי להקטין את הסיכוי לבעיות.
בגרסאות עדכניות של Android, בדרך כלל GC פועל בשרשור ברקע שנקרא HeapTaskDaemon. הקצאה של כמויות משמעותיות יכולה להוביל להוצאה של יותר משאבי CPU על GC, כפי שמוצג באיור 5.
מומלץ בשבילך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- השוואה לשוק של האפליקציה
- סקירה כללית של מדידת ביצועי האפליקציה
- שיטות מומלצות לאופטימיזציה של אפליקציות