РИТ++ 2017, App's Conf
Зал Найроби, 6 июня, 15:00
Тезисы:
http://appsconf.ru/2017/abstracts/2605.html
Доклад посвящен проблеме ускорения запуска приложений на мобильных устройствах под управлением iOS — как правильно замерить время запуска, оптимизировать системную и пользовательскую части, гарантировать сохранение результата в дальнейшем.
Рассказ основан на личном опыте оптимизации запуска Яндекс.Карт, описывает весь процесс от осознания проблемы до получения результата, подкреплен множеством технических подробностей и реальных примеров. Доклад является концептуальным, содержит конкретные предложения по разработке мобильных приложений с быстрым запуском и будет полезен iOS-разработчикам любого уровня.
7. Важное конкурентное преимущество
Влияет на retention, настроение пользователей, оценку в сторе
Максимальное время запуска - 20s, при превышении система
останавливает загрузку
Зачем сокращать время запуска?
4
8. Важное конкурентное преимущество
Влияет на retention, настроение пользователей, оценку в сторе
Максимальное время запуска - 20s, при превышении система
останавливает загрузку
› Актуально для слабых устройств
Зачем сокращать время запуска?
4
11. Множество приложений и фреймворков переходят на swift
Swift нельзя компилировать в статические библиотеки
Актуальность
5
12. Множество приложений и фреймворков переходят на swift
Swift нельзя компилировать в статические библиотеки
Динамические библиотеки грузятся долго
Актуальность
5
13. Множество приложений и фреймворков переходят на swift
Swift нельзя компилировать в статические библиотеки
Динамические библиотеки грузятся долго
WWDC 2016: Optimizing App Startup Time
Актуальность
5
31. 1. Динамические библиотеки
2. Исправление указателей, связывание
3. Objective-C контекст
4. +load, c++ globals
pre-main
14
32. Холодный запуск и pre-main
15
Холодный
Теплый
0 200 400 600 800
dylib loading time rebase/binding time
ObjC setup time initializer time
0.11s
0.73s
33. Полное время от нажатия на иконку до момента готовности к
использованию
Что замерять?
16
34. Полное время от нажатия на иконку до момента готовности к
использованию
› Замерять холодные и теплые запуски
Что замерять?
16
35. Полное время от нажатия на иконку до момента готовности к
использованию
› Замерять холодные и теплые запуски
› Учитывать pre-main
Что замерять?
16
59. Релизная конфигурация с оптимизациями
› отключены ассерты и функциональность для дебага
exit(0) после завершения загрузки, если
processInfo.environment["DYLD_PRINT_STATISTICS"] != nil
Сборка для тестов
25
60. На каждой итерации:
1. idevicediagnostics restart, дождаться загрузки
2. idevicedebug run - холодный запуск
3. idevicedebug run - теплый запуск
4. Обработать вывод, сохранить лог
Организация скрипта
26
62. На каждой итерации:
1. ideviceinstaller -i
2. idevicedebug run - холодный запуск
3. idevicedebug run - теплый запуск
4. Обработать вывод, сохранить лог
Организация скрипта
28
69. Бинарь приложения + 146 системных библиотек
Для проекта на swift дополнительно загружаются 9 библиотек из
бандла приложения, т.н. swift standard libraries
Список загружаемых библиотек
35
...
dyld: loaded: /private/var/containers/Bundle/Application/
AE358ED8-8FE3-4E90-88C8-FC602FDEA528/Empty.app/Frameworks/libswiftCore.dylib
dyld: loaded: /private/var/containers/Bundle/Application/
AE358ED8-8FE3-4E90-88C8-FC602FDEA528/Empty.app/Frameworks/libswiftUIKit.dylib
...
74. Загрузка системных библиотек оптимизирована
Библиотеки из бандла приложения грузятся долго
Любое приложения на swift будет грузиться на iPhone 5
минимум секунду при холодном запуске
Выводы
37
76. Уменьшить число загружаемых динамических библиотек
Уменьшить использование Objective-C:
› Использовать swift
Перенести код +load в +initialize
Избавиться от статических с++ переменных со сложными
конструкторами
На что теоретически можно повлиять?
39
77. Уменьшить число загружаемых динамических библиотек
Уменьшить размер бинарного файла
› вынести символы в динамические библиотеки
› грузить лениво через dlopen
На что практически можно повлиять?*
40
*в готовом проекте на swift
78. 41
...
use_frameworks!
target :OriginalApp do
pod 'AlamofireObjectMapper'
pod 'AlamofireImage'
pod 'FacebookLogin'
pod 'ObjectMapper'
pod 'PhoneNumberKit'
pod 'UIImageEffects'
pod 'pop'
pod 'KissXML'
pod 'MTDates'
pod 'Punycode-Cocoa'
pod 'YandexSpeechKit'
pod 'YandexMapKit'
end
Тестовый: Podfile
use_frameworks! из-за подов
на swift
много подов, собираемых из
исходных файлов
79. Тестовый: Функциональность
42
func handleDirectionsRequest(_ request: MKDirectionsRequest) {
//... использует MapKit из iOS SDK
}
func presentMap() {
//... использует YandexMapKit
}
func startSpeechRecognition() {
//... использует YandexSpeechKit и AVFoundation
}
90. import CoreLocation в *.swift
#import <CoreLocation/CoreLocation.h> в bridging header
Приводят к добавлению libswiftCoreLocation.dylib в
бандл приложения
swift standard libraries
51
92. Обернуть CoreLocation в Objective-C с таким же интерфейсом,
но другим префиксом
› CLWLocationManger, CLWLocation, CLWHeading и т.д.
53
Objective C обертки
93. Обернуть CoreLocation в Objective-C с таким же интерфейсом,
но другим префиксом
› CLWLocationManger, CLWLocation, CLWHeading и т.д.
53
Objective C обертки
Использовать в swift только эти обертки
› Тогда libswiftCoreLocation.dylib не добавляется
94. swift standard libraries
54
import AVFoundation
libswiftAVFoundation.dylib
libswiftCoreMedia.dylib
libswiftCoreAudio.dylib
import MapKit
libswiftMapKit.dylib
libswiftCoreLocation.dylib
import CoreLocation
99. В рассматриваемом примере карта и распознавание звука не
включаются на старте
› Можно конвертировать YandexSpeechKit и YandexMapKit в
динамические библиотеки и лениво загружать через dlopen
Неизбежно уменьшит rebase&bind, obj startup, initialization
Уменьшение размера бинарного файла
58
100. Создать отдельный таргет «Cocoa Touch Framework»
В созданный таргет
› Добавить pod со статической библиотекой в Podfile
› Прилинковать недостающие зависимости
static lib -> dynamic lib
59
101. Для основного таргета
› Убрать pod со статической библиотекой в Podfile
› Добавить новый framework в «embedded binaries»
› Перенести ресурсы
› Сделать Objective-C обертки для ленивой загрузки символов
библиотеки
static lib -> dynamic lib
60
122. Избыточное создание сущностей на старте
Избыточный UI
Инициализация, необязательная для показа стартового UI
На что обратить внимание в первую очередь
70
123. Инъекция зависимостей
71
class RootViewController {
init(searchFacade: SearchFacade, routingFacade: RoutingFacade) {
...
}
}
let searchFacade: SearchFacade = SearchFacadeImpl(...)
let routingFacade: RoutingFacade = RoutingFacadeImlp(...)
let rootVC = RootViewController(searchFacade: searchFacade,
routingFacade: routingFacade)
128. Оформить зависимости сущностей в протоколы
В конструкторе принимать реализацию протокола
В реализации использовать lazy var
Инъекция ленивых зависимостей на Swift
73
129. Инъекция ленивых зависимостей
74
protocol RootViewControllerDeps {
var searchFacade: SearchFacade { get }
var routingFacade: RoutingFacade { get }
}
class RootViewController {
init(deps: RootViewControllerDeps) {
...
}
}
130. Инъекция ленивых зависимостей
75
protocol RootViewControllerDeps {
var searchFacade: SearchFacade { get }
var routingFacade: RoutingFacade { get }
}
class RootViewControllerDepsImpl: RootViewControllerDeps {
lazy var searchFacade: SearchFacade = { return SearchFacadeImpl(...) }()
lazy var routingFacade: RoutingFacade = { return RoutingFacadeImpl(...) }()
}
132. На старте создаются только нужные сущности
Не используется рефлексия
resolve проверяется компилятором
Инъекция ленивых зависимостей
77
133. Сократить view-tree, создаваемое на старте
› ленивое создание контейнерных view и view-контроллеров
› ленивое создание view с опциональным контентом
Оптимизация UI
78
137. › Ленивая загрузка шрифтов
› Графику, генерируемую программно, отрисовать в ассеты
› Текст также отрисовать в ассеты
› Сложный autolayout перевести на фреймы
› …
Прочие оптимизации UI
80
138. Цель - как можно быстрее разблокировать UI для пользователя
Пока UI не загружен - выполнять только необходимые
действия, прочие отложить на 0.1-0.3с
Необязательная на старте работа
81
139. Отложили на 0.3с
› Синхронизацию закладок
› Отображение закладок на карте
› Загрузку конфигурации приложения
› Настройку аудиосессии
› ...
В Яндекс.Картах
82
143. Встроить вызов скрипта c автозапусками в CI
Собирать статистику запусков
Continuous integration
84
144. Встроить вызов скрипта c автозапусками в CI
Собирать статистику запусков
Обеспечить доступ к статистике
Continuous integration
84
145. Логировать создание зависимостей в composition root
Логировать список загружаемых динамических бибиотек через
objc_copyImageNames()
Сверять с эталонными логами
Идеи для CI
85
146. Информация о процессе через sysctl
86
#import <sys/sysctl.h>
int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid() };
struct kinfo_proc kp;
size_t len = sizeof(kp);
sysctl(mib, 4, &kp, &len, NULL, 0)
147. Время старта через sysctl
87
#import <sys/sysctl.h>
int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid() };
struct kinfo_proc kp;
size_t len = sizeof(kp);
sysctl(mib, 4, &kp, &len, NULL, 0)
struct timeval start_time = kp.kp_proc.p_starttime;
148. Замерять полное время запуска от нажатия на иконку
приложения через sysctl
Отправлять в системы аналитики
Проверять графики
88
Пользовательские запуски
150. Яндекс.Карты - холодный, 5s
90
До оптимизаций
После оптимизаций
0 625 1250 1875 2500
dylib loading time rebase/binding time
ObjC setup time initializer time
before didFinishLaunching time didFinishLaunching time
1.6s, -30%
2.3s
152. Яндекс.Карты - теплый, 5s
92
До оптимизаций
После оптимизаций
0 300 600 900 1200
dylib loading time rebase/binding time
ObjC setup time initializer time
before didFinishLaunching time didFinishLaunching time
0.75s, -37.5%
1.2s