Наш досвід прискорення додатків на iOS

    
 
Мене звуть Митя Куркін, я керую розробкою iOS месенджерів Mail.Ru Group. Сьогодні я розповім про наш досвід прискорення додатків на iOS. Висока швидкість роботи дуже важлива для 99% додатків. Особливо це актуально на мобільних платформах, де обчислювальні потужності і, відповідно, заряд акумулятора дуже обмежені. Тому кожен поважаючий себе розробник прагне оптимізувати роботу свого додатку з метою усунення різних затримок, з яких складається загальний час реакції.
 
 Вимірювання Перш ніж робити маніпуляції, потрібно зафіксувати поточний стан справ. Тобто заміряти, скільки часу зараз втрачається в проблемних місцях. Метод виміру повинен бути відтворюємо, інакше ці дані буде безглуздо порівнювати з подальшими досягненнями. Як заміряти? Ситуації можуть бути різні, але секундомір у нас завжди є. Правда, це найменш точний варіант.
 
Можна заміряти за допомогою профайлера. Якщо є характерні ділянки на графіку (спад або пік навантаження), то можна міряти по них. Такий варіант дає більш точний результат. Крім того, на графіку буде видно вплив додаткових факторів. Наприклад, якщо заміряти швидкість роботи всіх процесів пристрою, то можна з'ясувати, що роблять інші програми і чи позначається це на результат нашого виміру. Якщо в профайлера зачепитися нема за що, то можна міряти своїми логами. Це може дати ще більш точний результат, але для цього буде потрібно змінити додаток, що може деяким чином позначитися на його роботі. Також у цьому випадку не буде видно вплив додаткових факторів.
 
 Ідеальний результат Щоб розуміти, чи можна прискоритися в конкретній ситуації, бажано заздалегідь зрозуміти, яким може бути мінімальний час. Найбільш швидке рішення — це розглянути роботу аналогічної функції у додатку-конкурента. Це може дати орієнтир, наскільки швидко може виконуватися така операція. Необхідно оцінити, чи дозволяють вибрані технології досягти бажаної швидкості. Потрібно прибрати все, що тільки можливо, щоб отримати самий мінімум виконуваної функції:
 
     
  • відключити паралельно працюючі процеси;
  •  
  • замінити змінні на константи;
  •  
  • замість повноцінного завантаження екрану показати тільки заглушку;
  •  
  • залишити від мережевої операції тільки послідовність константних мережевих запитів;
  •  
  • можна навіть створити чисте додаток, що виконує тільки цю константну функцію.
  •  
Якщо в цьому випадку ми отримуємо бажану швидкість роботи, то далі можна потихеньку повертати відключені елементи і дивитися, як це впливає на продуктивність. Якщо ж навіть після виконання всіх описаних процедур результат незадовільний, значить потрібні радикальніші дії: зміна використовуваних бібліотек, зменшення обсягу трафіку за рахунок його якості, зміна використовуваного протоколу і т.д.
 
 Профилировщик При оптимізації просте читання коду може легко повести по хибному шляху. Можливо, вам попадеться деяка «важка» операція, яку насилу, але можна трохи оптимізувати. І ось, витративши багато часу, застосувавши новітні та наймодніші алгоритми, вам це вдається. Але при цьому до ідеалу як і раніше як до Китаю. А може бути, навіть стало гірше. Хоча насправді проблема може критися в найнесподіваніших місцях, абсолютно не викликають підозри. Десь можуть виконуватися абсолютно не потрібні дії, чи це просто помилка, що приводить до зависання. Тому спочатку потрібно заміряти, на що йде час. Тим більше що у нас є така можливість завдяки інструментарію від Apple.
  
 
 
Для прискорення роботи програми в першу чергу потрібно Time Profiler. Його інтерфейс досить зрозумілий: зверху графік навантаження на процесор, внизу дерево викликів, що показує який метод скільки з'їв. Є розбиття на потоки, фільтри, виділення фрагмента, різні сортування та ще багато всього.
 
Щоб найбільш ефективно працювати з цими даними, потрібно розуміти, як вони обчислюються. Візьмемо ось такий графік:
 
 
 
При великому збільшенні він виглядає так:
 
 
 
Профайлер вимірює витрата часу за рахунок періодичного опитування стану програми. Якщо під час такого виміру воно використовує процесор, то значить всі методи з стека викликів використовують процесорний час. По загальній сумі таких замірів ми отримуємо дерево викликів з вказівкою витраченого часу:
 
 
 
Інструменти дозволяють налаштовувати частоту таких вимірів:
 
 
 
Виходить, що чим частіше у вимірах відзначається використання процесора, тим вище рівень на вихідному графіку. Але тоді, якщо процесор не використовується, цей час не позначається на загальному результаті. Як тоді шукати ті випадки, коли додаток знаходиться в стані очікування якої-небудь події, — наприклад відповіді на http-запит? У цьому випадку може допомогти настройка «Record Waiting Threads». Тоді при замірах будуть записуватися і ті стани, коли процесор не використовується. У нижній таблиці автоматично включиться колонка з кількістю вимірів на функцію замість витраченого часу. Відображення цих колонок можна налаштовувати, але за замовчуванням показується або час, або кількість замірів.
 
Розглянемо такий приклад:
 
 
- (void)someMethod
{
     [self performSelector:@selector(nothing:) onThread:[self backThread] withObject:nil waitUntilDone:YES];
}

- (void)nothing:(id)object
{
     for (int i=0; i<10000000; ++i)
     {
          [NSString stringWithFormat:@"get%@", @"Some"];
     }
}

Замір додатки з запуском такого коду дасть приблизно такий результат:
 
 
 
На малюнку видно, що за часом головний потік займає 94 мс і 2,3%, а у вимірах (samples) — 9276 і 27%. Проте різниця не завжди може бути так помітна. Як шукати такі випадки в реальних додатках? Тут допомагає режим відображення графіка у вигляді потоків:
 
 
 
У цьому режимі видно, коли запускаються потоки, коли вони виконують якісь дії і коли «сплять». Окрім перегляду графіка у верхній частині можна також включити режим відображення списку замірів (Sample List) у нижній таблиці. Переглядаючи ділянки «сну» головного потоку, можна знайти винуватця підвисання інтерфейсу.
 
 Не зупиняйтеся на системних викликах Проводячи виміри, дуже легко можна впертися в системні виклики. Виходить, що весь час йде на роботу системного коду. Що тут поробиш? Насправді, головне не зупинятися на цьому. Поки є можливість, потрібно заглиблюватися в ці виклики. Якщо покопатися, то може запросто виявитися колбек або делегат, який викликає ваш код, і відчутний витрата часу виявляється саме через нього.
 
 Вимикайте Отже, підозрюваний знайдений. Перш ніж переробляти, потрібно перевірити, як працюватиме додаток зовсім без нього.
 
Буває так, що потенційних винуватців гальмування багато, і заміряти все це профілювальником проблематично. Наприклад, проблема добре відтворюється тільки у деяких користувачів і не завжди, а тільки в певній ситуації. Щоб швидко зрозуміти, чи у правильному напрямку ми рухаємося, чи ті місця заміряємо і оптимізуємо, дуже добре допомагає відключення цих модулів.
 
Якщо вже все вимкнено, а до ідеалу далеко, то потрібно спробувати рухатися з іншого боку. Створити пусте додаток і нарощувати його функціонал.
 
 Враховуйте технічні особливості пристроїв Технічні характеристики пристроїв змінюються, і це також варто враховувати. Наприклад, починаючи з iPhone 4S почали використовувати багатоядерні процесори. Тому там використання багатопоточності більш ефективно за рахунок використання декількох ядер. Однак на одноядерному процесорі це може уповільнити отримання кінцевого результату, оскільки ми все одно можемо використовувати тільки одне ядро, але при цьому додатково витрачаємо ресурси на перемикання контексту потоку.
 
 Будьте обережні при підключенні великих фреймворків Чим більше і потужніше механізм ви підключаєте, тим більше він бере в свої руки. Тим менше ви контролюєте ситуацію. І, відповідно, додаток стає менш гнучким. У нашому випадку ми міцно сіли на CoreData. Прекрасна технологія. Всіляка підтримка міграцій, FetchResultController, кешування — дуже спокусливо. Але візьмемо запуск програми. Дли ініціалізації стека CoreData потрібно як мінімум завантажити базу і завантажити модель. Якщо використовувати sqlite без CoreData — завантаження моделі не потрібна. У нашому випадку модель містить 26 сутностей. Її завантаження займає відчутний час, особливо на старих пристроях, де швидкість запуску відчувається найбільш гостро.
 
Наші додатки активно розвиваються, тому постійно є потреба в додаванні сутностей в базу. Завдяки зручному механізму міграції це не викликає проблеми. Але от їх уже майже 40. У першу чергу це сильно позначається на розмірі додатка. У сумі все міграції додають близько 30%. Крім того міграції працюють послідовно. Значить чим їх більше — тим довше відбувається міграція. І це знову позначається на швидкості запуску.
 
Ми також зіткнулися з проблемою видалення. При нашій моделі і достатньою великою базі видалення, що зачіпає всі сутності, займало близько 10 хвилин. Включивши чарівну опцію налагодження CoreData SQLDebug, ми побачили величезну кількість SELECT-ів, UPDATE-ів і трохи DELETE-ів. Основна проблема тут в тому, що в NSManagedObjectContact немає методу типу deleteObjects. Тобто об'єкти можна видаляти тільки по одному, хоча SQL сам по собі вміє видаляти через DELETE… WHERE someValue IN… Крім того для видалення кожного об'єкта робиться SELECT його ключа і тільки потім видалення. Аналогічно відбувається видалення залежних об'єктів.
 
У нашій ситуації становище ускладнюється ще й тим, що користувачі мобільних пристроїв як правило не чекають такого величезного терміну і «вбивають» додаток. У результаті виходить бита база.
 
 Висновки Як бачите, шляхів по оптимізації швидкості роботи мобільних додатків досить багато. Але, обклавшись цифрами і графіками, потрібно не відриватися від реальності. Бажано зберігати працездатність програми, щоб ефект від оптимізації можна було помацати в бойових умовах. На жаль, найчастіше розробники або приділяють оптимізації недостатньо уваги, або надто захоплюються цим заняттям. Головне — пам'ятати, що оптимізація повинна давати відчутні користувачем результати. Оптимізація заради самої оптимізації, коли ефект виходить гомеопатичним, є марною тратою часу і сил. Всього має бути в міру.
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.