Копаем внутрь SpringBoard

Я, наверное как и многие iOS и macOS разработчики, каждый год жду WWDC чтобы увидеть новые API, новые инструменты и улучшения существующих. Но помимо всего, связанного с разработкой, я жду саму ОС — хочу увидеть что для меня, как для обычного пользователя ОС, изменилось.

В этом году произошло два больших изменения домашнего экрана которые у всех на слуху: добавили возможность размещать виджеты и представили библиотеку приложений. В первый же день я удалил весь хлам, бережливо расфасованный по папочкам «Other», «Utility» с домашнего экрана, оставив только то, чем пользуюсь каждый несколько раз в день на регулярной основе. Я уже давно не ищу глазами иконку нужного приложения, а пользуюсь поиском Spotlight, поэтому Apps Library стала для меня фишкой номер один в этом релизе.

Помимо удобного для меня механизма организации и поиска приложений, я обратил внимание на верхний бар со строкой поиска. Мой взгляд зацепился за блюр который, который я ранее не встречал в системе. Это градиентный блюр, радиус размытия которого плавно меняется от 0 до заданного значения. Выглядит реально круто, особенно в динамике, просто попробуйте поскройлить список! Мне стало жутко интересно, можно ли и как сделать своими руками.

Немного про блюр в iOS

Немного отвлечемся и поговорим про блюр. Несмотря на то, что Apple предоставляет разработчикам богатое API, позволяющее сделать блюр и другие эффекты для изображения, возможность делать это с живым UI существенно ограничены. Конечно всегда можно отрендерить слой, наложить на изображение эффекты и фильтры и показать это в ImageView, но как несложно догадаться, производительность такого решения будет оставлять лучшего. Всё, что остаётся — использовать дарованный с барского плеча UIVisualEffectView, который предоставляет возможность сделать эффект матового стекла с помощью блюра и поиграться с Vibrancy эффектом.

☝️

Несмотря на то, что Apple предоставляет разработчикам богатое API, позволяющее сделать блюр и другие эффекты для изображения, возможность делать это такое с живым UI существенно ограничены.

Начинаем раскопки

Итак мы поняли, что из коробки сделать ничего не получиться. Что делать, куда копать?

Без джейлбрейка мы не можем подключиться к чужому процессу и сдампить иерархию вьюх чтобы покопаться и найти нужную нам вьюху. Придется искать в слепую. Благо у нас есть симулятор и возможность покопаться в некоторых системных файлах.

Первым делом определимся где будем искать. Библиотека App Library находится на домашнем экране, значит нам нужно найти что-то связанное с процессом SpringBoard — приложением, которое ответственное за домашний экран, запуск приложений и всё с этим связанное. Попробуем его найти:

Удача! Попробуем загрузить бинарник в Hopper Disassembler чтобы посмотреть что там внутри. Надежды на то, что внутри что-то полезное мало, т. к. размер бинарника всего 140 КБайт. Дизассемблер встречает нас диалоговым окном с просьбой выбрать архитектуру, т. к. бинарник жирный.

☝️

Ага! Значит Apple, начиная с Xcode 12 поставляет «потроха» скомпилированные под классический x86_64 и под новый ARM (Apple Silicon). Сразу становится понятным почему Xcode 12 занимает так много места!

Как и ожидалось, внутри исполняемого файла ничего полезного нет. Лишь несколько процедур отвественных за старт. Значит интересное нам прячется где-то в системных фреймворках. Посмотрим с чем слинкован исполняемый файл.

Приватные фреймворки

Много анализировать не приходится, «кишки» SpringBoard’а находятся в одноименном приватном фреймворке. Вернемся вверх по дереву файлов до iOS.simruntime и поищем его относительно этой директории. А вот и папка с приватными библиотеками и нужный нам фреймворк! Грузим в дизассемблер.

Сразу попытаемся найти какие-нибудь символы, связанные с AppLibrary. Что-то есть, но не жирно. Поиграемся с поиском попробуем найти вью контроллер этой библиотеки. Есть зацепка! По строке LibraryViewController находится сеттеры и геттеры этого контроллера для корневого SBIconController’а (не трудно догадаться его назначение), но реализацией тут не пахнет. Если её тут нет, значит она есть где-то в другом фреймворке, другого не дано. Попробуем посмотреть с otool.

Ого! Этот фреймворк использует 170 внешних зависимостей! Методом перебора не справимся, нужен другой способ. В сегменте Mach-O файлов (коим является бинарник фреймворка) есть сегмент External Symbols, который содержит список внешних символов, которые содержаться во внешних библиотеках. Возвращаемся в Hopper и попробуем найти символ используя список отфильтрованный по libraryviewcontroller.

Вот и символ _OBJC_CLASS_$_SBHLibraryViewController, но нажатию на который мы можем увидеть, что объявлен он в соседнем фреймворке SpringBoardHome. Грузим его в Hopper! Теперь найдем и покопаемся в классе SBHLibraryViewController.

Ноете что у вас MassiveViewController? Взгляните на заголовок этого класса: 147 методов вам готовы насмеять в лицо вашему вьюконтроллеру:)

☝️

Сдампить все все методы всех Objective-C классов внутри Mach-O файла поможет class-dump.


Ладно, первым делом глянем метод init, переключившись в режим псевдокода — Hopper настолько хорош, что может попытаться сделать человекопонятный псевдокод из машинного.

Первая зацепка

Метод init дергает initWithCategoryMapProvider: который по сути инициализирует родительский контроллер SBNestingViewController через initWithNib:bundle: и определяет очередь неких событий. В initWithNib:bundle: передаются два nil’a что говорит о том, что инициализация вью происходит в коде. Вспоминаем жизней цикл вью контроллера (наканец-то пригодилось!) — для загрузки вью обычно используется loadView, посмотрим что там.

Ага, метод переопределен и тут вызывается _setupIconTableViewController.

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

Находится три потенциальных класса SBHIconLibraryTableViewController, SBHLibraryPodFolderController, SBHLibrarySearchController. Самым перспективным кажется последний, т. к. строка в топ баре, который я хочу повторить есть поиска. Увы, никаких зацепок найти не удаётся. Следующим на очереди был SBHLibraryPodFolderController, но и там ничего интересного найти не удалось. Наверное я бы искал еще долго, если бы по счастливой случайности/запарке я не стал стирать название целиком, а стер бекспейсом только слово Controller. Оказывается, есть еще SBHLibraryPodFolderView у которого есть метод-геттер navigationBar! Да! Это то, что нам нужно; похоже мы нашли нужный нам класс — SBHFeatherBlurNavigationBar!

И почему я раньше просто не поискал по строке Blur?! Ладно, не время себя винить, глянем что внутри. Первым делом видим initWithFrame, где создается SBHFeatherBlurView методом initWithRecipe: с параметром 0x2.

Hoper в сторону! Настало время экспериментов. Создадим новый проект в Xcode предварительно указав язык ObjectiveC. Накинем UIScrollView и какой нибудь контент для эксперимента. Не будем напрямую линковать фреймворк, загрузим его в рантайме.

Далее воспользуемся возможностями рантайма ObjectiveC и создадим инстанс искомого класса. Добавим его на нашу вью и проверим верна ли наша догадка. Специально для статьи я сделал пример похожий на оригинальный экран, чтобы было с чем сравнить. Чтож, запустим на симуляторе.


#import "FeatherBlurView.h"
#import 

#define SBH_PATH @"/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/SpringBoardHome.framework/SpringBoardHome"

// Объявим интерфейс класса из SpribngBoardHome для удобной инициализации
@interface SBHFeatherBlurView : UIView
- (instancetype) initWithRecipe:(int)arg1;
@end

@implementation FeatherBlurView

+ (void)load {
    const char *path = [SBH_PATH cStringUsingEncoding:NSUTF8StringEncoding];
    dlopen(path, RTLD_NOW);
}

- (instancetype) init {
    self = [super init];
    if (self) {
        SBHFeatherBlurView *view = [NSClassFromString(@"SBHFeatherBlurView") alloc];
        view = [view initWithRecipe:0x2];
        view.translatesAutoresizingMaskIntoConstraints = NO;
        [self addSubview:view];
        [[view.topAnchor constraintEqualToAnchor:self.topAnchor] setActive:YES];
        [[view.bottomAnchor constraintEqualToAnchor:self.bottomAnchor] setActive:YES];
        [[view.leftAnchor constraintEqualToAnchor:self.leftAnchor] setActive:YES];
        [[view.rightAnchor constraintEqualToAnchor:self.rightAnchor] setActive:YES];
    }
    return self;
}

@end

Первая победа!

📦 Тестовый проект доступен на GitHub

Йоу! Всё получилось и работает как я и хотел! Можете скачать тестовый проект и поиграться самостоятельно.

🖖

Время подвести некоторые выводы и подчеркнуть интересные факты

  • Изучать внутренности ОС не так сложно и очень интересно
  • Знания Objective-C и жизненного цикла вью контроллера могут пригодиться в 2k20
  • Все фреймворки и утилиты внутри Xcode поставляются в виде жирных бинарников, собранных под ARM и x86_64, это частично объясняет увеличенный почти вдвое размер Xcode
  • Несмотря на 2020 год и актуальный Swift версии 5.4, Apple активно использует Objective-C в системных фреймворках. Даже код для управления виджетами (которые, на секунду SwiftUI-only) написан на Objective-C
  • В Apple не бояться Massive View Controller и глубокого наследования


Но неужели так всё просто и такой блюр можно использовать в своих проектах?! К сожалению, нет. Дело даже не в использовании приватных библиотек, что запрещает Apple, а дело в том, что у нас в принципе нет возможности использовать внешние фреймворки находясь в песочнице. То есть даже сейчас запустить на девайсе это не получиться. Но это не повод опускать руки! Уже в следующей статье мы поковыряемся во внутренностях этой вью и попытаемся воссоздать её так, чтобы код можно было запустить и на девайсе! Stay tuned!

Поделиться
Отправить
Запинить
1 комментарий
Егор Меркушев 4 мес

Эффект, так-то, можно и с обычным публичным блюром воспроизвести с парочкой законных трюков.