الرسوم المتحركة iOS وضبطها لتحقيق الكفاءة

نشرت: 2022-03-11

لا يتعلق إنشاء تطبيق رائع بالمظهر أو الوظائف فحسب ، بل يتعلق أيضًا بمدى أدائه الجيد. على الرغم من أن مواصفات الأجهزة الخاصة بالأجهزة المحمولة تتحسن بوتيرة سريعة ، فإن التطبيقات التي تعمل بشكل ضعيف أو تتلعثم في كل انتقال للشاشة أو التمرير مثل عرض الشرائح يمكن أن تدمر تجربة المستخدم وتصبح سببًا للإحباط. سنرى في هذه المقالة كيفية قياس أداء تطبيق iOS وضبطه لتحقيق الكفاءة. لغرض هذه المقالة ، سننشئ تطبيقًا بسيطًا بقائمة طويلة من الصور والنصوص.

الرسوم المتحركة iOS وضبطها لتحقيق الكفاءة

لأغراض اختبار الأداء ، أوصي باستخدام أجهزة حقيقية. إذا كنت جادًا في إنشاء التطبيقات وتحسينها للحصول على رسوم متحركة سلسة لنظام iOS ، فإن المحاكاة ببساطة لا تقطعها. قد تكون المحاكاة في بعض الأحيان بعيدة عن الواقع. على سبيل المثال ، قد يكون المحاكي قيد التشغيل على جهاز Mac الخاص بك مما يعني على الأرجح أن وحدة المعالجة المركزية (وحدة المعالجة المركزية) أقوى بكثير من وحدة المعالجة المركزية على جهاز iPhone الخاص بك. على العكس من ذلك ، تختلف وحدة معالجة الرسومات (GPU) بين جهازك وجهاز Mac الخاص بك لدرجة أن جهاز Mac الخاص بك يحاكي بالفعل وحدة معالجة الرسومات الخاصة بالجهاز. نتيجة لذلك ، تميل العمليات المرتبطة بوحدة المعالجة المركزية إلى أن تكون أسرع على جهاز المحاكاة الخاص بك بينما تميل العمليات المرتبطة بوحدة معالجة الرسومات إلى أن تكون أبطأ.

الرسوم المتحركة بمعدل 60 إطارًا في الثانية

يتمثل أحد الجوانب الرئيسية للأداء المدرك في التأكد من تشغيل الرسوم المتحركة بمعدل 60 إطارًا في الثانية (إطارات في الثانية) ، وهو معدل تحديث شاشتك. هناك بعض الرسوم المتحركة على أساس المؤقت ، والتي لن نناقشها هنا. بشكل عام ، إذا كنت تعمل بسرعة أكبر من 50 إطارًا في الثانية ، فسيبدو تطبيقك سلسًا وعالي الأداء. إذا توقفت الرسوم المتحركة الخاصة بك بين 20 و 40 إطارًا في الثانية ، فسيكون هناك تلعثم ملحوظ وسيكتشف المستخدم "خشونة" في الانتقالات. أي شيء أقل من 20 إطارًا في الثانية سيؤثر بشدة على قابلية استخدام تطبيقك.

قبل أن نبدأ ، ربما يجدر مناقشة الاختلاف بين العمليات المرتبطة بوحدة المعالجة المركزية والعمليات المرتبطة بوحدة معالجة الرسومات. GPU عبارة عن شريحة متخصصة تم تحسينها لرسم الرسومات. بينما يمكن لوحدة المعالجة المركزية أيضًا ، فهي أبطأ بكثير. هذا هو السبب في أننا نريد إلغاء تحميل أكبر قدر من عرض الرسومات لدينا ، عملية إنشاء صورة من نموذج ثنائي الأبعاد أو ثلاثي الأبعاد ، إلى وحدة معالجة الرسومات. لكننا بحاجة إلى توخي الحذر ، لأنه عندما تنفد وحدة معالجة الرسومات (GPU) من قوة المعالجة ، فإن الأداء المرتبط بالرسومات سوف يتدهور حتى لو كانت وحدة المعالجة المركزية مجانية نسبيًا.

Core Animation هو إطار عمل قوي يتعامل مع الرسوم المتحركة داخل التطبيق وخارجه. يقسم العملية إلى 6 خطوات رئيسية:

  1. التخطيط: حيث تقوم بترتيب الطبقات وتعيين خصائصها ، مثل اللون وموضعها النسبي

  2. العرض: هذا هو المكان الذي يتم فيه رسم الصور الداعمة على سياق. أي روتين كتبته في drawRect drawRect: أو drawLayer:inContext: يمكن الوصول إليه هنا.

  3. التحضير: في هذه المرحلة ، تؤدي الرسوم المتحركة الأساسية ، نظرًا لأنها على وشك إرسال سياق إلى العارض للاستفادة منه ، بعض المهام الضرورية مثل فك ضغط الصور.

  4. الالتزام: هنا ترسل Core Animation كل هذه البيانات إلى خادم التقديم.

  5. إلغاء التسلسل: كانت جميع الخطوات الأربع السابقة داخل تطبيقك ، والآن تتم معالجة الرسوم المتحركة خارج تطبيقك ، ويتم إلغاء تسلسل الطبقات المجمعة إلى شجرة يمكن لخادم العرض فهمها. يتم تحويل كل شيء إلى هندسة OpenGL.

  6. الرسم: يجسد الأشكال (في الواقع مثلثات).

ربما تكون قد خمنت أن العمليات 1-4 هي عمليات وحدة المعالجة المركزية و 5-6 عمليات GPU. في الواقع ، لا يمكنك التحكم إلا في أول خطوتين. أكبر قاتل لوحدة معالجة الرسومات هو الطبقات شبه الشفافة حيث يتعين على وحدة معالجة الرسومات ملء نفس البكسل عدة مرات لكل إطار. وأيضًا أي رسم خارج الشاشة (تأثيرات طبقة متعددة مثل الظلال أو الأقنعة أو الزوايا الدائرية أو تنقيط الطبقة ستجبر الرسوم المتحركة الأساسية على الرسم خارج الشاشة) سيؤثر أيضًا على الأداء. ستتم معالجة الصور الكبيرة جدًا بحيث لا يمكن معالجتها بواسطة وحدة معالجة الرسومات بواسطة وحدة المعالجة المركزية (CPU) الأبطأ كثيرًا بدلاً من ذلك. بينما يمكن تحقيق الظلال بسهولة عن طريق تعيين خاصيتين مباشرة على الطبقة ، إلا أنها يمكن أن تقتل الأداء بسهولة إذا كان لديك العديد من الكائنات على الشاشة مع الظلال. يجدر أحيانًا التفكير في إضافة هذه الظلال كصور.

قياس أداء الرسوم المتحركة لنظام iOS

سنبدأ بتطبيق بسيط يحتوي على 5 صور PNG وعرض جدول. في هذا التطبيق ، سنقوم بشكل أساسي بتحميل 5 صور ، لكننا سنكررها على أكثر من 10000 صف. سنضيف الظلال إلى كل من الصور والتسميات الموجودة بجانب الصور:

 -(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; }

يتم إعادة تدوير الصور ببساطة بينما تكون التسميات مختلفة دائمًا. النتيجه هي:

عند التمرير رأسيًا ، من المحتمل جدًا أن تلاحظ تلعثمًا أثناء تمرير العرض. في هذه المرحلة ، قد تعتقد أن حقيقة أننا نقوم بتحميل الصور في الموضوع الرئيسي هي المشكلة. قد يكون إذا نقلنا هذا إلى موضوع الخلفية ، فسيتم حل جميع مشاكلنا.

بدلاً من التخمينات العمياء ، دعنا نجربها ونقيس الأداء. حان الوقت للأدوات.

لاستخدام الآلات ، تحتاج إلى التغيير من "تشغيل" إلى "الملف الشخصي". ويجب أيضًا أن تكون متصلاً بجهازك الحقيقي ، فليست جميع الأدوات متاحة على جهاز المحاكاة (سبب آخر لعدم قيامك بتحسين الأداء على جهاز المحاكاة!). سنستخدم بشكل أساسي قوالب "GPU Driver" و "Core Animation" و "Time Profiler". هناك حقيقة غير معروفة وهي أنه بدلاً من التوقف وتشغيل أداة مختلفة ، يمكنك سحب وإسقاط أدوات متعددة وتشغيل عدة أدوات في نفس الوقت.

الآن بعد أن تم تجهيز أدواتنا ، دعنا نقيس. أولاً ، دعنا نرى ما إذا كانت لدينا مشكلة بالفعل مع FPS.

Yikes ، أعتقد أننا حصلنا على 18 إطارًا في الثانية هنا. هل تحميل الصور من الحزمة على الموضوع الرئيسي حقاً باهظ الثمن ومكلف؟ لاحظ أن استخدام العارض الخاص بنا قد بلغ الحد الأقصى تقريبًا. هذا هو استخدامنا القرميد. كلاهما فوق 95٪. وهذا ليس له علاقة بتحميل صورة من الحزمة في الخيط الرئيسي ، لذلك دعونا لا نبحث عن الحلول هنا.

ضبط من أجل الكفاءة

هناك خاصية تسمى shouldRasterize ، وربما يوصيك الناس باستخدامها هنا. ما الذي يجب أن تفعله Rasterize بالضبط؟ فإنه يخزن الطبقة الخاصة بك كصورة بالارض. كل تلك الرسومات الطبقية باهظة الثمن يجب أن تحدث مرة واحدة. في حالة تغيير إطارك بشكل متكرر ، فلا فائدة من ذاكرة التخزين المؤقت ، حيث سيتعين إعادة إنشائها في كل مرة على أي حال.

عند إجراء تعديل سريع على الكود الخاص بنا ، نحصل على:

 -(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; }

ونقيس مرة أخرى:

باستخدام سطرين فقط ، قمنا بتحسين FPS لدينا بمقدار 2x. نحن الآن يبلغ متوسطنا فوق 40 إطارًا في الثانية. ولكن هل سيساعدنا إذا نقلنا تحميل الصورة إلى سلسلة رسائل في الخلفية؟

 -(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; }

عند القياس ، نرى أن متوسط ​​الأداء يبلغ حوالي 18 إطارًا في الثانية:

لا شيء يستحق الاحتفال. بعبارة أخرى ، لم يحدث أي تحسن في معدل عرض الإطارات لدينا. هذا لأنه على الرغم من أن انسداد الخيط الرئيسي خطأ ، إلا أنه لم يكن عنق الزجاجة لدينا ، كان التقديم.

بالعودة إلى المثال الأفضل ، حيث كان متوسطنا أعلى من 40 إطارًا في الثانية ، كان الأداء أكثر سلاسة بشكل ملحوظ. لكن في الواقع يمكننا القيام بعمل أفضل.

عند التحقق من "طبقات الألوان الممزوجة" في أداة الرسوم المتحركة الأساسية ، نرى:

يظهر "Color Blended Layers" على الشاشة حيث يقوم GPU الخاص بك بالكثير من العرض. يشير اللون الأخضر إلى أقل قدر من نشاط العرض بينما يشير اللون الأحمر إلى أكبر قدر. لكننا حددنا shouldRasterize إلى YES . وتجدر الإشارة إلى أن "طبقات الألوان الممزوجة" ليست مثل "اللون يضرب باللون الأخضر ويفتقد الأحمر". يبرز الجزء الأخير بشكل أساسي الطبقات المنقطة باللون الأحمر حيث يتم إعادة إنشاء ذاكرة التخزين المؤقت (أداة جيدة لمعرفة ما إذا كنت لا تستخدم ذاكرة التخزين المؤقت بشكل صحيح). يجب ألا يكون للتنقيط إلى YES أي تأثير على التصيير الأولي للطبقات غير المعتمة.

هذه نقطة مهمة ، وعلينا أن نتوقف لحظة للتفكير. بغض النظر عما إذا كان يجب تعيين التنقيط على YES أم لا ، يجب أن يقوم إطار العمل بالتحقق من جميع طرق العرض ، والمزج (أو لا) بناءً على ما إذا كانت العروض الفرعية شفافة أو غير شفافة. في حين أنه قد يكون من المنطقي أن يكون UILabel الخاص بك غير معتم ، فقد يكون عديم الفائدة ويقضي على أدائك. على سبيل المثال ، من المحتمل أن تكون علامة UILabel الشفافة على خلفية بيضاء لا قيمة لها. لنجعله معتمًا:

ينتج عن هذا أداء أفضل ، ولكن تغير شكل التطبيق ومظهره. الآن ، نظرًا لأن ملصقاتنا وصورنا غير شفافة ، فقد تحرك الظل حول صورتنا. ربما لن يكون أي شخص مغرمًا بهذا التغيير ، وإذا أردنا الحفاظ على الشكل والمظهر الأصليين بأداء من الدرجة الأولى ، فلن نفقد الأمل.

لاستخراج بعض الإطارات الإضافية مع الحفاظ على المظهر الأصلي ، من المهم إعادة النظر في مرحلتين من مراحل الرسوم المتحركة الأساسية التي أهملناها حتى الآن.

  1. مستعد
  2. ارتكب

قد تبدو هذه خارج أيدينا تمامًا ، لكن هذا ليس صحيحًا تمامًا. نعلم أنه يجب فك ضغط الصورة ليتم تحميلها. يتغير وقت فك الضغط حسب تنسيق الصورة. بالنسبة إلى ملفات PNG ، يكون إلغاء الضغط أسرع بكثير من ملفات JPEG (على الرغم من أن التحميل أطول ، وهذا يعتمد على حجم الصورة أيضًا) ، لذلك كنا نوعا ما على المسار الصحيح لاستخدام PNG ، لكننا لا نفعل شيئًا حيال عملية إلغاء الضغط ، وإلغاء الضغط هذا يحدث في "نقطة الرسم"! هذا هو أسوأ مكان ممكن حيث يمكننا قتل الوقت - في الموضوع الرئيسي.

هناك طريقة لفرض الضغط. يمكننا تعيينه على خاصية صورة UIImageView على الفور. لكن هذا لا يزال يفك ضغط الصورة على الخيط الرئيسي. هل هناك أي طريقة أفضل؟

هناك واحد. ارسمها في CGContext ، حيث تحتاج الصورة إلى فك ضغطها قبل أن يمكن رسمها. يمكننا القيام بذلك (باستخدام وحدة المعالجة المركزية) في سلسلة خلفية ، ومنحها حدودًا حسب الضرورة بناءً على حجم عرض الصورة لدينا. سيؤدي ذلك إلى تحسين عملية رسم الصور الخاصة بنا عن طريق القيام بذلك خارج السلسلة الرئيسية ، وحفظنا من عمليات "التحضير" غير الضرورية في السلسلة الرئيسية.

بينما نحن فيه ، لماذا لا نضيف الظلال بينما نرسم الصورة؟ يمكننا بعد ذلك التقاط الصورة (وتخزينها مؤقتًا) كصورة واحدة ثابتة ومعتمة. رمز على النحو التالي:

 - (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; }

وأخيرًا:

 -(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; }

والنتائج هي:

بلغ متوسطنا الآن أكثر من 55 إطارًا في الثانية ، واستخدامنا للتصيير واستخدام القرميد يقارب نصف ما كان عليه في الأصل.

يتم إحتوائه

فقط في حال كنت تتساءل ما الذي يمكننا فعله أيضًا لإخراج عدد قليل من الإطارات في الثانية ، فلا مزيد من البحث. يستخدم UILabel WebKit HTML لعرض النص. يمكننا الذهاب مباشرة إلى CATextLayer وربما اللعب بالظلال هناك أيضًا.

ربما لاحظت في تطبيقنا أعلاه ، أننا لم نكن نقوم بتحميل الصورة في سلسلة رسائل في الخلفية ، وبدلاً من ذلك كنا نخزنها مؤقتًا. نظرًا لوجود 5 صور فقط ، فقد عمل هذا بسرعة كبيرة ولا يبدو أنه يؤثر على الأداء العام (خاصة وأن جميع الصور الخمس تم تحميلها على الشاشة قبل التمرير). ولكن قد ترغب في محاولة نقل هذا المنطق إلى سلسلة رسائل خلفية للحصول على أداء إضافي.

الضبط من أجل الكفاءة هو الفرق بين تطبيق عالمي وآخر للهواة. قد يكون تحسين الأداء ، خاصة عندما يتعلق الأمر بالرسوم المتحركة لنظام iOS ، مهمة شاقة. ولكن بمساعدة الآلات ، يمكن للمرء بسهولة تشخيص الاختناقات في أداء الرسوم المتحركة على نظام iOS.