Производительность
Профилирование через Instruments, время запуска и hitch'и, стоимость декодирования изображений, размер бинарника, санитайзеры и отсечение регрессий в CI.
13 вопросов
JuniorТеорияОчень частоЧто такое Instruments и как с его помощью профилировать работающее приложение?
Что такое Instruments и как с его помощью профилировать работающее приложение?
Instruments — набор профилировщиков Xcode. Вы запускаете сборку через Product ▸ Profile, берёте шаблон вроде Time Profiler или Allocations, и он записывает процесс. Вы читаете реальные CPU, память и тайминги по живым сэмплам.
Типичные ошибки
- ✗Думают, что Instruments анализирует исходник статически, а не сэмплирует работающий процесс
- ✗Путают Instruments с memory graph отладчика или с символикатором crash log
- ✗Считают, что один шаблон измеряет всё, вместо выбора отдельного под каждую задачу
Уточняющие вопросы
- →Почему профилировать нужно Release-сборку, а не Debug-сборку?
- →Чем шаблон Time Profiler отличается от шаблона Allocations?
MiddleПроизводительностьОчень частоTable view роняет кадры при прокрутке — в каком порядке вы проверяете вероятные причины?
Table view роняет кадры при прокрутке — в каком порядке вы проверяете вероятные причины?
Сначала дешёвые причины. Проверьте, что ячейки берутся из dequeue и переиспользуются; уберите декодирование и работу с диском или сетью с главного потока; кэшируйте высоты; избегайте offscreen-проходов; затем профилируйте Time Profiler.
Типичные ошибки
- ✗Винят GPU или устройство вместо проверки переиспользования и работы на главном потоке
- ✗Декодируют картинки или считают layout ячеек на главном потоке во время скролла
- ✗Пропускают профилирование и гадают о причине вместо того, чтобы её измерить
Уточняющие вопросы
- →Почему декодирование JPEG на главном потоке дёргает прокрутку?
- →Какие offscreen-рендеринг эффекты чаще всего стоят кадров внутри ячейки?
MiddleПроизводительностьЧастоПочему JPEG 4000×3000 стоит ~48 МБ RAM после показа и как это чинит даунсэмплинг при декодировании?
Почему JPEG 4000×3000 стоит ~48 МБ RAM после показа и как это чинит даунсэмплинг при декодировании?
JPEG сжат на диске, но показ декодирует его в несжатый bitmap width × height × 4 байта — 4000 × 3000 × 4 ≈ 48 МБ — при любом файле. Даунсэмплинг при декодировании через ImageIO даёт сразу рисуемый размер, и RAM соответствует view.
Типичные ошибки
- ✗Считают, что RAM картинки равен её сжатому размеру файла на диске
- ✗Уменьшают уже после декодирования полного bitmap вместо даунсэмплинга при декодировании
- ✗Думают, что маленький frame у image view ограничивает размер декодированного bitmap
Уточняющие вопросы
- →Почему уменьшение UIImage после загрузки не снижает пиковую память?
- →Как ImageIO декодирует сразу в миниатюру без полного bitmap?
MiddleПроизводительностьЧастоВ запуске приложения что такое pre-main против post-main и от чего холодный старт медленный?
В запуске приложения что такое pre-main против post-main и от чего холодный старт медленный?
Pre-main идёт до main() — загрузка и линковка библиотек через dyld, binding, rebasing, настройка рантайма и статические инициализаторы. Post-main — ваш код до первого кадра, вроде didFinishLaunching. Обе фазы меряете инструментом App Launch; старт тормозят лишние фреймворки.
Типичные ошибки
- ✗Меняют местами определения pre-main и post-main времени запуска
- ✗Считают dyld, линковку и статические инициализаторы бесплатными при запуске
- ✗Думают, что больше динамических фреймворков ускоряют холодный старт, а не замедляют
Уточняющие вопросы
- →Почему множество динамических фреймворков удлиняет именно pre-main время?
- →Какая синхронная работа в
didFinishLaunchingчаще всего задерживает первый кадр?
MiddleДебаггингЧастоДокажите, что главный поток заблокирован при 400-мс зависании UI, и чем
Докажите, что главный поток заблокирован при 400-мс зависании UI, и чем
Подтвердите инструментом, а не догадкой — Main Thread Checker или hang report помечает столл, а стек Thread 0 в Time Profiler показывает ~396 мс в одном синхронном вызове. Здесь DataStore.loadAll() парсит JSON на главном потоке, заблокирован на диске.
Типичные ошибки
- ✗Гадают о причине вместо подтверждения столла инструментом
- ✗Считают заблокированным фоновый поток, когда застрял стек главного
- ✗Винят тяжёлый рендеринг или утечку вместо синхронного вызова на главном потоке
Уточняющие вопросы
- →Какой сигнал в Time Profiler говорит, что главный поток стоял, а не работал?
- →Почему лист
__read_nocancelдоказывает, что блокировка — это дисковый I/O?
MiddleТеорияЧастоЧто ловит каждый из Thread Sanitizer, Address Sanitizer и Main Thread Checker и почему их не оставляют в релизе?
Что ловит каждый из Thread Sanitizer, Address Sanitizer и Main Thread Checker и почему их не оставляют в релизе?
Thread Sanitizer (TSan) находит гонки данных, Address Sanitizer (ASan) ловит ошибки памяти вроде переполнений, а Main Thread Checker помечает вызовы UIKit не с главного потока. Каждый сильно инструментирует бинарник, поэтому их берут в debug и CI, но не в Release.
Типичные ошибки
- ✗Думают, что санитайзер можно оставить включённым в выпускаемой Release-сборке
- ✗Путают, что ловит каждый — гонки против ошибок памяти против вызовов не с главного потока
- ✗Считают санитайзеры бесплатными compile-time проверками без рантайм-стоимости
Уточняющие вопросы
- →Почему Thread Sanitizer требует специально инструментированной сборки, чтобы увидеть гонку данных?
- →Какой баг ловит Address Sanitizer, которого не покажет обычный crash log?
MiddleПроизводительностьЧастоПрочитайте call tree в Instruments Time Profiler и найдите hot path
Прочитайте call tree в Instruments Time Profiler и найдите hot path
Инвертируйте call tree, чтобы тяжёлые по self time листья поднялись наверх, спрячьте системные библиотеки, и сфокусируйтесь на главном потоке. У start большой total, но ~0 self. Hot path — это -[FeedCell buildAttributedText:] с ~62% self.
Типичные ошибки
- ✗Читают верхний фрейм по total time вместо инверсии ради self time
- ✗Оставляют системные библиотеки видимыми и винят фреймворковый фрейм
- ✗Принимают большой total time за стоимость вместо большого self time
Уточняющие вопросы
- →Почему у
startбольшой total time, но почти нулевой self time? - →Когда системные библиотеки стоит оставить видимыми, а не прятать?
SeniorПроизводительностьЧастоБинарник приложения 180 МБ — от чего он большой, как измерить это и что реально его режет?
Бинарник приложения 180 МБ — от чего он большой, как измерить это и что реально его режет?
Размер идёт в основном от ассетов, исполняемого __TEXT и встроенных фреймворков. Мерьте по отчёту размера App Store Connect и linkmap, а не по сжатому .ipa. Здесь 96 МБ — картинки, так что сжимайте ассеты и берите on-demand resources, затем dead-strip кода.
Типичные ошибки
- ✗Думают, что размер задают строки исходника, а не ассеты и фреймворки
- ✗Мерят сжатый .ipa вместо отчёта размера в App Store Connect
- ✗Добавляют динамические фреймворки или bitcode, ожидая уменьшения
Уточняющие вопросы
- →Почему размер загрузки в App Store отличается от
.ipaна диске? - →Как dead-code stripping решает, какие символы удалить?
MiddleДебаггингИногдаПамять растёт, но инструмент Leaks ничего не находит — что такое abandoned object graph?
Память растёт, но инструмент Leaks ничего не находит — что такое abandoned object graph?
Abandoned object graph — это достижимая память под strong-ссылкой, которая больше не используется, поэтому Leaks, сообщающий лишь о недостижимых блоках, её не видит. Генерационный анализ Allocations показывает ~12 МБ persistent на каждый экран — это удержанный граф.
Открыть задачу →Типичные ошибки
- ✗Считают любой рост памяти без утечки фрагментацией, которую не починить
- ✗Верят, что инструмент Leaks ловит любой рост памяти
- ✗Путают заброшенный граф с retain cycle или проблемой autorelease pool
Уточняющие вопросы
- →Почему инструмент Leaks сообщает ноль, пока память всё ещё растёт?
- →Как метка поколения на каждый экран изолирует удержанные объекты?
MiddleПроизводительностьИногдаКак инструментировать код через signpost-API os_signpost, чтобы свой интервал появился в Instruments?
Как инструментировать код через signpost-API os_signpost, чтобы свой интервал появился в Instruments?
Создайте OSSignposter на категории .pointsOfInterest и оберните работу в пару begin/end — beginInterval до неё, endInterval с возвращённым state после. Именованный интервал тогда виден в треке Points of Interest.
Типичные ошибки
- ✗Меряют через Date и print вместо signpost-API
- ✗Пишут одну строку лога и ждут, что она станет интервалом
- ✗Считают, что в Points of Interest попадают лишь события фреймворков Apple
Уточняющие вопросы
- →Почему конечный signpost должен использовать state, возвращённый вызовом begin?
- →Чем трек Points of Interest отличается от инструмента Time Profiler?
SeniorПроизводительностьИногдаПереход на 120-Гц дисплей ProMotion вскрыл hitch'и — каков бюджет кадра и что их чинит?
Переход на 120-Гц дисплей ProMotion вскрыл hitch'и — каков бюджет кадра и что их чинит?
При 120 Гц бюджет кадра ~8.3 мс — половина от 16.7 мс при 60 Гц — так что прежняя работа переполняет и даёт hitch. Animation Hitches сообщает hitch-time ratio и долгий hitch, здесь 41 мс от декодирования в layoutSubviews. Уберите декодирование с главного.
Типичные ошибки
- ✗Думают, что бо́льшая частота даёт больше времени на кадр, а не меньше
- ✗Читают инструмент как средний FPS вместо hitch-time ratio
- ✗Считают, что бюджет 16.7 мс не меняется при 120 Гц
Уточняющие вопросы
- →Почему тот же код даёт hitch при 120 Гц, но не при 60 Гц?
- →Что hitch-time ratio измеряет такого, что упускает сырой FPS?
SeniorДебаггингИногдаДжанк скролла только на старых устройствах и только после 5 минут — воспроизведите, измерьте, найдите причину
Джанк скролла только на старых устройствах и только после 5 минут — воспроизведите, измерьте, найдите причину
Воспроизводите на старом устройстве и гоняйте пять минут — важно накопленное состояние, а не холодный запуск. Мерьте Time Profiler и Allocations вместе. Память растёт до 540 МБ, наверху ImageCache — безграничный кэш даёт давление. Ограничьте кэш.
Типичные ошибки
- ✗Профилируют холодный запуск на новом устройстве вместо состарившегося состояния на старом
- ✗Отмахиваются от зависящего от времени джанка как невоспроизводимого
- ✗Игнорируют рост памяти как причину джанка скролла
Уточняющие вопросы
- →Почему давление памяти проявляется как джанк скролла, а не сперва как крах?
- →Как подтвердить, что рост — это кэш картинок, а не утечка?
SeniorДизайнРедкоВаше iOS-приложение постоянно регрессирует по производительности между релизами — тяжёлый view controller удвоил время холодного старта, небрежная правка ячейки вдвое ухудшила плавность скролла, а утёкший кэш вывел память за предел OS memory-kill (jetsam) на старых устройствах — и каждое замечали пользователи уже после выпуска. Спроектируйте гейт регрессий производительности в CI, который блокирует merge, когда pull request измеримо всё ухудшает. Решите, что мерить (например холодное и тёплое время запуска, scroll hitch ratio и пиковую или установившуюся память), как собирать эти числа детерминированно на фиксированном устройстве или симуляторе, чтобы замеры были сопоставимы от прогона к прогону, откуда берутся пороги и baseline, и как гейт сообщает автору об ошибке, не будучи столь флаки, что команда его игнорирует. Объясните, как держать baseline честным по мере роста приложения и как отличать настоящую регрессию от шума измерений.
Ваше iOS-приложение постоянно регрессирует по производительности между релизами — тяжёлый view controller удвоил время холодного старта, небрежная правка ячейки вдвое ухудшила плавность скролла, а утёкший кэш вывел память за предел OS memory-kill (jetsam) на старых устройствах — и каждое замечали пользователи уже после выпуска. Спроектируйте гейт регрессий производительности в CI, который блокирует merge, когда pull request измеримо всё ухудшает. Решите, что мерить (например холодное и тёплое время запуска, scroll hitch ratio и пиковую или установившуюся память), как собирать эти числа детерминированно на фиксированном устройстве или симуляторе, чтобы замеры были сопоставимы от прогона к прогону, откуда берутся пороги и baseline, и как гейт сообщает автору об ошибке, не будучи столь флаки, что команда его игнорирует. Объясните, как держать baseline честным по мере роста приложения и как отличать настоящую регрессию от шума измерений.
Мерьте набор метрик — время запуска, hitch ratio, пиковую память — на фиксированном устройстве, усредняя прогоны. Держите baseline из main; PR падает, превысив baseline плюс запас на шум, лишь на устойчивых регрессиях.
Типичные ошибки
- ✗Мерят на общем CI-железе, из-за чего числа несопоставимы между прогонами
- ✗Берут один абсолютный порог вместо baseline плюс запаса на шум
- ✗Роняют на одном шумном прогоне, и команда учится игнорировать гейт
Уточняющие вопросы
- →Как держать baseline честным по мере закономерного роста приложения?
- →Как отделить настоящую регрессию от шума измерений между прогонами?