כיצד להשיג 60 FPS יציב באנימציות מובייל
לעתים קרובות אנו רואים ש-60 FPS פירושו 16.67 אלפיות השנייה לכל פריים. אם פריים כלשהו לוקח יותר מ-17 אלפיות השנייה, Instruments יציג פריים שנפל. במסכים עם 120 הרץ (ProMotion), הסף הדוק עוד יותר: 8.3 אלפיות השנייה. משתמשי iPhone 13 Pro ו-Pixel 8 מרגישים זאת פיזית. המשימה שלנו היא להישאר במסגרת המגבלה הזו בכל תרחיש.
מדוע אנימציות נתקעות במכשירים אמיתיים
לפני האופטימיזציה, פתח את Xcode Instruments עם תבנית Core Animation. הרץ על מכשיר אמיתי (הסימולטור לא נחשב: יש לו GPU שונה). צפה בשני גרפים: FPS ו-CPU Usage. פסים אדומים בגרף FPS מצביעים על פריימים שנפלו.
באנדרואיד, השתמש ב-Android Profiler במצב GPU/CPU יחד עם Systrace לניתוח מפורט. באפשרויות המפתחים, הפעל Profile GPU Rendering (מציג פסים על המסך): אם האזור הכתום (Draw) והאזור האדום (Sync/Upload) חורגים באופן קבוע מקו ה-16ms, יש בעיה.
ממצא אופייני: אנימציה של מאפייני צל (shadowRadius, shadowOffset על CALayer) מחשבת מחדש טשטוש גאוסי על המעבד בכל פריים. ב-iPhone SE דור 2, זה יכול לקחת 8–10 אלפיות השנייה רק עבור הצל.
העיקרון המרכזי: שכבות GPU לעומת עיבוד CPU
אנימציה רק של transform ו-opacity אינה המלצה—זה חוק ביצועים. מאפיינים אלה מעובדים ישירות על ידי ה-Compositor thread ללא מעורבות של ה-main thread וללא קריאה ל-drawRect:.
כל דבר אחר מפעיל את המחזור Layout → Display → Prepare → Commit:
| מאפיין | מעובד על | פריימים שנפלו |
|---|---|---|
transform, opacity |
Compositor של GPU | לא |
backgroundColor |
GPU (CALayer) | לעיתים רחוקות |
bounds, frame |
CPU → GPU | לעתים קרובות |
cornerRadius + masksToBounds |
CPU (מחוץ למסך) | לעתים קרובות |
shadowPath (סטטי) |
GPU | לא |
shadowRadius (דינמי) |
CPU | לעתים קרובות מאוד |
cornerRadius עם masksToBounds = true גורם לעיבוד מחוץ למסך. עבור כל שכבה כזו, Core Animation מבצעת מעבר עיבוד נוסף. ב-Instruments: Debug → Color Offscreen-Rendered מדגיש אותן בצהוב. תיקון: הגדר layer.shadowPath באופן סטטי או השתמש במסכת תמונת וקטור.
כיצד לזהות אנימציות בעייתיות: UIKit
layer.shouldRasterize = true — השתמש בזהירות
drawRect: שומר את השכבה כ-bitmap. זה עוזר אם התוכן לא משתנה. זה הורג ביצועים אם הוא משתנה: המטמון נפסל בכל פריים והציור מחדש הופך ליקר יותר מאשר בלעדיו. בדוק דרך Instruments → Color Hits Green and Misses Red: אדום פירושו פסילה, אין עזרה.
contents = image.cgImage לעומת CALayer
דריסת preferredFramesPerSecond היא אפשרות גרעינית. אם היא נקראת במהלך אנימציה (למשל, שינוי bounds), ה-main thread עסוק בציור. חלופה: שים תוכן סטטי בשכבת CALayer נפרדת עם let displayLink = CADisplayLink(target: self, selector: #selector(tick)) displayLink.preferredFrameRateRange = CAFrameRateRange( minimum: 60, maximum: 120, preferred: 120 ) displayLink.add(to: .main, forMode: .common) , והנפש רק transform.
CADisplayLink לאנימציות מותאמות אישית
אם אתה כותב אנימציות מותאמות אישית עם CADisplayLink, קשר אותו ל-.automatic:
let displayLink = CADisplayLink(target: self, selector: #selector(tick))
displayLink.preferredFrameRateRange = CAFrameRateRange(
minimum: 60,
maximum: 120,
preferred: 120
)
displayLink.add(to: .main, forMode: .common)במכשירי ProMotion, זה מאפשר לאנימציות לרוץ ב-120 FPS. ללא ציון הטווח, המערכת עלולה לנעול ל-60 אפילו במסך 120 הרץ.
Lottie: בעיות ביצועים נפוצות
Lottie משתמש כברירת מחדל במצב עיבוד animationView.renderingEngine = .coreAnimation . באנימציות מורכבות עם מסכות ונתיבי חיתוך, זה לעתים קרובות אומר עיבוד CPU. כפה מעבר:
animationView.renderingEngine = .coreAnimation מנוע ה-Core Animation מעבד דרך CALayers ללא השתתפות ה-main thread. מגבלה: כמה אפקטים מורכבים (gradients דרך נתיבי חיתוך, מצבי מיזוג מסוימים) אינם נתמכים. בדוק עם Lottie Diagnostics.
מידע נוסף על עבודה עם Lottie
כדי להפחית עומס, אנו ממליצים להחליף מסכות מורכבות בצורות פשוטות או להשתמש בגרפיקה וקטורית.Compose: המלצות
השתמש ב-Modifier.graphicsLayer במקום לשנות ישירות פרמטרי פריסה:
// Плохо: вызывает relayout на каждый кадр
Box(modifier = Modifier.size(animatedSize))
// Хорошо: только GPU transform, layout стабилен
Box(
modifier = Modifier
.size(100.dp)
.graphicsLayer {
scaleX = animatedScale
scaleY = animatedScale
}
)// Плохо: вызывает relayout на каждый кадр Box(modifier = Modifier.size(animatedSize)) // Хорошо: только GPU transform, layout стабилен Box(modifier = Modifier .size(100.dp) .graphicsLayer { scaleX = animatedScale; scaleY = animatedScale } ) פועל בדומה ל-graphicsLayer ב-UIKit—מחוץ למעבר הפריסה.
הימנע משימוש ב-layer.transform בתוך lambdas של אנימציה. כל עדכון מצב דרך mutableStateOf מפעיל recomposition. השתמש ב-Animatable ישירות, או ב-remember { mutableStateOf() } שמעדכן רק את graphicsLayer ללא recomposition של המסך.
מקרי אופטימיזציה אופייניים מהפרקטיקה שלנו
באפליקציה של לקוח, רשימה עם תאים מותאמים אישית ירדה ל-40 FPS במהלך גלילה. סיבה: לכל תא היה layer.cornerRadius = 12 עם masksToBounds = true ו-layer.shadowRadius = 8. מעבר עיבוד כפול מחוץ למסך לכל תא. פתרון: רדיוס פינות דרך מסכת UIBezierPath (מעבר GPU יחיד), צל דרך shadowPath עם CGPath מחושב מראש. ה-FPS חזר ל-60 יציב. אנו מבטיחים תוצאות דומות בעבודה עם הפרויקט שלך.
השוואת ביצועים: UIKit לעומת Compose
| טכנולוגיה | ירידת FPS אופיינית | עומס על Main Thread | המלצה |
|---|---|---|---|
| UIKit עם transform/opacity | 0–2% | נמוך | הנפש רק מאפיינים אלה |
| UIKit עם bounds/corner | 10–20% | גבוה | החלף בשכבות GPU |
| Compose עם graphicsLayer | 0–5% | נמוך | השתמש ב-graphicsLayer |
| Compose עם mutableStateOf | 5–15% | בינוני | השתמש ב-Animatable |
תהליך האופטימיזציה
- פרופיל עם Instruments / Android Profiler במכשירים אמיתיים (לפחות מכשיר איטי אחד מקהל היעד).
- זהה עיבוד מחוץ למסך, קריאות ציור יקרות, אנימציות CPU.
- תקן אחד אחד, מדוד לאחר כל שינוי.
- בדיקת רגרסיה במכשירים ברמות שונות.
מה כוללת העבודה
- ביקורת מפורטת של האנימציות הנוכחיות עם דוח.
- תיקון אזורים בעייתיים (החלפת מאפיינים, אופטימיזציה של שכבות).
- מדידת FPS חוזרת במכשירי בקרה.
- המלצות להמשך פיתוח.
אנחנו בפיתוח מובייל למעלה מ-7 שנים ואופטימיזציה של אנימציות ב-20+ פרויקטים. המומחים שלנו מוסמכים על ידי Apple ו-Google. Apple Core Animation Programming Guide ו-Android Profiler הם הכלים העיקריים שלנו.
צור קשר כדי לבקש ביקורת או אופטימיזציה של האפליקציה שלך. קבל ייעוץ בנושאים ספציפיים—נעריך את הפרויקט שלך תוך יום אחד.







