Как стать автором
Обновить

Оптимизация ПО для iPhone: живой пример

Время на прочтение 7 мин
Количество просмотров 2.7K
Программирование на платформе iOS (той, что еще недавно называлась iPhone OS) – странное сочетание радости от плодотворной работы и муки плавания против течения. У каждого разработчика свое мнение относительно того, какая из этих компонент преобладает. Лично мне это занятие нравится, поэтому мне показалось уместным поделиться впечатлениями от процесса работы над очередным проектом.

В конце марта мне предложили написать мобильную версию Bookmate для iPhone. Дизайн большей части приложения был уже готов в виде толстенного PSD, на стороне сервера работа кипела, мне же оставалось, как говорится, «всего лишь» написать клиентскую часть на Objective-C.

В этой статье речь пойдет о первом контейнере с граблями, нас атаковавшими. Если Вы играете в Starcraft, более подходящей будет аналогия с зергами, которые вдруг полезли изо всех щелей в типично-неимоверных количествах.

Архитектура


В двух словах, Bookmate – это сервер, на котором хранятся книги, состоящие из набора HTML-файлов. Основная задача клиента Bookmate (в простонародии, «читалки») – показывать и обрабатывать этот HTML. Почему HTML? В отличие от, например, PDF, его достаточно легко переформатировать при изменении размера шрифта, что совершенно необходимо в мобильном приложении на крошечном экране. С другой стороны, поскольку в HTML отсутствует привычное понятие страницы, он, строго говоря, не очень подходит для отображения книг в традиционном постраничном виде. Чтобы правильно перенести строку, которая не умещается на экране, на следующую страницу, нужно знать ее вертикальный отступ на странице. Для этого движок HTML должен сначала обработать весь документ и просчитать размеры и координаты всех блоков в документе согласно таблице стилей. После этого можно делать то, что делает любая программа верстки типа Adobe InDesign или QuarkXPress.

Коллеги из Bookmate к тому моменту уже написали библиотеку на JavaScript, которая все это делает красиво и быстро.

Hello iPhone


В качестве базовой тестовой платформы мы выбрали iPhone 3G, т.к. это и очень популярная в России, и самая старая модель iPhone, у меня сохранившаяся. В качестве одной из тестовых книг выступило издание «Модели для сборки» Хулио Кортасара в одном файле размером 890 КБ.

Первый прототип представлял из себя UIWebView, библиотеку JavaScript и некоторое (хотя и существенное) количество кода прослойки между ними на Objective-C.

iPhone 3G – весьма и весьма шустрый телефон. Местами. Честно говоря, я вряд ли когда-либо пытался открывать в Mobile Safari настолько тяжелые сайты, да я и не уверен, что такие встречаются в природе. Бедный айфон! За те 40 с лишним секунд, которые у него уходили на открытие «Модели для сборки», он успевал и закрыть все остальные приложения из-за нехватки памяти, и орать мне о том, что сейчас память кончится вообще, и стонать о тяжелой судьбе, и греться не в силах что-либо изменить.

Но нам в тот момент было не до шуток. Первый зерг из авангарда вцепился в ногу, а земля дрожала под полчищем, еще невидимым, но уже страшным.

Оптимизировать, по сути, было нечего. Добрую половину этих 40 секунд WebKit занимался парсингом, построением DOM, пересчетом геометрии и пожиранием (за неимением других терминов) памяти. Последнее само по себе является серьезной проблемой в iOS из-за невозможности использовать диск для виртуальной памяти, а в нашем случае это еще приводило к необходимости срочно освободить память за счет фоновых программ (таких как Mail и Phone), что также занимает время. JavaScript, перебирающий весь DOM, просто добивал WebKit.

Такая производительность, очевидно, никого не устраивала. Очевидно было и то, что JavaScript на айфоне недостаточно быстр для наших целей, и то, что мы не можем позволить себе 20 МБ памяти.

И что с этим делать?


Чтобы лучше понять масштаб катастрофы, нужно немного прерваться на описание контекста, в котором она происходит. WebKit – библиотека с открытым кодом. Но в iOS она относится к private API, использование которых запрещено Эпплом. Даже если бы было возможно собрать WebKit для iOS самому и включить его в программу в виде статической библиотеки, UIKit уже линкуется с WebKit, что неизбежно приводит к коллизиям символов; даже если переименовать весь WebKit и избежать коллизий, UIWebView не предоставляет никакого доступа к компонентам WebKit, на которых он построен: см. выше про использование private API. Выходит, что мы не можем добраться до DOM из Objective-C напрямую, без JavaScript. А значит, как сказала еще недавно одноглазая черепаха слепой, «Ну все, приплыли».

Остается только одно – забыть про WebKit: парсить HTML с помощью libxml и рисовать DOM вручную. То есть нужно написать свой движок HTML. На такое я мог согласиться только ради эксперимента, чтобы понять, имеет ли это смысл с точки зрения производительности. В конце концов, я видел исходники WebKit и отдаю себе отчет в бесперспективности попытки переписать его в одиночку, при этом сделав его намного быстрее. С другой стороны, мы не используем возможности WebKit даже на 10%. Если писать свой узкоспециализированный движок, он будет в десятки раз легче любого современного браузера. Эх, где наша не пропадала!

libXML


На ежегодно проводимом в аду для программистов конкурсе на худшие API у libxml опять главный приз. Возможно, я придираюсь, но исходники намного понятнее документации. Типичный пример:
Function: htmlCtxtReset
void htmlCtxtReset(htmlParserCtxtPtr ctxt)
Reset a parser context
ctxt: an HTML parser context

На полном серьезе, и это все?! Все, что тут написано, и так понятно из названий функции и аргумента. Что именно эта функция делает? Зачем она нужна? В каких случаях ее использовать? Какой, извините, балбес это написал? Садись, libxml, двойка.

Справедливости ради стоит сказать, что сама библиотека работает достаточно хорошо, чтобы ей пользовались почти все. И в жизни она намного проще. И есть на каждом айфоне.

Первые результаты


Чтобы потерять как можно меньше времени на тупиковую разработку, я начал с самого медленного участка. Это вычисление размеров блоков текста, сводящееся к суммированию размеров глифов. Результаты замеров производительности оказались не без сюрпризов.
  • UIWebView+JavaScript – 38 секунд, 20 МБ памяти. Это наш первый вариант после всех оптимизаций.
  • NSString, -sizeWithFont: – 14 секунд, 8 МБ памяти. Поскольку нет смысла многократно вычислять размер одних и тех же глифов в одном шрифте, результаты кэшируются. 35% времени занимает вызов метода -[NSString sizeWithFont:].
  • Core Text под iPhone OS 3.1.3 – более 70 секунд. Core Text является private API в iPhone OS до версии 3.2, мне просто хотелось сравнить его скорость, т.к. это изумительно простой в использовании и очень быстрый на маке инструмент.

Вооружившись цифрами, я написал письмо в тех. поддержку для разработчиков (т.н. DTS, Developer Technical Support, являющаяся в целом платной услугой). Заодно спросил, почему функция CGFontGetGlyphsForUnichars(), лежащая в основе -[NSString sizeWithFont:], относится к private API.

Вот за что я уважаю Apple, так это за отношение к разработчикам, когда последние не ноют на весь интернет, а задают вопросы «по установленной форме». Через два дня я получил развернутый ответ на мои вопросы, из которого следовало:
  • доступ к WebKit не предусмотрен политикой партии;
  • Core Text под iPhone OS до версии 3.2 является private API из-за того, что слишком медленный; начиная с версии 3.2, он должен быть существенно быстрее UIWebView+JavaScript;
  • CGFontGetGlyphsForUnichars() является private API по причине ее не вполне адекватного дизайна; в интернете можно найти ее аналоги и убедиться, что это так и есть;
  • в целом я на правильном пути, ибо другого они сами не знают;
  • придется сделать выбор между удобством -[NSString sizeWithFont:] и скоростью Core Graphics.


За дело!


Алгоритм выкладки текста звучит довольно просто, если не вдаваться в детали. Сначала нужно разбить текст на фрагменты с одинаковым стилем, например, если в середине блока текста встречается фраза, выделенная курсивом, такой блок разбивается на три стилевых фрагмента. Затем нужно понять, в каких местах текст можно переносить на новую строку – это, как правило, знаки препинания между словами или переносы внутри слов. Назовем их кандидатами на перенос. Затем мы берем фрагменты текста между кандидатами на перенос, один за другим, считаем их длину, и ставим их на строку до тех пор, пока их суммарная длина не превысит длину строки. Аналогично со страницами: добавляем строки до тех пор, пока очередная строка не вылезет за высоту страницы.

Получается внушительный набор объектов: каждый блок текста (например, параграф) состоит из массива строк, содержащих т.н. glyph runs – фрагменты текста одного стиля. Эти самые glyph runs мы и рисуем целиком как они есть.

В этот момент появляются интересные возможности для оптимизации, а код быстро превращается в спагетти. По мере того, как мы опускаемся к самым низкоуровневым API, становится понятно, что лингвистического образования не хватает. На горизонте возникают все новые системы письменности, с которыми этот код не работает. Например, из-за направления письма, как в арабском или иврите, или из-за отсутствия пробелов между словами, как в тайском.

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

Тем не менее, по части производительности удалось добиться значительного прогресса по сравнению с тем, с чего мы начали. Сейчас шедевр Кортасара появляется на экране менее чем за 6(!) секунд. Это был по-настоящему тяжелый месяц. Мне пришлось временно перепрошить себе мозг, иначе он наотрез отказывался работать с HTML и CSS. Моя жена все это время слушала из моего угла только кряхтение да матюки. Клиенту было тоже не сладко: во-первых, никто не рассчитывал на лишний месяц, во-вторых, начало работы над проектом обескураживало («хорошенький старт, что ж дальше-то будет?»), в-третьих, не было никаких гарантий, что эта куча фанеры вообще сможет взлететь. Однако, кто не рискует, тот ходит пешком. А нам так хотелось в небо!

Приложение Bookmate уже доступно в App Store (ссылка открывается в iTunes; если она по какой-то причине не работает, есть альтернативная, открывается в браузере).

Эпилог


Про Стива Джобса рассказывают такую историю. В 1983 году он убеждал инженеров оптимизировать время загрузки макинтоша такими словами: «Сколько людей будет пользоваться макинтошем? Миллион? Нет, больше. Бьюсь об заклад, через несколько лет пять миллионов людей будут включать свои макинтоши хотя бы раз в день. Допустим, вы можете сократить время загрузки на 10 секунд. Умножьте это на пять миллионов пользователей, получится 50 миллионов секунд, каждый божий день. За год это, наверное, десятки жизней. Если вы сделаете его загрузку на 10 секунд быстрее, вы спасли десяток жизней. Ну что, оно того стоит?». Думаю, да.
Теги:
Хабы:
+76
Комментарии 83
Комментарии Комментарии 83

Публикации

Истории

Работа

Swift разработчик
38 вакансий
iOS разработчик
23 вакансии

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн