iPhone OS 1 и iPhone 1 появились в июне 2007, когда разработчики старались написать как можно больше программного кода и по-быстрее выпустить как можно больше приложений для широкой аудитории. Развитие аппаратного обеспечения, операционных систем, сетей привело также к появлению новых пользовательских GUI и инженерных дизайнерских решений, которые постепенно становились зрелыми, функциональными, устойчивыми и производительными. Но что значит производительность iOS приложения для 2007 года и для 2017 года?
Производительность мобильных приложений
10 лет назад скорость работы мобильных приложений являлась второстепенным фактором. В какой-то степени я согласен, что важнее было добиться правильной функциональности приложения, корректности выполнения поставленных перед ним задач. И если думать о производительности в самом начале жизненного цикла разработки, то получим ненужную преждевременную оптимизацию. Однако медленные, неоптимизированные iOS приложения раздражают пользователей и доставляют им головную боль. В этой статье я кратко расскажу, как писать оптимальный программный код с инженерной точки зрения и на что обратить внимание при оптимизации мобильных приложений под iOS.
Взгляните на следующую статистику
- 79% пользователей повторно открывает приложение 1 или 2 раза, если оно не работает в первый раз
- 25% пользователей отказывается от приложения, если оно не загружается в течение 3 секунд
- 31% пользователей расскажут своим друзьям о своем неудачном опыте использования приложения
Данные цифры еще раз подчеркивают важность оптимизации вашего мобильного приложения для повышения производительности. Пользователи реагируют на скорость работы софта также остро, как и на функциональность. Если ваше приложение будет функциональным, но тормозным — оно никого не заинтересует. Пользователю должно быть комфортно пользоваться приложением на протяжении всего взаимодействия с ним, на любых объемах данных и при любом состоянии сети, а не только во время запуска. В AppStore существует множество приложений для решения конкретной задачи, но пользователи будут пользоваться тем приложением, которое не имеет глюков и выделяется среди других своей производительностью.
Производительность мобильного приложения зависит от многих факторов: потребления памяти, эффективности использования пропускной способности сети, отзывчивости пользовательского интерфейса. С технической точки зрения, производительность, строго говоря, весьма смутное понятие. Допустим, кто-то идентифицирует приложение как высокопроизводительное. Использует ли это приложение меньше памяти? Экономит ли оно деньги и трафик при работе через сеть? Обеспечивает ли плавную работу? Подобные вопросы могут быть бесконечными и многогранными. Производительность мобильного приложения может быть связана с двумя аспектами: показателей производительности (то, что мы хотим измерять и мониторить) и непосредственно измерении (сбора данных).
Показатели производительности iOS приложения
Каждый из показателей производительности мобильного iOS приложения может иметь несколько атрибутов-параметров для удобства классификации.
Память
Показатель «Память» — это минимальное количество RAM, требуемое для запуска приложения, а также среднее и максимальное количество RAM, используемые приложением в процессе работы. Минимальный объем памяти жестко привязан к аппаратной начинке устройства, а использование приложением памяти выше среднего или по максимуму приведет к вылету фоновых процессов. Также необходимо убедиться, не происходит ли утечки памяти. Постепенное увеличение потребления памяти в процессе работы повышает вероятность крэша приложения из-за out-of-memory исключений.
Потребляемая мощность
Это чрезвычайно важный показатель, который нужно учитывать при написании оптимизированного кода. Ваши структуры данных и алгоритмы должны быть эффективными с точки зрения времени выполнения и ресурсов CPU. Если ваше приложение сажает батарею, его никто не оценит. Потребляемая мощность кроме использования CPU еще включает в себя эффективное использование аппаратных средств. Поэтому важно при попытках сокращения потребляемой мощности не навредить и убедиться, что со стороны пользователя нет ухудшений в работе.
Время инициализации
Приложение при запуске должно выполнять довольно простые задачи. Время, необходимое для выполнения этих задач является временем инициализации приложения. Правильный баланс зависит от потребностей приложения. Как вариант, можно отложить создание объекта и инициализацию до первого использования приложения (то есть, пока объект не требуется) — это называется отложенной инициализацией. При этом пользователь не должен ожидать каждый раз, когда выполняется любая последующая задача. В следующем списке приведены некоторые действия, которые вы можете выполнить во время инициализации вашего приложения, в произвольном порядке:
- убедитесь, что приложение в данный момент запущено в первый раз
- проверьте, что пользователь авторизовался
- если пользователь авторизовался, то загружать предыдущее состояние, если это приемлемо
- подключитесь к серверу для получения последних изменений
- проверьте, что приложение запустилось с deep link (глубинное связывание). Если да, то загрузить нужный UI и состояние для deep link
- проверьте, есть ли ожидающие задачи, которые были созданы с момента последнего запуска приложения. Возобновите их работу в случае необходимости.
- проинициализируйте объект и пул потоков, которые вы хотите использовать в дальнейшем
- проинициализируйте зависимости (например, объектно-реляционный маппинг, систему крэш-репортов и кэш-памяти)
Данный список может расти быстро, и трудно решить, что во время запуска отложить, а что запустить до следующих нескольких миллисекунд.
Скорость выполнения
После того, как пользователь открывает приложение, он хочет приступить к работе с ним как можно быстрее. Любая необходимая обработка должна выполняться как можно меньше времени, насколько это возможно. Для примера возьмем приложение фотографий. Интерактивный просмотр идеально подходит для простых эффектов, таких как изменение яркости или контрастности, где обработка должна произойти в течение миллисекунд.
Скорость отклика
Ваше приложение должно быстро отвечать на действия пользователя. Скорость отклика — это результат всех оптимизаций и компромиссов, которые вы сделали. В AppStore могут быть сотни подобных приложений, но пользователь выберет приложение с лучшей скоростью отклика.
Локальное хранилище
Каждое приложение, которое хранит данные на сервере и/или обновляет данные из внешнего источника, должно также иметь возможность локального сохранения данных для автономного просмотра. Например, почтовое приложение в оффлайне хотя бы должно показать загруженный список писем. Новостное приложение должно показать обновленные новости, а также индикатор новых и прочитанных новостей. Загрузка из локального хранилища и синхронизация данных должны быть быстрыми и безболезненными. Поэтому не только выбираемые данные должны кэшироваться локально, но и структуры данных, выбираемые из опций, и частота синхронизации.
Также нужна функция по очистке локального хранилища, что сейчас очень редко встречается на рынке мобильных приложений. Это действительно важно, так как локальное хранилище может занимать сотни мегабайт. Поэтому пользователи для освобождения места удаляют приложение целиком, что является довольно плохим экспириенсом. На следующем рисунке видно, что в локальном хранилище занимает 12 ГБ пространства, оставляя свободными только 950 МБ.
Пользователю обязательно должна быть доступна функция по очистке локального кэша, иначе если он включит резервное копирование на iCloud, то локальное хранилище может сразу переполнить квоту хранения в облаке.
Функциональная совместимость
Пользователи могут использовать множество приложений, чтобы выполнить одну задачу, которая требует функциональной совместимости каждого. Например, фотоальбом лучше просматривать в приложении слайд-шоу, но для его редактирования может потребоваться другое приложение. Приложение для просмотра должно быть в состоянии отправить фотографию приложению-редактору и получить отредактированную фотографию.
iOS предоставляет многочисленные возможности для функциональной совместимости и обмена данными через приложения, например, UIActivityViewController, deep linking и фреймворк MultipeerConnectivity.
Определение хорошей структуры URL для deep linking так же важно как и написание хорошего кода, понятного каждому. Аналогично и при обмене данными: важно идентифицировать контент для совместного использования, а также позаботиться о проблемах безопасности, которые могут возникнуть при обработке содержания из недоверенного источника.
И реально плохой экспириенс, если бы Ваше приложение тратит много времени только для подготовки данных обмена с соседним устройством.
Состояние сети
Мобильные устройства используются в сетях разного качества. Ваше приложение должно работать во всех следующих сценариях:
- Высокая пропускная способность и стабильная сеть
- Низкая пропускная способность, но стабильная сеть
- Высокая пропускная способность, но плохая сеть
- Низкая пропускная способность и плохая сеть
- Отсутствие сети
Пользователю можно показать прогрессбар или сообщение об ошибке. Плохо, если юзера держат в неведении и блокируется какой-то функционал, приложение висит в состоянии неопределенности или вообще вылетает.
На следующих скриншотах как раз показаны варианты отображения сообщений и прогрессбара. Приложение TuneIn показывает статус буферизации потоковой передачи данных и сообщает пользователю примерное время ожидания, прежде чем музыка снова начнет играть. Другие приложения, например, MoneyControl и Bank of America, показывают бесконечный прогрессбар — это характерно для непотоковых приложений.
Пропускная способность сети
Люди используют свои мобильные устройства сетях различного качества со скоростями в пределах от сотен килобит в секунду к десяткам мегабит в секунду. По сути, оптимальное использование пропускной способности сети — еще одно требование, которое определяет качество вашего приложения. Но если вы разрабатывали свое приложение в расчете на низкую пропускную способность сети, а теперь выполняете его в условиях высокой пропускной способности, то могут возникнуть неприятности.
Приблизительно в 2010 моя команда Lipasite и я разрабатывали приложение в Индии. В условиях низкой пропускной способности произошла бы локальная инициализация приложения задолго до первых ответов от сервера, и мы настраивали приложение для тех условий. Однако приложение фокусировалось на южнокорейском рынке, и когда мы протестировали его там, результаты кардинально отличались. Ни одна из наших оптимизаций не работала, и мы должны были переписать большой блок кода, который, возможно, привел бы конфликтной ситуации на уровне данных и ресурсов.
Расчет высокой производительности не всегда приводит к оптимизации, но может привести к компромиссам.
Обновление данных
Даже если у вас нет возможности оффлайн просмотра, Вы все равно можете периодически обмениваться с данными от сервера. Частота обновлений и переданный объем данных будут влиять на полное потребление данных. Если количество переданных байтов высоко, пользователь исчерпает свой тарифный план очень быстро. Вы просто потеряли пользователя.
В iOS 6.x и ниже приложение в фоновом режиме не может обновлять данные. В iOS 7 и выше приложение может использовать фоновое обновление для периодических обновлений. Для приложений живого чата постоянное HTTP-соединение или прямое соединение по протоколу TCP могут быть более полезными.
Поддержка многопользовательского режима
Многопользовательстий режим в вашем приложении имеет несколько сценариев применения. Представим семью из нескольких человек, где каждый пользуется одним мобильным устройством. Или, например, один пользователь может иметь несколько учетных записей на одно приложение. Например, два брата могли бы совместно использовать один iPad для игр. Как другой пример, семья может хотеть сконфигурировать одно устройство, чтобы проверить электронные письма каждого человека во время отпуска, чтобы минимизировать затраты в роуминге, особенно во время международного путешествия. Точно так же у одного человека могут быть разные почтовые ящики, которые будут сконфигурированы и настроены.
Хотите ли вы иметь многопользовательский режим, будет зависеть от Вашего продукта. Но если Вы действительно решите иметь эту функцию, удостоверьтесь, что следовали этим инструкциям:
- Добавление нового пользователя должно быть эффективным
- Обновления через пользователей должны быть эффективными
- Переключение между пользователями должно быть эффективным
- Границы пользовательских данных должны быть аккуратными и без каких-либо багов
Настройка системы отслеживания ошибок
Все системы отчетов об ошибках собирают логи отладки для последующего анализа. Существует огромное количество таких систем, но я выбрал Flurry, так как его довольно легко интегрировать в проект, используя один SDK. Для этого нужно создать учетную запись на https://login.flurry.com, получить API-ключ, скачать и установить Flurry SDK. Следующий код показывает подключение Flurry SDK к проекту:
1 2 3 4 5 6 7 |
#import "Flurry.h" - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [Flurry setCrashReportingEnabled:YES]; [Flurry startSession:@"API_KEY"]; } |
В последней строке нужно заменить API_KEY на полученный (его можно найти в консоли аккаунта Flurry).
Система ослеживания ошибок (CRS — Crash Reporting System) создает глобальный обработчик исключений (global exception handler), используя метод NSSetUncaughtExceptionHandler. Если вы использовали до этого свой кастомный обработчик, он будет потерян.
Если все-таки нужно сохранить свой кастомный обработчик исключений, поместите его после инициализации CRS. Вы можете получить обработчик CRS, используя метод NSGetUncaughtExceptionHandler.
Инструментирование приложения
Инструментирование — важный шаг в понимании пользовательского поведения, и что еще более важно — в идентификации критических уязвимостей мобильного приложения. Добавление дополнительного кода для записи ключевых метрик — хороший шаг для анализа производительности, помогающий абстрагировать и инкапсулировать любые зависимости, что позволит переключаться на последней минуте или даже работать с множеством систем одновременно, прежде чем принять окончательное решение. Это особенно полезно при большом количестве опций, и когда вы еще находитесь в стадии оценки.
Добавим класс под названием HPInstrumentation чтобы инкапсулировать инструментарий. Будем писать лог в консоль, используя NSLog, и посылать детальные данные на сервер Flurry.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//HPInstrumentation.h @interface HPInstrumentation : NSObject +(void)logEvent:(NSString *)name; +(void)logEvent:(NSString *)name withParameters:(NSDictionary *)parameters; @end //HPInstrumentation.m @implementation HPInstrumentation +(void)logEvent:(NSString *)name { NSLog(@"%@", name); [Flurry logEvent:name]; } +(void)logEvent:(NSString *)name withParameters:(NSDictionary *)parameters { NSLog(@"%@ -> %@", name, params); [Flurry logEvent:name withParameters:parameters]; } @end |
Инструментирование жизненного цикла мобильного приложения будет состоять из 3 основных этапов:
- каждый раз, когда приложение активируется и приходит на передний план, запускается обработчик DidBecomeActive;
- каждый раз, когда приложение переходит в фоновый режим, вызывается DidEnterBackground;
- и если приложение получает предупреждение о нехватке памяти, вызывается обработчик DidRecieveMemoryWarning.
Добавим также отдельную кнопку на первый HPFirstViewController, которая при нажатии специально вызовет разрушительный сбой приложения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (void)applicationDidBecomeActive:(UIApplication *)application { [HPInstrumentation logEvent:@"App Activated"]; } - (void)applicationDidEnterBackground:(UIApplication *)application { [HPInstrumentation logEvent:@"App Backgrounded"]; } - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { [HPInstrumentation logEvent:@"App Memory Warning"]; } |
Важно понимать, что инструментирование — это не альтернатива журналированию. Журналирование может быть гораздо детальнее. При инструментировании нужно свести детализацию к минимуму, так как используются ресурсы сети при создании отчетов и обращении к серверу. Кроме этого, важно инструментировать значимые для вас и вашей команды разработчиков события, а не все подряд. Существует тонкая грань между инструментированием и сверхинструментированием. Начните с небольшого количества отчетов и со временем постепенно увеличивайте их. Теперь давайте добавим UI контрол для генерации отчета об ошибках. На рисунке ниже показана кнопка для искусственного создания разрушительного крэша приложения, а далее идет код, где событие TouchUpInside связывается с методом crashButtonWasClicked.
1 2 3 4 |
- (IBAction)crashButtonWasClicked:(id)sender { [NSException raise:@"Crash Button Was Clicked" format:@""]; } |
Теперь повзаимодействуем с приложением, чтобы сгенерировать некоторые события.
Для этого нужно:
- Установить и запустить приложение.
- Перевести приложение в фон.
- Перевести приложение на передний план.
- Повторить шаги 2 и 3 несколько раз.
- Тапнуть кнопку Generate Crash. Это вызовет крэш приложения.
- Запустить приложение снова. Вот теперь крэш-репорт будет отправлен на сервер.
Анализ логов и крэш-репортов
Отправка и обработка первой пачки событий и крэш-репортов на сервер может занять некоторое время. После этого данные появятся в дашборде Flurry.
На следующем скрине показываются сеансы пользователя — т.е. сколько пользователей открыло приложение, по крайней мере, один раз в день. Подсчет многократных запусков учитывается в рамках одного сеанса и зависит от времени, прошедшего между запусками.

Следующий отчет полезнее, так как в нем можно видеть, какие элементы приложения использовались чаще всего по сравнению с другими.

Также можно скачать крэш-лог и посмотреть, что в нем записано.

В этой статье я рассказал про факторы, которые оказывают влияние на производительность iOS приложения в 2017 году, метрики и атрибуты, отражающиеся на эффективности использования ресурсов процессора, батареи, аппаратных устройств. Немного коснулся темы инструментирования и журналирования приложения, показав, как можно разработать генерацию событий во время работы с приложением и отправку их на сервер. Тема производительности мобильных iOS и написания оптимизированного кода достаточно обширна, впереди будет много интересного.
© Александр Худоев