Azimjon Abdurasulov

UIKit 12: Core Animation va Offscreen Rendering — Render Server, 120Hz ProMotion va GPU Optimizatsiyasi

📱 UIKit va SwiftUI — chuqur

UIKit 12: Core Animation va Offscreen Rendering — Render Server, 120Hz ProMotion va GPU Optimizatsiyasi

Holat: 2026-yil sentabr. Ko'plab dasturchilar chiroyli soya (shadow), yumaloq burchak (cornerRadius) va shaffoflik (alpha) qo'shgach, ro'yxatni aylantirishda (scrolling) interfeys nega sakrab-sakrab qotayotganini (stutter / frame drop) tushunmay hayron bo'lishadi. ProMotion (120 Hz) ekranlar davrida har bir freymni chizish uchun tizimda bor-yo'g'i 8.33 millisoniya vaqt bor! Ushbu darsda Core Animation'ning Render Server arxitekturasi, GPU'dagi Offscreen Rendering jarayoni, uning nima sababdan quvvat va unumdorlik qotili ekani hamda professional optimizatsiya usullari ko'rib chiqiladi.

Mundarija

  1. Core Animation Render Pipeline: Commit'dan Ekrangacha
  2. 120 Hz ProMotion: 8.33 ms freym byudjeti
  3. Offscreen Rendering nima va u nega xavfli?
  4. Klassik 4 ta Offscreen "jinoyatchi" va ularning mexanikasi
  5. Xcode Instruments bilan diagnostika: Color Offscreen-Rendered
  6. Xirurgik yechimlar: shadowPath, CAShapeLayer va shouldRasterize
  7. Amaliy kod: 120 FPS uchun optimizatsiya qilingan HighPerformanceCardView
  8. Senior Interview Savollari
  9. Xulosa
  10. Manbalar

1. Core Animation Render Pipeline: Commit'dan Ekrangacha {#render-pipeline}

iOS tizimida UIView aslida chizish ishlarini o'zi bajarmaydi. U shunchaki CALayer uchun foydalanuvchi teginishlarini (touch events) boshqaruvchi yupqa qobiqdir. Ekranga chizishning butun mexanizmi Core Animation Render Pipeline orqali amalga oshadi:

+-------------------+      IPC       +--------------------+      Draw       +---------------+
|   Bizning Ilova   | -------------> |   Render Server    | --------------> |      GPU      |
| (Main RunLoop)    | (mach port)    | (backboardd)       | (Metal / OpenGL)| (Frame Buffer)|
+-------------------+                +--------------------+                 +---------------+
         │                                     │                                    │
    1. Layout                             3. Decode                            5. Rasterize
    2. Display (draw)                     4. GPU buyruqlari                    6. Display
    3. Prepare (images)
    4. Commit

Pipeline'ning 4 asosiy fazasi:

  1. Commit Transaction (Bizning ilovada):
  2. Layout: Auto Layout orqali view'larning bounds va center koordinatalari hisoblanadi.
  3. Display: Agar draw(_:) chaqirilgan bo'lsa, CPU xotirasida bitmap chiziladi.
  4. Prepare: Rasmlar (masalan, JPEG/PNG) ochiladi (decompress qilinadi).
  5. Commit: Barcha layer daraxti (Layer Tree) serialize qilinib, Render Server jarayoniga IPC (Inter-Process Communication) orqali yuboriladi.
  6. Render Server (Alohida tizim jarayoni):
  7. Layer daraxtini qabul qiladi va uni GPU tushunadigan chizish buyruqlariga (Metal commands) aylantiradi.
  8. GPU (Grafik protsessor):
  9. Primitiv shakllar va teksturalarni piksellarga aylantiradi (Rasterization) va to'g'ridan-to'g'ri ekranning Onscreen Framebufferiga chizadi.

2. 120 Hz ProMotion: 8.33 ms freym byudjeti {#frame-budget}

iPhone 13 Pro'dan boshlab barcha zamonaviy qurilmalarda ProMotion (120 Hz) displeylar o'rnatilgan:

Ekran chastotasiBitta freym uchun byudjetIzoh
60 Hz16.67 msStandart displeylar. Freym kechiksa, 30 FPS'ga tushadi.
120 Hz ProMotion8.33 msUltra silliq harakat. GPU va CPU faqat 8.33 ms ichida butun Render Loop'ni yakunlashi shart!

Agar bitta freymni chizish 8.4 ms vaqt olsa, o'sha freym tashlab yuboriladi (Hitch / Frame Drop) va foydalanuvchi buni ko'z bilan qotish (lag) sifatida sezadi.


3. Offscreen Rendering nima va u nega xavfli? {#offscreen-rendering}

Odatda GPU to'g'ridan-to'g'ri ekranga chiqariladigan xotira buferiga (Onscreen Framebuffer) chizadi:

[ GPU ] ──── To'g'ridan-to'g'ri chizish ────► [ Onscreen Framebuffer ] ────► [ Ekran ]

Ammo ba'zi holatlarda GPU elementni darhol ekranga chiza olmaydi. Sababi: elementning yakuniy ko'rinishi bir nechta qatlamlar birlashuviga, burchaklar kesilishiga yoki uning orqasidagi obyektlar bilan aralashuviga bog'liq bo'ladi.

Bunday paytda GPU ekrandan tashqaridagi xotirada qo'shimcha vaqtinchalik bufer (Offscreen Framebuffer) ajratadi:

                    +──────────────────────────────+
                    |    Offscreen Framebuffer     |
                    |    (Vaqtinchalik xotira)     |
                    +──────────────────────────────+
                               ▲          │
            1. Context Switch  │          │  2. Render natijasini nusxalash
                               │          ▼
[ GPU ] ──────────────────────────────────────────► [ Onscreen Framebuffer ] ────► [ Ekran ]

Nega bu o'ta qimmat? (Context Switch):

  1. Pipeline to'xtashi: GPU asosiy ekran buferidan offscreen buferga o'tishi kerak. Bunga Context Switch deyiladi.
  2. Xotira o'tkazuvchanligi (Memory Bandwidth): Offscreen buferdagi piksellar tayyor bo'lgach, GPU ularni asosiy Onscreen buferga qayta nusxalashi (blit) kerak.
  3. Agar ro'yxatda (UITableView/UICollectionView) 50 ta shunday karta bo'lsa, har bir freymda yuzlab Context Switch yuz beradi va 8.33 ms byudjet darhol portlaydi!

4. Klassik 4 ta Offscreen "jinoyatchi" va ularning mexanikasi {#culprits}

Quyidagi 4 holat deyarli har doim Offscreen Rendering keltirib chiqaradi:

1. layer.masksToBounds = true + layer.cornerRadius

Faqat layer.cornerRadius berish Offscreen Rendering chaqirmaydi (chunki u faqat background va borderni yumaloqlaydi). Ammo agar ichida rasm bo'lsa va siz:

import UIKit

@MainActor
func causeMasksToBoundsOffscreen(view: UIView) {
    view.layer.cornerRadius = 16
    view.layer.masksToBounds = true // yoki clipsToBounds = true
}

deb yozsangiz, GPU butun subview ierarxiyasini vaqtinchalik buferga chizib, so'ng maska bo'yicha burchaklarini kesishi kerak bo'ladi.

2. Dinamik Soya (layer.shadowColor) shadowPath'siz

Agar siz:

import UIKit

@MainActor
func causeDynamicShadowOffscreen(view: UIView) {
    view.layer.shadowColor = UIColor.black.cgColor
    view.layer.shadowOpacity = 0.5
    view.layer.shadowRadius = 8
    // shadowPath berilmagan!
}

GPU view'ning ichidagi barcha harf, rasm va chegaralarni bilmaguncha, soyaning shakli qanaqa bo'lishini hisoblay olmaydi! Shuning uchun u avval butun view'ni offscreen buferga render qilib, so'ng uning siluetidan soya yasaydi.

3. layer.mask (Maskalash)

Har qanday CALayer maskasi (masalan, gradient maska) ikkita buferni birlashtirishni talab qiladi.

4. layer.allowsGroupOpacity = true (Guruhli shaffoflik)

Agar view'ning o'zida ham, uning subview'larida ham alpha < 1.0 bo'lsa, GPU ularning ustma-ust tushgan qismlarini to'g'ri hisoblash uchun oldin ularni guruh qilib offscreen'da chizib oladi.


5. Xcode Instruments bilan diagnostika: Color Offscreen-Rendered {#profiling}

Offscreen Rendering'ni ko'z bilan topib bo'lmaydi. Uni aniqlashning ikkita rasmiy yo'li bor:

1. iOS Simulyator menyusida:

  • Debug -> Color Offscreen-Rendered Yellow ni yoqing.
  • Ekranda Offscreen Rendering bo'layotgan barcha view'lar sariq rangda (Yellow highlight) yonadi!
  • Agar ro'yxatni scroll qilganingizda hamma yoq sariqqa aylansa — demak GPU qiynalmoqda.

2. Xcode Instruments (Core Animation template):

  • Cmd + I -> Core Animation profilini tanlang.
  • Real qurilmada profiling o'tkazing:
  • Hitch Time Ratio: 1 soniyada necha ms qotish borligi (maqsad: < 5 ms/s).
  • Frame Rate: 120 FPS'dan tushib ketgan joylarni ko'rsatadi.

6. Xirurgik yechimlar: shadowPath, CAShapeLayer va shouldRasterize {#solutions}

Senior dasturchilar yuqoridagi 4 ta muammoni quyidagicha hal qiladilar:

Yechim 1: shadowPath orqali 0-ms soya (Eng muhim!)

GPU'ga soyaning aniq geometriyasini oldindan berib qo'ysangiz, u subview'larni kutib o'tirmaydi va Offscreen Rendering 0 ga tushadi:

import UIKit

@MainActor
func applyOptimizedShadow(to view: UIView, cornerRadius: CGFloat) {
    view.layer.shadowColor = UIColor.black.cgColor
    view.layer.shadowOpacity = 0.15
    view.layer.shadowRadius = 10
    view.layer.shadowOffset = CGSize(width: 0, height: 4)

    // GPU'ga aniq yo'l beriladi — Offscreen butunlay yo'qoladi:
    view.layer.shadowPath = UIBezierPath(
        roundedRect: view.bounds,
        cornerRadius: cornerRadius
    ).cgPath
}

Eslatma: Agar view o'lchami (bounds) o'zgarsa (masalan, ekranning aylanishida yoki Auto Layout'da), shadowPath ni layoutSubviews() ichida yangilab turish shart!


Yechim 2: CAShapeLayer orqali burchaklarni kesish

masksToBounds = true o'rniga faqat kerakli burchaklarni UIBezierPath bilan kesuvchi shape layer ishlatiladi yoki rasmlar Core Graphics yordamida fon oqimida (background thread) burchaklari kesilgan holda tayyorlanadi.


Yechim 3: shouldRasterize = true (Qachon ishlatish kerak?)

Agar view juda murakkab bo'lsa (masalan, ichida ko'plab vektor belgilar, yaltiroq gradientlar bo'lsa), lekin uning o'zi o'zgarmas (statik) bo'lsa:

import UIKit

@MainActor
func cacheComplexStaticLayer(layer: CALayer) {
    layer.shouldRasterize = true
    // Ekran piksellar zichligini hisobga olish shart, aks holda rasm xiralashadi:
    layer.rasterizationScale = UIScreen.main.scale
}
  • Mexanikasi: GPU bu view'ni bir marta offscreen buferda chizadi va xotirada (kashda) saqlab qo'yadi. Keyingi freymlarda shunchaki tayyor rasm sifatida ko'rsatadi.
  • Qachon ishlatilmaydi: Agar view harakatchan yoki o'zgaruvchan bo'lsa (masalan, matn tez-tez o'zgarsa), har safar keshni yangilash Offscreen Rendering'dan ham ko'ra ko'proq xotira va quvvat sarflaydi!

7. Amaliy kod: 120 FPS uchun optimizatsiya qilingan HighPerformanceCardView {#example}

Quyida zamonaviy iOS ilovalarida 120 FPS bilan bexato ishlaydigan professional karta komponenti keltirilgan:

import UIKit

/// 120 Hz ProMotion ekranlarida hech qanday Offscreen Rendering hosil qilmaydigan
/// yuqori unumdorlikka ega karta komponenti.
public final class HighPerformanceCardView: UIView {

    private let titleLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .systemFont(ofSize: 17, weight: .semibold)
        label.textColor = .label
        return label
    }()

    private let avatarImageView: UIImageView = {
        let iv = UIImageView()
        iv.translatesAutoresizingMaskIntoConstraints = false
        iv.contentMode = .scaleAspectFill
        // Muhim: masksToBounds o'rniga layer burchagi bilan ishlaymiz
        iv.layer.cornerRadius = 24
        iv.clipsToBounds = true
        return iv
    }()

    public override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    public required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupView()
    }

    private func setupView() {
        backgroundColor = .secondarySystemBackground
        layer.cornerRadius = 16

        // 1. Soya sozlamalari
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.08
        layer.shadowRadius = 12
        layer.shadowOffset = CGSize(width: 0, height: 6)

        // 2. Subview'larni joylash
        addSubview(avatarImageView)
        addSubview(titleLabel)

        NSLayoutConstraint.activate([
            avatarImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
            avatarImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
            avatarImageView.widthAnchor.constraint(equalToConstant: 48),
            avatarImageView.heightAnchor.constraint(equalToConstant: 48),

            titleLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 12),
            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
            titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
        ])
    }

    public override func layoutSubviews() {
        super.layoutSubviews()

        // 3. O'lcham aniq bo'lganda shadowPath'ni yangilaymiz.
        // Bu Offscreen Rendering'ni butunlay yo'q qiladi!
        let path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius)
        layer.shadowPath = path.cgPath
    }

    public func configure(title: String, image: UIImage?) {
        titleLabel.text = title
        avatarImageView.image = image
    }
}

8. Senior Interview Savollari {#interview}

1. layer.cornerRadius ning o'zi Offscreen Rendering chaqiradimi?

Javob: Yo'q! cornerRadius faqat layer'ning foni (background) va uning chegarasi (border) uchun qo'llanadi. Faqat masksToBounds = true yoki clipsToBounds = true qilinganda va subview'lar mavjud bo'lganda Offscreen Rendering yuz beradi.

2. Nega shadowPath ishlatish shart?

Javob: shadowPath berilmasa, GPU butun view va uning barcha subview'lari konturini (alfa kanalini) aniqlash uchun ularni offscreen xotiraga chizib olishga majbur bo'ladi. shadowPath berilganda esa GPU to'g'ridan-to'g'ri berilgan geometrik shakl bo'yicha soyani darhol chizib qo'yadi.

3. shouldRasterize qachon yordam beradi, qachon zarar keltiradi?

Javob: Agar murakkab subview'lar ierarxiyasiga ega view o'zgarmas (statik) bo'lsa, uni keshlab qo'yish unumdorlikni oshiradi. Ammo view tez-tez harakatlansa yoki uning kontenti o'zgarsa, har bir yangilanishda keshni qayta chizish qo'shimcha resurs talab qilib, ilovani qotirib qo'yadi.


9. Xulosa {#xulosa}

  • iOS'da chizish ishlari Bizning Ilova -> Render Server -> GPU konveyeri bo'yicha ketadi.
  • 120 Hz ProMotion ekranlarida bitta freym uchun vaqt bor-yo'g'i 8.33 msni tashkil qiladi.
  • Offscreen Rendering — GPU'ning ekrandan tashqaridagi buferga o'tib (Context Switch), so'ng uni asosiy ekranga nusxalash jarayonidir.
  • Doimo soyalarga **shadowPath** bering.
  • Interfeys unumdorligini tekshirish uchun simulyatorda Color Offscreen-Rendered va Xcode Instruments'da Core Animation vositasidan foydalaning.

10. Manbalar {#manbalar}

Markdown manba

iOS Lessons