צינור העיבוד של Android 2D תומך בהאצת חומרה, כלומר כל פעולות הציור שמבוצעות בקנבס משתמשות ב-GPU. בגלל הגידול במשאבים שנדרשים להפעלת שיפור מהירות באמצעות חומרה, האפליקציה תצרוך יותר RAM.
התכונה 'שיפור מהירות באמצעות חומרה' מופעלת כברירת מחדל. אם האפליקציה שלכם משתמשת רק ברכיבי קומפוזיציה רגילים, הפעלת האפשרות הזו באופן גלובלי לא אמורה לגרום להשפעות שליליות על הרינדור. עם זאת, מכיוון שהאצת חומרה לא נתמכת בכל פעולות הציור הדו-ממדיות, הפעלת ההגדרה הזו עשויה להשפיע על חלק מהקריאות של ציורים בהתאמה אישית. הבעיות בדרך כלל מתבטאות ברכיבים בלתי נראים, בחריגים או בפיקסלים שמוצגים בצורה שגויה. כדי לפתור את הבעיה הזו, מערכת Android מאפשרת להפעיל או להשבית את שיפור המהירות באמצעות חומרה בכמה רמות. איך שולטים בשיפור המהירות באמצעות חומרה
אם האפליקציה מבצעת ציור בהתאמה אישית, כדאי לבדוק אותה במכשירי חומרה אמיתיים עם שיפור מהירות באמצעות חומרה כדי לאתר בעיות. בקטע תמיכה בפעולות שרטוט מפורטות בעיות מוכרות שקשורות לשיפור המהירות באמצעות חומרה, ומוסבר איך לעקוף אותן.
אפשר גם לעיין במאמר בנושא OpenGL עם ממשקי Framework API.
שליטה בשיפור המהירות באמצעות חומרה
אפשר לשלוט בשיפור מהירות באמצעות חומרה ברמות הבאות:
- אפליקציה
- פעילות
- חלון
- קומפוזבילי
רמת האפליקציה
בקובץ המניפסט של Android, מוסיפים את המאפיין הבא לתג <application> כדי להפעיל האצת חומרה לכל האפליקציה:
<application android:hardwareAccelerated="true" ...>
רמת הפעילות
אם האפליקציה לא פועלת בצורה תקינה כשהאצת החומרה מופעלת באופן גלובלי, אפשר לשלוט בה גם עבור פעילויות ספציפיות. כדי להפעיל או להשבית את שיפור המהירות באמצעות חומרה ברמת הפעילות, אפשר להשתמש במאפיין android:hardwareAccelerated של הרכיב <activity>. בדוגמה הבאה מופעלת האצת חומרה לכל האפליקציה, אבל היא מושבתת לפעילות אחת:
<application android:hardwareAccelerated="true">
<activity ... />
<activity android:hardwareAccelerated="false" />
</application>
רמת החלון
אם אתם רוצים שליטה מדויקת יותר, אתם יכולים להפעיל שיפור מהירות באמצעות חומרה עבור חלון מסוים באמצעות הקוד הבא:
window.setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
)
רמת הקומפוזיציה
ב-Compose, אין מתג לכל רכיב שאפשר להשתמש בו כדי להשבית את שיפור המהירות באמצעות חומרה.
כדי להציג קומפוזבילי בשכבה משלו, משתמשים ב-Modifier.graphicsLayer. כך אפשר לשנות מאפייני טרנספורמציה (כמו alpha, scaleX, scaleY, translationX, translationY, rotationX, rotationY, rotationZ ו-transformOrigin) בלי להריץ מחדש את קוד הציור של הרכיב הניתן להרכבה. כדי להשיג את הביצועים הטובים ביותר, תמיד צריך להשתמש בפורמט lambda של משנה כדי להגדיר את המאפיינים האלה.
כדי לכפות באופן מפורש מאגר זמני מחוץ למסך לפעולות מתקדמות של ציור, כמו מיזוג בהתאמה אישית בשכבה, משתמשים ב-CompositingStrategy.Offscreen.
מידע נוסף זמין במאמר בנושא שינוי הגדרות הגרפיקה.
אם יש לכם פעולת ציור בהתאמה אישית שדורשת אך ורק עיבוד תוכנה, אתם יכולים לארח View מדור קודם באמצעות AndroidView ולקרוא ל-setLayerType(View.LAYER_TYPE_SOFTWARE, null) בתצוגה הזו.
תמיכה בפעולות שרטוט
כשמבצעים האצת חומרה, צינור העיבוד של רינדור דו-ממדי תומך בפעולות הציור הנפוצות ביותר של Canvas, וגם בפעולות רבות שפחות נפוצות. כל פעולות הציור שמשמשות לעיבוד אפליקציות שמגיעות עם Android, רכיבי composable רגילים ואפקטים חזותיים מתקדמים נפוצים כמו השתקפויות ומרקמים עם אריחים נתמכות.
בטבלה הבאה מתואר רמת התמיכה בפעולות שונות ברמות API שונות:
| רמת ה-API הראשונה שנתמכת | ||||
| Canvas | ||||
| drawBitmapMesh() (colors array) | 18 | |||
| drawPicture() | 23 | |||
| drawPosText() | 16 | |||
| drawTextOnPath() | 16 | |||
| drawVertices() | 29 | |||
| setDrawFilter() | 16 | |||
| clipPath() | 18 | |||
| clipRegion() | 18 | |||
| clipRect(Region.Op.XOR) | 18 | |||
| clipRect(Region.Op.Difference) | 18 | |||
| clipRect(Region.Op.ReverseDifference) | 18 | |||
| clipRect() עם סיבוב או פרספקטיבה | 18 | |||
| צבע | ||||
| setAntiAlias() (לטקסט) | 18 | |||
| setAntiAlias() (לשורות) | 16 | |||
| setFilterBitmap() | 17 | |||
| setLinearText() | ✗ | |||
| setMaskFilter() | ✗ | |||
| setPathEffect() (לשורות) | 28 | |||
| setShadowLayer() (מלבד טקסט) | 28 | |||
| setStrokeCap() (עבור קווים) | 18 | |||
| setStrokeCap() (לנקודות) | 19 | |||
| setSubpixelText() | 28 | |||
| Xfermode | ||||
| PorterDuff.Mode.DARKEN (framebuffer) | 28 | |||
| PorterDuff.Mode.LIGHTEN (מקטע Frame buffer) | 28 | |||
| PorterDuff.Mode.OVERLAY (מקטע Frame buffer) | 28 | |||
| תוכנת הצללה (shader) | ||||
| ComposeShader בתוך ComposeShader | 28 | |||
| אותו סוג של הצללות בתוך ComposeShader | 28 | |||
| מטריצה מקומית ב-ComposeShader | 18 | |||
שינוי גודל התצוגה באזור העריכה
צינור העיבוד של רינדור דו-ממדי מואץ חומרה נבנה קודם כדי לתמוך בציור לא מותאם, כאשר חלק מפעולות הציור מורידות את האיכות באופן משמעותי בערכי קנה מידה גבוהים יותר. הפעולות האלה מיושמות כמרקמים שמצוירים בקנה מידה 1.0, ועוברים טרנספורמציה על ידי ה-GPU. החל מרמת API 28, כל פעולות הציור יכולות להתרחב ללא בעיה.
בטבלה הבאה אפשר לראות מתי ההטמעה שונתה כדי לטפל נכון בהיקפים גדולים:
| פעולת השרטוט שצריך לשנות את קנה המידה שלה | רמת ה-API הראשונה שנתמכת |
| drawText() | 18 |
| drawPosText() | 28 |
| drawTextOnPath() | 28 |
| צורות פשוטות | 17 |
| צורות מורכבות | 28 |
| drawPath() | 28 |
| שכבת צל | 28 |
אם פעולת ציור שאתם מסתמכים עליה לא מואצת על ידי חומרה, צריך לעבד את הציור המושפע לתוך תוכנה Bitmap (או ImageBitmap) מחוץ למסך ולצייר את התוצאה. שאר ממשק המשתמש ימשיך להשתמש בנתיב עם האצת חומרה.
טיפים וטריקים
מעבר לגרפיקה דו-ממדית עם האצת חומרה יכול לשפר את הביצועים באופן מיידי, אבל עדיין כדאי לעצב את האפליקציה כך שתשתמש ב-GPU בצורה יעילה. לשם כך, מומלץ לפעול לפי ההמלצות הבאות:
- צמצום מורכבות הפריסה והרה-קומפוזיציה
- חשוב לשמור על עומק קטן של עץ הפריסה ולהגביל את מספר הפעמים שמתבצעת קומפוזיציה מחדש. כדאי לדחות קריאות של מצב להיקף הצר ביותר, כדי ששינוי יגרום לציור מחדש של האזור הקטן ביותר האפשרי. לדוגמה, קריאת מצב האנימציה בתוך
Modifier.graphicsLayer { }ולא בגוף של פונקציה שאפשר להרכיב. מידע נוסף זמין במאמר ביצועים של Jetpack Compose. - הימנעות ממשיכת יתר
- אל תציירו יותר מדי שכבות אחת על השנייה. מסירים רכיבי ממשק משתמש שמוסתרים לגמרי על ידי רכיבים אטומים אחרים שנמצאים מעליהם. אם אתם צריכים לצייר כמה שכבות שמשולבות אחת על השנייה, כדאי למזג אותן לשכבה אחת. כלל אצבע טוב לשימוש בחומרה הנוכחית הוא לא לצייר יותר מפי 2.5 ממספר הפיקסלים במסך לכל פריים (פיקסלים שקופים במפת סיביות נספרים!).
- לא ליצור אובייקטים של עיבוד בשיטות ציור
- טעות נפוצה היא ליצור
Paintחדש אוPathחדש בכל פעם שמפעילים שיטת עיבוד. כך מאלצים את איסוף האשפה לפעול בתדירות גבוהה יותר, ועוקפים גם את המטמון ואת האופטימיזציות בצינור העברת הנתונים של החומרה. כדי להימנע מכך, כדאי לעשות שימוש חוזר באובייקטים ולשנות אותם:- שימוש בשיטות סטנדרטיות: שיטות סטנדרטיות של
DrawScope(כמוdrawRectו-drawCircle) כבר עושות שימוש חוזר באובייקטים שלPaintבאופן פנימי, בלי לדרוש הקצאה למפתחים. - שינוי במקום הקצאה מחדש: כשכותבים לוגיקה מותאמת אישית, צריך להשתמש ב-
path.rewindכדי לנקותPathקיים במקום ליצור מופע חדש שלPath. - שמירה יעילה של מצב: בתוך רכיב קומפוזבילי, מקצים אובייקטים פעם אחת באמצעות
remember { Path() }. אם אתם יוצרים נכסי מודעות מותאמים אישית לשימוש חוזר, אתם יכולים להטמיעModifier.Nodeמותאם אישית באמצעותDrawModifierNodeכדי להקצות מחדש את האובייקטים ולעשות בהם שימוש חוזר בלי לגרום להקצאות חדשות של זיכרון בערימה.
- שימוש בשיטות סטנדרטיות: שיטות סטנדרטיות של
- לא לשנות את הצורות לעיתים קרובות מדי
- צורות מורכבות, נתיבים ומעגלים, לדוגמה, מעובדים באמצעות מסכות טקסטורה. בכל פעם שיוצרים או משנים נתיב, צינור העיבוד של החומרה יוצר מסכה חדשה, וזה עלול להיות יקר.
- לא לשנות מפות סיביות לעיתים קרובות מדי
- בכל פעם שמשנים את התוכן של מפת סיביות, הוא מועלה שוב כמרקם GPU בפעם הבאה שמציירים אותו.
- חשוב להשתמש בגרסת אלפא בזהירות
- כשיוצרים רכיב שקוף שאפשר להוסיף לו רכיבים אחרים באמצעות
Modifier.alphaאו ממשקי ה-API של אנימציות ב-Compose, הוא בדרך כלל עובר רינדור במאגר זמני מחוץ למסך, מה שמכפיל את קצב המילוי הנדרש. כדי להימנע מהתקורה של מאגר הנתונים הזמני מחוץ למסך עבור תוכן שלא חופף, מגדירים אתCompositingStrategy.ModulateAlpha. לגבי קריאות ספציפיות לציור, אפשר להחיל אלפא ישירות על פקודת הציור (כמו ב-color = Color.Red.copy(alpha = 0.5f)) בלי ליצור שכבה.