Verimlilik için iOS Animasyon ve Ayarlama
Yayınlanan: 2022-03-11Harika bir uygulama oluşturmak yalnızca görünüm veya işlevsellikle ilgili değildir, aynı zamanda ne kadar iyi performans gösterdiğiyle de ilgilidir. Mobil cihazların donanım özellikleri hızla gelişiyor olsa da, düşük performans gösteren, her ekran geçişinde takılma yapan veya slayt gösterisi gibi kaydırma yapan uygulamalar, kullanıcının deneyimini mahvedebilir ve hayal kırıklığına neden olabilir. Bu yazıda, bir iOS uygulamasının performansının nasıl ölçüleceğini ve verimlilik için nasıl ayarlanacağını göreceğiz. Bu makalenin amacı için, uzun bir resim ve metin listesi içeren basit bir uygulama oluşturacağız.
Performansı test etmek amacıyla gerçek cihazların kullanılmasını tavsiye ederim. Uygulamalar oluşturma ve bunları sorunsuz iOS animasyonu için optimize etme konusunda ciddiyseniz, simülatörler bunu kesmez. Simülasyonlar bazen gerçeklikten uzak olabilir. Örneğin, simülatör Mac'inizde çalışıyor olabilir, bu da muhtemelen CPU'nun (Merkezi İşlem Birimi) iPhone'unuzdaki CPU'dan çok daha güçlü olduğu anlamına gelir. Bunun tersine, GPU (Grafik İşlem Birimi), cihazınızla Mac'iniz arasında o kadar farklıdır ki, Mac'iniz aslında cihazın GPU'sunu taklit eder. Sonuç olarak, CPU'ya bağlı işlemler simülatörünüzde daha hızlı olma eğilimindeyken, GPU'ya bağlı işlemler daha yavaş olma eğilimindedir.
60 FPS'de animasyon
Algılanan performansın önemli bir yönü, animasyonlarınızın ekranınızın yenileme hızı olan 60 FPS'de (saniyede kare) çalışmasını sağlamaktır. Burada bahsetmeyeceğimiz bazı zamanlayıcı tabanlı animasyonlar var. Genel olarak konuşursak, 50 FPS'den daha yüksek bir hızda çalışıyorsanız, uygulamanız sorunsuz ve performanslı görünecektir. Animasyonlarınız 20 ile 40 FPS arasında takılırsa gözle görülür bir takılma olur ve kullanıcı geçişlerde "pürüzlülük" algılar. 20 FPS'nin altındaki herhangi bir şey, uygulamanızın kullanılabilirliğini ciddi şekilde etkiler.
Başlamadan önce, muhtemelen CPU'ya bağlı ve GPU'ya bağlı işlemler arasındaki farkı tartışmaya değer. GPU, grafik çizmek için optimize edilmiş özel bir çiptir. CPU da yapabilirken, çok daha yavaştır. Bu nedenle, 2B veya 3B modelden bir görüntü oluşturma süreci olan grafik işlememizin çoğunu GPU'ya boşaltmak istiyoruz. Ancak GPU'nun işlem gücü bittiğinde, CPU nispeten boş olsa bile grafiklerle ilgili performans düşeceğinden dikkatli olmalıyız.
Core Animation, hem uygulamanızın içinde hem de dışında animasyonu işleyen güçlü bir çerçevedir. Süreci 6 temel adıma böler:
Düzen: Katmanlarınızı düzenlediğiniz ve renk ve göreli konumları gibi özelliklerini ayarladığınız yer
Görüntüleme: Bu, destek görüntülerinin bir bağlam üzerine çizildiği yerdir.
drawRect:
veyadrawLayer:inContext:
içinde yazdığınız herhangi bir rutine buradan erişilebilir.Hazırla: Bu aşamada Core Animation, çizim yapmak için oluşturucuya bağlam göndermek üzere olduğundan, görüntülerin sıkıştırılması gibi bazı gerekli görevleri gerçekleştirir.
Taahhüt: Burada Core Animation, tüm bu verileri oluşturma sunucusuna gönderir.
Seri durumdan çıkarma: Önceki 4 adımın tamamı uygulamanızın içindeydi, şimdi animasyon uygulamanızın dışında işleniyor, paketlenmiş katmanlar, oluşturma sunucusunun anlayabileceği bir ağaçta seri durumdan çıkarılıyor. Her şey OpenGL geometrisine dönüştürülür.
Draw: Şekilleri işler (aslında üçgenler).
1-4 arasındaki işlemlerin CPU işlemleri ve 5-6 işlemlerinin GPU işlemleri olduğunu tahmin etmiş olabilirsiniz. Gerçekte sadece ilk 2 adım üzerinde kontrole sahipsiniz. GPU'nun en büyük katili, GPU'nun aynı pikseli kare başına birden çok kez doldurması gereken yarı saydam katmanlardır. Ayrıca herhangi bir ekran dışı çizim (gölgeler, maskeler, yuvarlatılmış köşeler veya katman rasterleştirme gibi çeşitli katman efektleri, Core Animation'ı ekran dışında çizmeye zorlayacaktır) de performansı etkileyecektir. GPU tarafından işlenemeyecek kadar büyük olan resimler, bunun yerine çok daha yavaş CPU tarafından işlenecektir. Doğrudan katmana iki özellik ayarlanarak gölgeler kolayca elde edilebilirken, ekranda gölgeli çok sayıda nesneniz varsa performansı kolayca öldürebilirler. Bazen bu gölgeleri resim olarak eklemeyi düşünmeye değer.
iOS Animasyon Performansını Ölçme
5 PNG resimli basit bir uygulama ve bir tablo görünümü ile başlayacağız. Bu uygulamada, esasen 5 resim yükleyeceğiz, ancak bunu 10.000 satırın üzerinde tekrarlayacağız. Hem resimlere hem de resimlerin yanındaki etiketlere gölgeler ekleyeceğiz:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath]; NSInteger index = (indexPath.row % [self.images count]); NSString *imageName = [self.images objectAtIndex:index]; NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:filePath]; cell.customCellImageView.image = image; cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5); cell.customCellImageView.layer.shadowOpacity = 0.8f; cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)]; cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3); cell.customCellMainLabel.layer.shadowOpacity = 0.5f; return cell; }
Etiketler her zaman farklıyken görüntüler basitçe geri dönüştürülür. Sonuç:
Dikey olarak kaydırdığınızda, görünüm kayarken kekemelik fark etmeniz çok olasıdır. Bu noktada, ana konuya resim yüklememizin sorun olduğunu düşünebilirsiniz. Belki bunu arka planda tutarsak tüm sorunlarımız çözülebilir.
Kör tahminler yapmak yerine deneyelim ve performansını ölçelim. Enstrümanlar zamanı.
Enstrümanları kullanmak için “Çalıştır”dan “Profil”e geçmeniz gerekir. Ayrıca gerçek cihazınıza da bağlı olmalısınız, simülatörde tüm enstrümanlar mevcut değildir (simülatörde performansı optimize etmemenizin başka bir nedeni!). Öncelikle “GPU Driver”, “Core Animation” ve “Time Profiler” şablonlarını kullanacağız. Az bilinen bir gerçek, farklı bir enstrümanda durup çalıştırmak yerine, birden fazla enstrümanı sürükleyip bırakabilir ve aynı anda birkaç tane çalıştırabilirsiniz.
Şimdi aletlerimizi kurduk, hadi ölçelim. Öncelikle FPS'mizde gerçekten bir problem olup olmadığına bakalım.
Yikes, sanırım burada 18 FPS alıyoruz. Paketteki görüntüleri ana iş parçacığına yüklemek gerçekten bu kadar pahalı ve maliyetli mi? Oluşturucu kullanımımızın neredeyse maksimuma ulaştığına dikkat edin. Kiremit kullanımımız da öyle. İkisi de %95'in üzerinde. Ve bunun ana iş parçacığındaki paketten bir görüntü yüklemekle ilgisi yok, bu yüzden burada çözüm aramayalım.
Verimlilik için Ayarlama
ShouldRasterize adında bir özellik var ve insanlar muhtemelen burada kullanmanızı tavsiye edeceklerdir. ShouldRasterize tam olarak ne yapar? Katmanınızı düzleştirilmiş bir görüntü olarak önbelleğe alır. Tüm bu pahalı katman çizimlerinin bir kez gerçekleşmesi gerekiyor. Çerçevenizin sık sık değişmesi durumunda, yine de her seferinde yeniden oluşturulması gerekeceğinden önbellek kullanımı yoktur.
Kodumuzda hızlı bir değişiklik yaparak şunları elde ederiz:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath]; NSInteger index = (indexPath.row % [self.images count]); NSString *imageName = [self.images objectAtIndex:index]; NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:filePath]; cell.customCellImageView.image = image; cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5); cell.customCellImageView.layer.shadowOpacity = 0.8f; cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)]; cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3); cell.customCellMainLabel.layer.shadowOpacity = 0.5f; cell.layer.shouldRasterize = YES; cell.layer.rasterizationScale = [UIScreen mainScreen].scale; return cell; }
Ve tekrar ölçüyoruz:

Sadece iki satır ile FPS'mizi 2 kat artırdık. Şu anda ortalama 40 FPS'nin üzerindeyiz. Ancak, resim yüklemeyi bir arka plan dizisine taşısaydık yardımcı olur muydu?
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath]; NSInteger index = (indexPath.row % [self.images count]); NSString *imageName = [self.images objectAtIndex:index]; cell.tag = indexPath.row; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:filePath]; dispatch_async(dispatch_get_main_queue(), ^{ if (indexPath.row == cell.tag) { cell.customCellImageView.image = image; } }); }); cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5); cell.customCellImageView.layer.shadowOpacity = 0.8f; cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)]; cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3); cell.customCellMainLabel.layer.shadowOpacity = 0.5f; // cell.layer.shouldRasterize = YES; // cell.layer.rasterizationScale = [UIScreen mainScreen].scale; return cell; }
Ölçüm üzerine, performansın ortalama 18 FPS civarında olduğunu görüyoruz:
Kutlamaya değer bir şey yok. Başka bir deyişle, kare hızımızda hiçbir iyileştirme yapmadı. Bunun nedeni, ana iş parçacığının tıkanması yanlış olsa da, bu bizim darboğazımız değildi, render oldu.
40FPS'nin üzerinde ortalama aldığımız daha iyi örneğe geri dönersek, performans belirgin şekilde daha pürüzsüz. Ama aslında daha iyisini yapabiliriz.
Core Animation Tool'da “Color Blended Layers”ı kontrol ederek şunu görüyoruz:
Ekranda GPU'nuzun çok fazla işleme yaptığı “Renk Harmanlanmış Katmanlar” gösterilir. Yeşil, en az işleme faaliyeti miktarını gösterirken, kırmızı en fazla olanı gösterir. Ama ShouldRasterize'ı YES
olarak ayarladık. “Renk Harmanlanmış Katmanlar”ın “Renk Yeşile Vurur ve Kırmızıyı Kaçırır” ile aynı şey olmadığını belirtmekte fayda var. Daha sonra, önbellek yenilenirken temel olarak rasterleştirilmiş katmanları kırmızıyla vurgular (önbelleği düzgün kullanıp kullanmadığınızı görmek için iyi bir araç). Rasterize Etmeli ayarının YES
olarak ayarlanması, opak olmayan katmanların ilk işlemesi üzerinde hiçbir etkiye sahip değildir.
Bu önemli bir nokta ve düşünmek için biraz ara vermemiz gerekiyor. Rasterize öğesinin YES
olarak ayarlanıp ayarlanmadığına bakılmaksızın, çerçeveyi oluşturmak için tüm görünümleri kontrol etmesi ve alt görünümlerin şeffaf mı yoksa opak mı olduğuna bağlı olarak harmanlaması (ya da karıştırmaması) gerekir. UILabel'inizin opak olmaması mantıklı olsa da, değersiz olabilir ve performansınızı öldürebilir. Örneğin, beyaz bir arka plan üzerinde şeffaf bir UILabel muhtemelen değersizdir. Opak hale getirelim:
Bu, daha iyi performans sağlar, ancak uygulamanın görünümü ve hissi değişti. Şimdi, etiketimiz ve resimlerimiz opak olduğu için gölge, resmimizin etrafında hareket etti. Muhtemelen kimse bu değişikliği sevmeyecek ve orijinal görünümü ve hissi birinci sınıf performansla korumak istiyorsak, umudumuzu yitirmiyoruz.
Orijinal görünümü korurken ekstra FPS'yi sıkıştırmak için, şu ana kadar ihmal ettiğimiz Çekirdek Animasyon aşamalarımızdan ikisini tekrar gözden geçirmek önemlidir.
- Hazırlamak
- İşlemek
Bunlar tamamen elimizde değilmiş gibi görünebilir, ancak bu tam olarak doğru değil. Bir görüntünün yüklenmesi için sıkıştırılması gerektiğini biliyoruz. Dekompresyon süresi, görüntü formatına bağlı olarak değişir. PNG'ler için sıkıştırma açma işlemi JPEG'lerden çok daha hızlıdır (ancak yükleme daha uzundur ve bu da görüntü boyutuna bağlıdır), bu nedenle PNG'leri kullanmak için bir bakıma doğru yoldaydık, ancak sıkıştırma işlemi ve bu açma hakkında hiçbir şey yapmıyoruz. “çizim noktasında” oluyor! Bu, ana iş parçacığında - zaman öldürebileceğimiz en kötü yer.
Dekompresyona zorlamanın bir yolu var. Bunu hemen bir UIImageView'ın image özelliğine ayarlayabiliriz. Ancak bu yine de ana iş parçacığındaki görüntüyü açar. Daha iyi bir yol var mı?
Bir tane var. Resmin çizilmeden önce sıkıştırılması gereken bir CGContext'e çizin. Bunu (CPU kullanarak) bir arka plan iş parçacığında yapabilir ve görüntü görünümümüzün boyutuna göre gerektiği şekilde sınır verebiliriz. Bu, ana iş parçacığından yaparak görüntü çizim sürecimizi optimize edecek ve bizi ana iş parçacığı üzerinde gereksiz “hazırlık” hesaplamalarından kurtaracaktır.
Hazır buradayken, resmi çizerken neden gölgeleri eklemiyoruz? Daha sonra görüntüyü tek bir statik, opak görüntü olarak yakalayabilir (ve önbelleğe alabiliriz). Kod aşağıdaki gibidir:
- (UIImage*)generateImageFromName:(NSString*)imageName { //define a boudns for drawing CGRect imgVwBounds = CGRectMake(0, 0, 48, 48); //get the image NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:filePath]; //draw in the context UIGraphicsBeginImageContextWithOptions(imgVwBounds.size, NO, 0); { //get context CGContextRef context = UIGraphicsGetCurrentContext(); //shadow CGContextSetShadowWithColor(context, CGSizeMake(0, 3.0f), 3.0f, [UIColor blackColor].CGColor); CGContextBeginTransparencyLayer (context, NULL); [image drawInRect:imgVwBounds blendMode:kCGBlendModeNormal alpha:1.0f]; CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0); CGContextEndTransparencyLayer(context); } image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
Ve sonunda:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath]; // NSInteger index = (indexPath.row % [self.images count]); // NSString *imageName = [self.images objectAtIndex:index]; // // cell.tag = indexPath.row; // // dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; // UIImage *image = [UIImage imageWithContentsOfFile:filePath]; // // dispatch_async(dispatch_get_main_queue(), ^{ // if (indexPath.row == cell.tag) { // cell.customCellImageView.image = image; // } // }); // }); cell.customCellImageView.image = [self getImageByIndexPath:indexPath]; cell.customCellImageView.clipsToBounds = YES; // cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5); // cell.customCellImageView.layer.shadowOpacity = 0.8f; cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)]; cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3); cell.customCellMainLabel.layer.shadowOpacity = 0.5f; cell.layer.shouldRasterize = YES; cell.layer.rasterizationScale = [UIScreen mainScreen].scale; return cell; }
Ve sonuçlar:
Şu anda ortalama 55 FPS'nin üzerindeyiz ve render kullanımımız ve kiremit kullanımımız başlangıçtakinin neredeyse yarısı.
Sarmak
Saniyede birkaç kare daha atmak için başka neler yapabileceğimizi merak ediyorsanız, başka yere bakmayın. UILabel, metin oluşturmak için WebKit HTML'yi kullanır. Doğrudan CATextLayer'a gidebilir ve belki oradaki gölgelerle de oynayabiliriz.
Yukarıdaki uygulamamızda fark etmiş olabilirsiniz, bir arka plan dizisinde görüntü yükleme yapmıyorduk ve bunun yerine onu önbelleğe alıyorduk. Yalnızca 5 görüntü olduğundan, bu gerçekten hızlı çalıştı ve genel performansı etkilemedi (özellikle 5 görüntünün tümü kaydırmadan önce ekrana yüklendiğinden). Ancak, ekstra performans için bu mantığı bir arka plan iş parçacığına taşımayı denemek isteyebilirsiniz.
Verimliliği ayarlamak, birinci sınıf bir uygulama ile amatör bir uygulama arasındaki farktır. Performans optimizasyonu, özellikle iOS animasyonu söz konusu olduğunda göz korkutucu bir görev olabilir. Ancak Instruments'ın yardımıyla, iOS'ta animasyon performansındaki darboğazlar kolayca teşhis edilebilir.