רינדור ממשק המשתמש הוא הפעולה של יצירת פריים מהאפליקציה והצגתו במסך. כדי להבטיח שהאינטראקציה של המשתמש עם האפליקציה תהיה חלקה, האפליקציה צריכה לעבד פריימים במהירות של פחות מ-16 אלפיות השנייה כדי להגיע לקצב של 60 FPS. כדי להבין למה כדאי להשתמש ב- 60 fps, אפשר לעיין במאמר דפוסי ביצועים ב-Android: למה לבחור ב-60fps? אם ניסית מגיעים ל- 90 fps, ואז החלון הזה יורד ל-11 אלפיות השנייה, וב- 120 fps 8 אלפיות השנייה.
אם חורגים מהחלון הזה באלפיות השנייה, זה לא אומר שהמסגרת מוצגת.
איחור של דקה, אבל Choreographer
משחרר את הפריים לחלוטין. אם באפליקציה שלכם יש רינדור איטי של ממשק המשתמש, סימן
המערכת נאלץ לדלג על פריימים, והמשתמש רואה את התצוגה של האפליקציה.
התהליך הזה נקרא jank. בדף הזה מוסבר איך לאבחן ולפתור בעיות בממשק (jank).
אם אתם מפתחים משחקים שלא משתמשים
View
, ואז אפשר לעקוף
Choreographer
במקרה הזה, קצב הפריימים
עזרה בספרייה
OpenGL
משחקי Vulkan מאפשרים עיבוד חלק
קצב הפריימים הנכון ב-Android.
כדי לשפר את איכות האפליקציה, מערכת Android עוקבת באופן אוטומטי אחר האפליקציה כדי לבדוק אם יש jank ומציג את המידע במרכז הבקרה של תפקוד האפליקציה. למידע על אופן איסוף הנתונים, אפשר לעיין במאמר מעקב אחר האיכות הטכנית של האפליקציה באמצעות במכשירי Android תפקוד האפליקציה.
זיהוי בעיות בממשק (jank)
לא תמיד קל למצוא את הקוד שגורם לבעיות בממשק (jank) באפליקציה שלכם. הקטע הזה מתארות שלוש שיטות לזיהוי jank:
בדיקה חזותית מאפשרת לכם לעבור על כל התרחישים לדוגמה באפליקציה שלכם אבל הוא לא מספק יותר פירוט כמו Systrace. הפונקציה Systrace מספקת לפרטים נוספים, אבל אם מריצים את Systrace לכל התרחישים לדוגמה באפליקציה, אפשר מוצפים בכל כך הרבה נתונים שקשה לנתח. שני הרכיבים החזותיים בדיקה ו-Systrace לזיהוי בעיות בממשק (jank) במכשיר המקומי שלך. אם לא ניתן לשחזר אין ממשקים במכשירים מקומיים, אפשר ליצור מעקב מותאם אישית אחרי הביצועים כדי למדוד חלקים ספציפיים של האפליקציה שלך במכשירים שפועלים בשטח.
בדיקה ויזואלית
בדיקה חזותית עוזרת לכם לזהות את תרחישי השימוש שמייצרים גרסאות jank. שפת תרגום לבצע בדיקה ויזואלית, לפתוח את האפליקציה ולעבור באופן ידני חלקים באפליקציה שלך ולחפש ממשק משתמש (jank).
ריכזנו כאן כמה טיפים לביצוע בדיקות חזותיות:
- מפעילים גרסה של האפליקציה, או לפחות גרסה שלא ניתנת לניפוי באגים. שעון ארגנטינה (ART) סביבת זמן הריצה משביתה מספר אופטימיזציות חשובות לצורך תמיכה בניפוי באגים לכן חשוב לוודא שאתם צופים במשהו שדומה לזה רואה.
- הפעלת GPU של פרופיל רינדור. במסגרת 'עיבוד GPU של הפרופיל' מוצגים עמודות במסך שמספקים ייצוג של משך הזמן שנדרש כדי לעבד את הפריימים של חלון ממשק משתמש ביחס לנקודת ההשוואה של 16 אלפיות השנייה. לכל עמודה יש רכיבים צבעוניים שממפים לשלב בצינור עיבוד הנתונים, כך שאפשר לראות נמשך הכי הרבה זמן. לדוגמה, אם הפריים נמשך הרבה זמן צריך לבדוק את קוד האפליקציה שמטפל בקלט של משתמשים.
- עוברים על רכיבים שהם מקורות נפוצים של בעיות בממשק (jank), כמו
בתור
RecyclerView
. - מפעילים את האפליקציה במצב קר התחלה.
- כדי להחמיר את הבעיה, כדאי להפעיל את האפליקציה במכשיר איטי יותר.
כשתמצאו תרחישים לדוגמה שמייצרים jank, יכול להיות שיש לכם מושג טוב שגורמת לבעיות בממשק באפליקציה. אם אתם זקוקים למידע נוסף, אפשר להשתמש ב-Systrace. כדי לבדוק את הסיבה לכך.
מערכת
למרות ש-Systrace הוא כלי שמראה מה המכשיר כולו עושה, הוא יכול שימושי לזיהוי jank באפליקציה שלכם. ל-Systrace יש מערכת מינימלית מהתקרה, כדי שתוכלו לחוות תזוזות מציאותית במהלך האינסטרומנטציה.
מקליטים מעקב באמצעות Systrace במהלך ביצוע מיושן במכשיר שלכם. הוראות לשימוש ב-Systrace זמינות במאמר תיעוד מעקב מערכת בשורת הפקודה. Systrace מפוצלת על ידי תהליכים ו שרשורים. מחפשים את התהליך של האפליקציה ב-Systrace, שנראה בערך כך. איור 1.

הדוגמה של Systrace באיור 1 מכילה את המידע הבא עבור זיהוי jank:
- מערכת Systrace מופיעה כשכל פריים משורטט, ו כל פריים מסומן כצבע להדגיש זמני רינדור איטיים. כך אפשר למצוא יותר פריימים מיוחדים במדויק מאשר בדיקה ויזואלית. מידע נוסף זמין במאמר הבא: בדיקת ממשק המשתמש פריימים והתראות.
- Systrace מזהה בעיות באפליקציה ומציגה התראות של פריימים בודדים התראות . מומלץ לפעול לפי ההנחיות שמופיעות בהתראה.
- חלקים ב-framework ובספריות של Android, כמו
RecyclerView
, מכילה סמני מעקב. כלומר, ציר הזמן של systrace מראה מתי השיטות האלו מבוצעות בממשק המשתמש ואת משך ההפעלה שלהם.
אחרי שתבדקו את הפלט של Systrace, ייתכן שיש שיטות באפליקציה שלכם
שאתם חושדים שגורמים לבעיות בממשק. לדוגמה, אם ציר הזמן מראה שמהירות
בעיית הפריים נגרמת בגלל שייקח הרבה זמן ל-RecyclerView
. אפשר להוסיף מעקב בהתאמה אישית
אירועים לקוד הרלוונטי
הריצו מחדש את Systrace כדי לקבל מידע נוסף. בממשק החדש של Systrace, ציר הזמן מראה
מתי מתבצעת קריאה לשיטות האפליקציה שלכם ומשך ההפעלה שלהן.
אם ב-Systrace לא מוצגים פרטים על הסיבה לכך שהשרשורים בממשק המשתמש נמשכים זמן רב. ואז להשתמש ב-Android CPU Profiler כדי להקליט מעקב באמצעות דגימות או אינסטרומנטציה. באופן כללי, מעקבי שיטות לא מתאימים לזהות בעיות בממשק, כי הם מייצרים תגובות כוזבות בגלל בתקורה, והם לא יכולים לראות מתי השרשורים פועלים ומתי השרשורים חסומים. אבל, יכולים לעזור לכם לזהות את השיטות באפליקציה שמבצעות את הכי הרבה זמן. אחרי זיהוי השיטות האלה, מוסיפים נתוני מעקב סמנים ולהריץ מחדש את Systrace כדי לראות. אם השיטות האלה גורמות לבעיות בממשק.
מידע נוסף זמין בקטע הבנה Systrace.
מעקב מותאם אישית אחרי ביצועים
אם לא מצליחים לשחזר את ה-jank במכשיר מקומי, אפשר ליצור ביצועים מותאמים אישית למעקב אחר האפליקציה כדי לזהות את מקור ה-jank במכשירים השדה הזה.
לשם כך, צריך לאסוף זמני רינדור פריימים מחלקים ספציפיים באפליקציה באמצעות
FrameMetricsAggregator
ולתעד ולנתח את הנתונים באמצעות ביצועי Firebase
מעקב.
למידע נוסף, ראו תחילת העבודה עם מעקב ביצועים עבור ב-Android.
פריימים קפואים
פריימים קפואים הם פריימים בממשק המשתמש שנמשכים יותר מ-700 אלפיות השנייה. זהו בעיה כי נראה שהאפליקציה תקועה ולא מגיבה לקלט של משתמשים למשך כמעט שנייה שלמה בזמן עיבוד הפריים. מומלץ לבצע אופטימיזציה כדי לעבד פריים תוך 16 אלפיות שנייה כדי להבטיח ממשק משתמש חלק. אבל במהלך השימוש באפליקציות, או בזמן המעבר למסך אחר, זו תופעה רגילה שרטוט הפריים הראשוני נמשך יותר מ-16 אלפיות השנייה כי האפליקציה חייבת לנפח את מוצגת פריסת המסך, וביצוע שרטוט ראשוני מהתחלה. לכן מערכת Android עוקבת אחרי פריימים קפואים בנפרד מהרינדור האיטי. לא העיבוד של פריימים באפליקציה שלכם אמור להימשך יותר מ-700 אלפיות השנייה.
כדי לעזור לכם לשפר את איכות האפליקציה, מערכת Android עוקבת באופן אוטומטי אחר האפליקציה למשך פריימים קפואים ומציגים את המידע במרכז הבקרה של תפקוד האפליקציה. מידע נוסף על אופן איסוף הנתונים זמין במאמר מעקב אחר הדרישות הטכניות של האפליקציה האיכות של תפקוד האפליקציה.
פריימים קפואים הם צורה קיצונית של עיבוד איטי, לאבחון ולתיקון הבעיה הוא זהה.
jank למעקב
FrameTimeline ב-Perfetto יכולות לעזור לך לעקוב אחרי נתונים איטיים או של פריימים קפואים.
הקשר בין פריימים איטיים, פריימים קפואים ומקרי ANR
פריימים איטיים, פריימים קפואים ומקרי ANR הם סוגים שונים של בעיות אבטחה שאתם עשויים להיתקל בהן. אפשר להיעזר בטבלה הבאה כדי להבין את ההבדלים.
פריימים איטיים | פריימים קפואים | מקרי ANR | |
---|---|---|---|
זמן רינדור | בין 16 אלפיות השנייה ל-700 אלפיות שנייה | בין 700 אלפיות השנייה ל-5 שניות | יותר מ-5 שנ' |
אזור ההשפעה על המשתמשים גלוי |
|
|
|
מעקב נפרד אחר פריימים איטיים ופריימים קפואים
במהלך הפעלת האפליקציה או בזמן המעבר למסך אחר, זה נורמלי כך שהפריים הראשוני יימשך יותר מ-16 אלפיות השנייה, כי האפליקציה חייבת לניפוח מספר הצפיות, לפרוס את המסך ולבצע את השרטוט הראשוני מאפס.
שיטות מומלצות לתעדוף ופתרון בעיות בממשק
כדאי לזכור את השיטות המומלצות הבאות כשמנסים לפתור בעיות שקשורות לבעיות בממשק (jank) app:
- זיהוי ופתרון של מופעי ה-jank שאפשר לשחזר בקלות רבה ביותר.
- מתן עדיפות למקרי ANR. למרות שפריימים איטיים או קפואים עשויות להופיע באפליקציה איטיים, ומקרי ANR גורמים לאפליקציה להפסיק להגיב.
- קשה לשחזר עיבוד איטי, אבל אפשר להתחיל בהרג של 700 אלפיות שנייה פריימים. מצב כזה קורה לעיתים קרובות בזמן שהאפליקציה מפעילה או משנה מסכים.
תיקון בעיות בממשק (jank)
כדי לתקן בעיות טכניות, צריך לבדוק אילו פריימים לא הושלמו תוך 16 אלפיות השנייה ולחפש
שגוי. צריך לבדוק אם משך הזמן של Record View#draw
או Layout
חריג באופן חריג
מסגרות מסוימות. ראו מקורות נפוצים של בעיות בממשק (jank) כדי לפתור את הבעיות האלה
אחרים.
כדי למנוע בעיות, מריצים משימות ממושכות באופן אסינכרוני מחוץ ל-thread של ממשק המשתמש. חשוב תמיד לשים לב לשרשור שבו הקוד פועל ולהיזהר פרסום משימות לא טריות לשרשור הראשי.
אם יש לכם ממשק משתמש ראשי מורכב וחשוב לאפליקציה, כמו רשימת גלילה — כדאי לשקול שימוש באינסטרומנטציה לכתיבה. בדיקות שיכולות באופן אוטומטי לזהות זמני רינדור איטיים ולהריץ בדיקות בתדירות גבוהה כדי למנוע רגרסיות.
מקורות נפוצים של בעיות בממשק (jank)
בקטעים הבאים מוסברים מקורות נפוצים של בעיות בממשק (jank) באפליקציות שמשתמשות ברכיב View
חדשות ושיטות מומלצות לטיפול בהן. לקבלת מידע על תיקון
בעיות בביצועים של Jetpack פיתוח נייטיב, לעיון ב-Jetpack
כתיבת ביצועים.
רשימות הניתנות לגלילה
ListView
, ובמיוחד
RecyclerView
– לרוב רשימות גלילה מורכבות
חשופות לבעיות בממשק. שניהם מכילים סמנים של Systrace, כך שתוכל להשתמש ב-Systrace.
כדי לבדוק אם הם תורמים ל-jank באפליקציה שלכם. העברה של שורת הפקודה
הארגומנט -a
<your-package-name>
כדי לקבל קטעי מעקב ב-RecyclerView
— וגם
כסמנים למעקב שהוספתם, שיופיעו. אם האפשרות זמינה, פועלים לפי ההנחיות
התראות שנוצרות בפלט של Systrace. Inside Systrace, אפשר ללחוץ
קטעים שעברו RecyclerView
, כדי להציג הסבר על העבודה RecyclerView
עושה.
RecyclerView: NotificationDataSetChanged()
אם תראו שכל פריט בRecyclerView
מוחזר, ולכן הוא מוצג מחדש
ומצונזרים מחדש במסגרת אחת — הקפידו לא להתקשר
notifyDataSetChanged()
,
setAdapter(Adapter)
,
או swapAdapter(Adapter,
boolean)
לעדכונים קטנים. השיטות האלה מרמזות על כך שחלו שינויים
מציגים את התוכן ב-Systrace בתור RV Full Validate. במקום זאת, השתמשו
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
, במיוחד עם
רשימה אנכית של רשימות גלילה אופקית. דוגמה לכך היא רשתות
מהאפליקציות בדף הראשי של חנות 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: יותר מדי אינפלציה או יצירת תהליכים נמשכים יותר מדי זמן
ברוב המקרים, תכונת השליפה מראש (prefetch) ב-RecyclerView
יכולה לעזור לעקוף
עלות האינפלציה באמצעות עבודה מראש בזמן שה-thread של ה-UI לא פעיל.
אם רואים ניפוח בתוך מסגרת ולא בקטע עם התווית קרוואנים
שליפה מראש (prefetch), חשוב לוודא שאתם בודקים במכשיר נתמך ומשתמשים
של ספריית התמיכה.
השליפה מראש (prefetch) נתמכת רק ב-Android 5.0 API ברמה 21 ואילך.
אם הרבה פעמים רואים אינפלציה שגורמת לבעיות בממשק (jank) כשפריטים חדשים מופיעים במסך,
שאין לכם יותר סוגי תצוגות ממה שאתם צריכים. ככל שיהיו פחות סוגי צפיות
את התוכן של RecyclerView
, כך צריך להפחית את האינפלציה כאשר
מוצגים במסך. אם אפשר, כדאי למזג בין סוגי תצוגות הנתונים במקרים שבהם הדבר סביר. אם המיקום
רק סמל, צבע או קטע טקסט שונים בין הסוגים, אפשר לבצע את השינוי
משתנים בזמן הקישור ומונעים אינפלציה, שמפחיתה את כמות הזיכרון של האפליקציה
בו-זמנית.
אם סוגי התצוגות נראים טוב, כדאי לבדוק איך אפשר להפחית את עלות האינפלציה.
אפשר לצמצם את מספר התצוגות של קונטיינרים או מבניים מיותרים. מומלץ ליצור
itemViews
עם ConstraintLayout
,
מה שיכול לעזור בהפחתת תצוגות מבניות.
אם אתם רוצים לבצע אופטימיזציה נוספת לביצועים, והיררכיות הפריטים שלכם הן פשוט ואין צורך בתכונות עיצוב וסגנון מורכבות, כדאי לשקול את ה-constructors בעצמכם. עם זאת, לרוב לא כדאי להתפשר על הפסד את הפשטות ואת התכונות של XML.
RecyclerView: הקישור נמשך יותר מדי זמן
הקשר – כלומר onBindViewHolder(VH,
int)
– צריך להיות ישיר ולמשך פחות מאלפית שנייה עד
הכול מלבד הפריטים המורכבים ביותר. היא חייבת לקחת אובייקט Java ישן פשוט (POJO)
פריטים מנתוני הפריט הפנימיים של המתאם שלך ומהגדרות השיחה בתצוגות
ViewHolder
אם לוקח הרבה זמן ל-RV OnBindView, צריך לוודא
לעשות עבודה מינימלית בקוד הקישור.
אם אתם משתמשים באובייקטים בסיסיים של POJO כדי להחזיק נתונים במתאם, תוכלו:
להימנע לגמרי מכתיבת קוד הקישור ב-onBindViewHolder
באמצעות הפקודה
ספרייה לקישור נתונים.
RecyclerView או ListView: פריסה או שרטוט שנמשכים יותר מדי זמן
לבעיות בשרטוט ופריסה, אפשר לעיין בביצועי הפריסה קטעי ביצועי רינדור.
ListView: אינפלציה
אם חשוב לא לדעת, אפשר להשבית בטעות מיחזור ב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
, כמו
הבאים:
אפשר לשנות את כל אלה בצורה הרבה יותר זולה מאשר מאפייני פריסה, כמו
מרווח פנימי או שוליים. באופן כללי, שינוי השרטוט פשוט הרבה יותר זול
מאפיינים של תצוגה באמצעות קריאה לרכיב קבוע שמפעיל
invalidate()
ואחריו
draw(Canvas)
בפריים הבא. פעולה זו מתעדת מחדש פעולות שרטוט עבור התצוגה
ולכן הוא גם זול יותר בדרך כלל מהפריסה.
ביצועי הרינדור
ממשק המשתמש של Android פועל בשני שלבים:
- Record View#draw ב-thread של ממשק המשתמש, שרץ
draw(Canvas)
בכל תצוגה לא חוקית, ויכול להפעיל שיחות לתצוגות מותאמות אישית או לקוד. - DrawFrame ב-
RenderThread
, שפועל בסביבתRenderThread
המקורית אבל היא פועלת על סמך עבודה שנוצרה בשלב Record View#draw.
ביצועי עיבוד: פרוטוקול Thread בממשק המשתמש
אם Record View#draw נמשך הרבה זמן, לעיתים קרובות מפת סיביות על ה-thread של ממשק המשתמש. כדי לצבוע במפת סיביות (bitmap) נעשה שימוש בעיבוד במעבד (CPU), לכן להימנע מכך בדרך כלל, כשאפשר. אפשר להשתמש במעקב method באמצעות Android הכלי לניתוח ביצועים של מעבד (CPU) כדי לבדוק אם את הבעיה.
לרוב, צביעה במפת סיביות מתבצעת כשאפליקציה רוצה לקשט מפת סיביות לפני כן להציג אותו - שלפעמים הוא קישוט, כמו הוספת פינות מעוגלות:
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.
אם זה סוג העבודה שאתם עושים ב-thread של ממשק המשתמש, אפשר במקום זאת
את זה בשרשור של הפענוח ברקע. במקרים מסוימים, כמו בדוגמה הקודמת
למשל, אפשר אפילו לבצע את העבודה בזמן ציור. לכן, אם
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);
העלאות של מפות סיביות (bitmap)
מערכת Android מציגה מפות סיביות בתור מרקמים של OpenGL, והפעם הראשונה שבה שמוצג במסגרת, הוא מועלה ל-GPU. אפשר לראות את זה ב-Systrace בתור העלאת מרקם(מזהה) רוחב x גובה. התהליך עשוי להימשך כמה אלפיות שנייה, כי שמוצג באיור 2, אבל יש צורך להציג את התמונה עם ה-GPU.
אם אלה לוקחים זמן רב, בדקו תחילה את מספרי הרוחב והגובה למעקב. מוודאים שמפת הסיביות שמוצגת לא גדולה משמעותית מ- האזור שבו הוא מופיע במסך. אם כן, זה מבזבז זמן העלאה זיכרון. באופן כללי, ספריות טעינה של מפת סיביות (bitmap) מאפשרות לבקש מפת סיביות בגודל מתאים.
ב-Android 7.0, קוד טעינה של מפת סיביות (bitmap) — לרוב באמצעות ספריות — יכול לקרוא
prepareToDraw()
עד
להפעיל העלאה מוקדמת לפני שיהיה צורך בה. כך ההעלאה מתבצעת בשלב מוקדם
בזמן ש-RenderThread
לא פעיל. אפשר לעשות זאת אחרי הפענוח או במהלך הקישור
מפת סיביות לתצוגה, כל עוד אתם מכירים את מפת הסיביות. באופן אידיאלי, מפת הסיביות נטענת
לעשות את זה בשבילכם, אבל אם אתם מנהלים חשבון משלכם או רוצים לוודא
לא מעלים סרטונים במכשירים חדשים יותר. אפשר להתקשר למספר prepareToDraw()
משלך

prepareToDraw()
.עיכובים בתזמון של שרשורים
מתזמן השרשורים הוא החלק במערכת ההפעלה של Android שאחראית על כדי לקבוע אילו שרשורים במערכת יצטרכו לפעול, מתי הם יפעלו ולמשך כמה זמן.
לפעמים תקלה נגרמת בגלל שה-thread של ממשק המשתמש של האפליקציה חסום או לא פועל. Systrace משתמשת בצבעים שונים, כפי שמוצג באיור 3, כדי לציין מתי שרשור. שינה (אפור), ניתנת לריצה (כחול: היא יכולה לרוץ, אבל לא נבחרת על ידי לוח הזמנים לפעול עדיין), פועל באופן פעיל (ירוק) או בשינה ללא הפרעות (אדום או כתום). זה שימושי במיוחד לניפוי באגים בבעיות שקשורות לבעיות בממשק (jank) בגלל עיכובים בתזמון השרשורים.

לעיתים קרובות, Binder קורא למנגנון התקשורת בין התהליכים (IPC) Android - גורמות להשהיות ארוכות בהפעלת האפליקציה. בגרסאות מאוחרות יותר של Android, זו אחת מהסיבות הנפוצות ביותר להפסקת הפעולה של ה-thread בממשק המשתמש. באופן כללי, הפתרון הוא להימנע משליחת קריאה לפונקציות שמבצעות הפעלות של binder. אם זה בלתי נמנעת, לשמור את הערך במטמון או להעביר עבודה לשרשורים ברקע. כ-codebase גדולים יותר, אפשר להוסיף בטעות קריאה ל-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.אם לא מופיעה פעילות של binder אבל עדיין לא רואים את ה-thread של ממשק המשתמש חשוב לוודא שאתם לא ממתינים לנעילה או לפעולה אחרת משרשור אחר. בדרך כלל, ה-thread של ממשק המשתמש לא חייב להמתין לתוצאות משרשורים אחרים. בשרשורים אחרים צריך לפרסם מידע.
הקצאת אובייקטים ואיסוף אשפה
הקצאת אובייקטים ואיסוף אשפה (GC) פחות בעייתית באופן משמעותי מאז ש-ART הופיע כזמן הריצה שמוגדר כברירת מחדל ב-Android 5.0, אבל הוא עדיין שאפשר לשקלל את השרשורים עם העבודה הנוספת הזו. זה בסדר להקצות בתגובה לאירוע נדיר שלא מתרחש פעמים רבות בשנייה — כמו כשמשתמש לוחץ על לחצן, אבל חשוב לזכור שלכל הקצאה יש עלות. אם המיקום הם נמצאים בלופ, וקוראים לעיתים קרובות, כדאי להימנע מההקצאה כדי להקל את העומס על GC.
Systrace מראה לך אם GC פועל לעתים קרובות, וזיכרון Android הכלי לניתוח ביצועים יכול להראות לך איפה הקצאות המקור שלו. אם ניתן להימנע מהקצאות, במיוחד כאשר לולאות, יש פחות סיכויים שתיתקלו בבעיות.

בגרסאות עדכניות של Android, GC פועל בדרך כלל בשרשור ברקע בשם HeapTaskDaemon. כמות גדולה של הקצאה עלולה להגדיל את כמות המעבד (CPU) משאבים שהושקעו על GC, כפי שמוצג באיור 5.
מומלץ עבורך
- הערה: טקסט הקישור מוצג כאשר JavaScript מושבת
- השוואה לשוק של האפליקציה
- סקירה כללית של מדידת ביצועים של אפליקציות
- שיטות מומלצות לאופטימיזציה של אפליקציות