Animasi dan Penyetelan iOS untuk Efisiensi
Diterbitkan: 2022-03-11Membangun aplikasi yang hebat bukan hanya tentang tampilan atau fungsionalitas, tetapi juga tentang seberapa baik kinerjanya. Meskipun spesifikasi perangkat keras perangkat seluler meningkat dengan cepat, aplikasi yang berkinerja buruk, tersendat di setiap transisi layar, atau menggulir seperti tayangan slide dapat merusak pengalaman penggunanya dan menjadi penyebab frustrasi. Pada artikel ini kita akan melihat cara mengukur kinerja aplikasi iOS dan menyetelnya untuk efisiensi. Untuk tujuan artikel ini, kami akan membuat aplikasi sederhana dengan daftar panjang gambar dan teks.
Untuk tujuan pengujian kinerja, saya akan merekomendasikan penggunaan perangkat nyata. Jika Anda serius membuat aplikasi, dan mengoptimalkannya untuk animasi iOS yang mulus, simulator tidak akan membantu. Simulasi terkadang tidak sesuai dengan kenyataan. Misalnya, simulator mungkin berjalan di Mac Anda yang mungkin berarti CPU (Central Processing Unit) jauh lebih kuat daripada CPU di iPhone Anda. Sebaliknya, GPU (Graphics Processing Unit) sangat berbeda antara perangkat Anda dan Mac sehingga Mac Anda benar-benar mengemulasi GPU perangkat. Akibatnya, operasi yang terikat CPU cenderung lebih cepat di simulator Anda sementara operasi yang terikat GPU cenderung lebih lambat.
Beranimasi pada 60 FPS
Salah satu aspek kunci dari kinerja yang dirasakan adalah memastikan animasi Anda berjalan pada 60 FPS (frame per detik), yang merupakan kecepatan refresh layar Anda. Ada beberapa animasi berbasis timer, yang tidak akan kita bahas di sini. Secara umum, jika Anda menjalankan sesuatu yang lebih besar dari 50 FPS, aplikasi Anda akan terlihat mulus dan berkinerja. Jika animasi Anda macet antara 20 dan 40 FPS, akan ada stutter yang terlihat dan pengguna akan mendeteksi "kekasaran" dalam transisi. Apa pun di bawah 20 FPS akan sangat memengaruhi kegunaan aplikasi Anda.
Sebelum kita mulai, mungkin ada baiknya membahas perbedaan antara operasi terikat CPU dan terikat GPU. GPU adalah chip khusus yang dioptimalkan untuk menggambar grafik. Sementara CPU juga bisa, itu jauh lebih lambat. Inilah mengapa kami ingin melepas sebanyak mungkin rendering grafis kami, proses menghasilkan gambar dari model 2D atau 3D, ke GPU. Tetapi kita perlu berhati-hati, karena ketika GPU kehabisan daya pemrosesan, kinerja terkait grafis akan menurun meskipun CPU relatif bebas.
Core Animation adalah kerangka kerja yang kuat yang menangani animasi baik di dalam aplikasi Anda, maupun di luarnya. Ini memecah proses menjadi 6 langkah utama:
Tata Letak: Tempat Anda mengatur lapisan dan mengatur propertinya, seperti warna dan posisi relatifnya
Tampilan: Di sinilah gambar latar digambar ke dalam konteks. Rutinitas apa pun yang Anda tulis di
drawRect:
ataudrawLayer:inContext:
diakses di sini.Siapkan: Pada tahap ini Animasi Inti, karena akan mengirim konteks ke penyaji untuk digambar, melakukan beberapa tugas yang diperlukan seperti dekompresi gambar.
Komit: Di sini Core Animation mengirimkan semua data ini ke server render.
Deserialisasi: 4 langkah sebelumnya semuanya ada di dalam aplikasi Anda, sekarang animasi sedang diproses di luar aplikasi Anda, lapisan terpaket dideserialisasi menjadi pohon yang dapat dipahami oleh server render. Semuanya diubah menjadi geometri OpenGL.
Draw: Membuat bentuk (sebenarnya segitiga).
Anda mungkin telah menebak bahwa proses 1-4 adalah operasi CPU dan 5-6 adalah operasi GPU. Pada kenyataannya Anda hanya memiliki kendali atas 2 langkah pertama. Pembunuh terbesar dari GPU adalah lapisan semi-transparan di mana GPU harus mengisi piksel yang sama beberapa kali per frame. Juga setiap gambar di luar layar (beberapa efek lapisan seperti bayangan, topeng, sudut membulat, atau rasterisasi lapisan akan memaksa Animasi Inti untuk menggambar di luar layar) juga akan memengaruhi kinerja. Gambar yang terlalu besar untuk diproses oleh GPU akan diproses oleh CPU yang jauh lebih lambat. Sementara bayangan dapat dengan mudah dicapai dengan mengatur dua properti secara langsung pada lapisan, mereka dapat dengan mudah mematikan kinerja jika Anda memiliki banyak objek di layar dengan bayangan. Terkadang ada baiknya mempertimbangkan untuk menambahkan bayangan ini sebagai gambar.
Mengukur Kinerja Animasi iOS
Kami akan mulai dengan aplikasi sederhana dengan 5 gambar PNG, dan tampilan tabel. Dalam aplikasi ini, kami pada dasarnya akan memuat 5 gambar, tetapi akan mengulanginya lebih dari 10.000 baris. Kami akan menambahkan bayangan ke gambar dan label di sebelah gambar:
-(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; }
Gambar hanya didaur ulang sementara label selalu berbeda. Hasilnya adalah:
Saat menggesek secara vertikal, kemungkinan besar Anda akan melihat kegagapan saat tampilan bergulir. Pada titik ini Anda mungkin berpikir bahwa fakta bahwa kami memuat gambar di utas utama adalah masalahnya. Mungkin jika kita memindahkan ini ke utas latar belakang, semua masalah kita akan terpecahkan.
Daripada menebak-nebak, mari kita coba dan ukur kinerjanya. Saatnya untuk Instrumen.
Untuk menggunakan Instrumen, Anda perlu mengubah dari "Jalankan" menjadi "Profil". Dan Anda juga harus terhubung ke perangkat asli Anda, tidak semua instrumen tersedia di simulator (alasan lain mengapa Anda tidak boleh mengoptimalkan kinerja di simulator!). Kami terutama akan menggunakan template “GPU Driver”, “Core Animation” dan “Time Profiler”. Fakta yang sedikit diketahui adalah bahwa alih-alih berhenti dan menjalankan instrumen yang berbeda, Anda dapat menarik dan melepas beberapa instrumen dan menjalankan beberapa instrumen secara bersamaan.
Sekarang setelah kita menyiapkan instrumen, mari kita ukur. Pertama mari kita lihat apakah kita benar-benar memiliki masalah dengan FPS kita.
Astaga, saya pikir kita mendapatkan 18 FPS di sini. Apakah memuat gambar dari bundel di utas utama benar-benar mahal dan mahal? Perhatikan penggunaan perender kami hampir maksimal. Begitu juga pemanfaatan tiler kami. Keduanya di atas 95%. Dan itu tidak ada hubungannya dengan memuat gambar dari bundel di utas utama, jadi jangan mencari solusi di sini.
Menyetel untuk Efisiensi
Ada properti bernama shouldRasterize, dan orang mungkin akan merekomendasikan Anda untuk menggunakannya di sini. Apa yang harus dilakukan Rasterize? Ini men-cache layer Anda sebagai gambar yang diratakan. Semua gambar layer mahal itu harus terjadi sekali. Jika bingkai Anda sering berubah, tidak ada gunanya cache, karena tetap harus dibuat ulang setiap kali.
Membuat amandemen cepat pada kode kami, kami mendapatkan:
-(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; }
Dan kami mengukur lagi:
Dengan hanya dua baris, kami telah meningkatkan FPS kami sebanyak 2x. Kami sekarang rata-rata di atas 40 FPS. Tetapi apakah akan membantu jika kami telah memindahkan pemuatan gambar ke utas latar belakang?

-(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; }
Setelah mengukur, kami melihat kinerjanya rata-rata sekitar 18 FPS:
Tidak ada yang layak dirayakan. Dengan kata lain, tidak ada peningkatan pada frame rate kami. Itu karena meskipun menyumbat utas utama salah, itu bukan hambatan kami, renderingnya.
Kembali ke contoh yang lebih baik, di mana kami memiliki rata-rata di atas 40FPS, kinerjanya lebih mulus. Tapi sebenarnya kami bisa lebih baik.
Memeriksa "Lapisan Campuran Warna" pada Alat Animasi Inti, kita melihat:
“Lapisan Campuran Warna” ditampilkan di layar tempat GPU Anda melakukan banyak rendering. Hijau menunjukkan paling sedikit jumlah aktivitas rendering sementara merah menunjukkan paling banyak. Tapi kami mengatur shouldRasterize ke YES
. Perlu ditunjukkan bahwa "Lapisan Campuran Warna" tidak sama dengan "Warna Hits Hijau dan Misses Red". Nanti pada dasarnya menyoroti lapisan raster dengan warna merah saat cache dibuat ulang (alat yang bagus untuk melihat apakah Anda tidak menggunakan cache dengan benar). Pengaturan shouldRasterize ke YES
tidak berpengaruh pada rendering awal lapisan non-buram.
Ini adalah poin penting, dan kita perlu berhenti sejenak untuk berpikir. Terlepas dari apakah shouldRasterize disetel ke YES
atau tidak, untuk merender kerangka kerja perlu memeriksa semua tampilan, dan memadukan (atau tidak) berdasarkan apakah subview transparan atau buram. Meskipun masuk akal jika UILabel Anda tidak buram, itu mungkin tidak berharga dan mematikan kinerja Anda. Misalnya UILabel transparan dengan latar belakang putih mungkin tidak berguna. Mari kita membuatnya buram:
Ini menghasilkan kinerja yang lebih baik, tetapi tampilan dan nuansa aplikasi kami telah berubah. Sekarang, karena label dan gambar kita buram, bayangan telah bergerak di sekitar gambar kita. Tidak seorang pun mungkin akan menyukai perubahan ini, dan jika kami ingin mempertahankan tampilan dan nuansa asli dengan kinerja terbaik, kami tidak kehilangan harapan.
Untuk memeras beberapa FPS ekstra sambil mempertahankan tampilan aslinya, penting untuk meninjau kembali dua fase Animasi Inti kami yang telah kami abaikan sejauh ini.
- Mempersiapkan
- Melakukan
Ini mungkin tampak sepenuhnya di luar kendali kita, tetapi itu tidak sepenuhnya benar. Kami tahu bahwa gambar yang akan dimuat perlu didekompresi. Waktu dekompresi berubah tergantung pada format gambar. Untuk PNG dekompresi jauh lebih cepat daripada JPEG (meskipun memuat lebih lama, dan ini tergantung pada ukuran gambar juga), jadi kami berada di jalur yang benar untuk menggunakan PNG, tetapi kami tidak melakukan apa pun tentang proses dekompresi, dan dekompresi ini sedang terjadi pada "titik menggambar"! Ini adalah tempat terburuk di mana kita dapat menghabiskan waktu - di utas utama.
Ada cara untuk memaksa dekompresi. Kita bisa langsung mengaturnya ke properti image dari UIImageView. Tapi itu masih mendekompresi gambar di utas utama. Apakah ada cara yang lebih baik?
Ada satu. Gambarkan ke dalam CGContext, di mana gambar perlu didekompresi sebelum dapat digambar. Kita dapat melakukan ini (menggunakan CPU) di utas latar belakang, dan memberikan batasan seperlunya berdasarkan ukuran tampilan gambar kita. Ini akan mengoptimalkan proses menggambar gambar kita dengan melakukannya di luar utas utama, dan menyelamatkan kita dari perhitungan "persiapan" yang tidak diperlukan di utas utama.
Sementara kita melakukannya, mengapa tidak menambahkan bayangan saat kita menggambar? Kami kemudian dapat menangkap gambar (dan menyimpannya) sebagai satu gambar statis dan buram. Kodenya adalah sebagai berikut:
- (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; }
Dan akhirnya:
-(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; }
Dan hasilnya adalah:
Kami sekarang rata-rata di atas 55 FPS, dan pemanfaatan render dan pemanfaatan tiler kami hampir setengah dari yang semula.
Bungkus
Kalau-kalau Anda bertanya-tanya apa lagi yang bisa kami lakukan untuk memutar beberapa frame per detik, tidak perlu mencari lagi. UILabel menggunakan WebKit HTML untuk merender teks. Kita bisa langsung masuk ke CATextLayer dan mungkin bermain dengan bayangan di sana juga.
Anda mungkin telah memperhatikan dalam implementasi kami di atas, kami tidak melakukan pemuatan gambar di utas latar belakang, dan sebagai gantinya kami menyimpannya dalam cache. Karena hanya ada 5 gambar, ini bekerja sangat cepat dan tampaknya tidak mempengaruhi kinerja secara keseluruhan (terutama karena semua 5 gambar dimuat di layar sebelum digulir). Tetapi Anda mungkin ingin mencoba memindahkan logika ini ke utas latar belakang untuk beberapa kinerja ekstra.
Menyetel untuk efisiensi adalah perbedaan antara aplikasi kelas dunia dan aplikasi amatir. Optimalisasi kinerja, terutama dalam hal animasi iOS, bisa menjadi tugas yang menakutkan. Tetapi dengan bantuan Instrumen, seseorang dapat dengan mudah mendiagnosis kemacetan dalam kinerja animasi di iOS.