Оптимизация ресурсов iOS приложений

в 9:46, , рубрики: iOS, optipng, pngcrush, разработка под iOS, метки: , ,

При сборке приложений под iOS для оптимизации ресурсов используется скрипт iphoneos-optimize из набора XCode. Работает он отлично, но если копнуть поглубже, то становится ясно, что некоторые файлы не пережимаются, а другие хоть и немного уменьшаются, но все-равно далеки от идеала. Можно сказать, что задача скрипта сделать файлы более совместимыми с iPhone, чтобы они быстрее читались или распаковывались, но скорее всего это имело смысл лишь на старых iPhone 1 и иже с ними, а уже на процессорах 1ГГц с ARM 7 это откровенно не актуально.
С помощью простых оптимизаций и парочки программ из набора MacPorts можно добиться существенного уменьшения PNG и JPG картинок в конечной программе, а при желании и других видов данных.

Разведка

Для начала посмотрим, что собой представляет оригинальный iphoneos-optimize. Это небольшой Perl скрипт, который лежит в папке /Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/. Для XCode 4.3 и выше это будет папка /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/

Основная часть скрипта довольно проста и хорошо читабельна:

print "$SCRIPT_NAME: Converting plists to binary in $dstrootn";
find( { wanted => &optimizePlists }, $dstroot );

exit(0) if defined $options and $options =~ /-skip-PNGs/;
print "$SCRIPT_NAME: Optimizing PNGs in $dstrootn";
find( { wanted => &optimizePNGs }, $dstroot );

Тут перебираются все файлы в dstroot и отправляются на вход функциям optimizePlists и OptimizePNGs. Для оптимизации PNG используется модифицированная Apple версия pngcrunch c ключом iphone:

my @args = ( $PNGCRUSH, "-iphone", "-f", "0", $name, $crushedname );
if (system(@args) != 0) {
	print STDERR "$SCRIPT_NAME: Unable to convert $name to an optimized png!n";
	return;
}
unlink $name or die "Unable to delete original file: $name";
rename($crushedname, $name) or die "Unable to rename $crushedname to $name";
Pngcrush vs OptiPNG

Что именно делает ключ -iphone нам не узнать, но по большому счету это и не так важно. При детальном рассмотрении видно, что если pngcrush вернул ошибку или не смог уменьшить файл, то временный файл удаляется, а основной остается без изменений. В моем случае так происходило с неким файлом browser.png:

Recompressing browser.png
Total length of data found in IDAT chunks = 433949
IDAT length with method 120 (fm 1 zl 9 zs 1) = 512501
Best pngcrush method = 120 (fm 1 zl 9 zs 1) for browser_iphone.png
(18.10% IDAT increase)
(18.11% filesize increase)

CPU time used = 2.569 seconds (decoding 0.040,
encoding 2.490, other 0.039 seconds)

Без ключа -iphone ситуация была получше и файл таки уменьшался:

Recompressing browser.png
Total length of data found in IDAT chunks = 433949
IDAT length with method 1 (fm 0 zl 4 zs 0) = 740769
IDAT length with method 2 (fm 1 zl 4 zs 0) = 611778
IDAT length with method 3 (fm 5 zl 4 zs 1) = 485419
IDAT length with method 9 (fm 5 zl 2 zs 2) = 743935
IDAT length with method 10 (fm 5 zl 9 zs 1) = 427514
Best pngcrush method = 10 (fm 5 zl 9 zs 1) for browser_tmp.png
(1.48% IDAT reduction)
(1.47% filesize reduction)

CPU time used = 3.949 seconds (decoding 0.176,
encoding 3.766, other 0.007 seconds)

Но есть еще и другой путь — GPL утилита optipng. Из MacPorts она устанавливается без проблем командой
sudo port install optipng

Вот результат работы утилиты с тем же самым browser.png:
** Processing: browser_opti.png
1024x1024 pixels, 4x8 bits/pixel, RGB+alpha
Input IDAT size = 433949 bytes
Input file size = 434043 bytes

Trying:
zc = 9 zm = 8 zs = 1 f = 5 IDAT size = 427390

Selecting parameters:
zc = 9 zm = 8 zs = 1 f = 5 IDAT size = 427390

Output IDAT size = 427390 bytes (6559 bytes decrease)
Output file size = 427484 bytes (6559 bytes = 1.51% decrease)

Как видно, размер файла стал еще меньше, чем у pngcrush, а скорость работы при этом заметно выше. В некоторых других случаях разрыв еще более заметен.
Важнее всего то, что полученные PNG файлы отлично открываются на iPhone и iPad, никаких визуальных искажений в них нет, разница в скорости открытия отсутствует или не заметна.

Интегрировать optipng в скрипт довольно просто: в шапке скрипта мы меняем переменную с указанием на PNG оптимизатор и путь к нему:

my $PNGCRUSH_NAME = "optipng";
my $PNGCRUSH = "/opt/local/bin/$PNGCRUSH_NAME";

В теле функции optimizePNGs меняется только строка с параметрами. Оптимальный результат я получил с параметрами -o2 и -f0:

my @args = ( $PNGCRUSH, "-o2", "-f0", $name, "-out", $crushedname );

Конечно, надо не забывать делать бекап скрипта, а заодно иметь права администратора на его редактирование.

Оптимизация JPG

JPEG файлы часто содержат EXIF информацию, а порой и различный мусор, не нужный на телефоне. Также есть разница при использовании прогрессивного режима и других настроек. Удобнее всего воспользоваться утилитой jpegoptim, которая сама откинет ненужное, оптимизирует таблицы Хафмана и выберет оптимальные настройки при том же уровне качества. При желании, можно задать параметры, чтобы утилита уменьшила качество изображения, тогда оно будет сжато заново с указанным качеством. Устанавливать ее тоже можно из MacPorts:

sudo port install jpegoptim

Остается только добавить вызов этой программы в iphoneos-optimize.
В заголовке:

my $JPGCRUSH_NAME = "jpegoptim";
my $JPGCRUSH = "/opt/local/bin/$JPGCRUSH_NAME";

В теле:

print "$SCRIPT_NAME: Optimizing jpgs in $dstrootn";
find( { wanted => &optimizeJPGs }, $dstroot );

Новая функция:

sub optimizeJPGs {
	my $name = $File::Find::name;
	if ( -f $name && $name =~ /^(.*).jpg$/i) {
		my @args = ( $JPGCRUSH, "--strip-all", $name );
		if (system(@args) != 0) {
			print STDERR "$SCRIPT_NAME: Unable to convert $name to an optimized jpg!n";
			return;
		}
		print "$SCRIPT_NAME: Optimized JPG: $namen";
	}
}

Я сознательно не стал повторять результат выполнения программы, потому что в случае неудачи оптимизации она просто не изменяет исходный файл.
Работает утилита очень быстро и выводит минимум информации:

jpegoptim --strip-all english.jpg
english.jpg 320x480 24bit JFIF [OK] 66466 --> 59686 bytes (10.20%), optimized.

Теперь достаточно пересобрать проект в XCode и процесс оптимизации ресурсов с iphoneos-optimize пройдет заметно быстрее, а результат будет на 10-15% меньше.

Другие оптимизации

Дополнительно можно дописать в скрипт другие расширения (JPEG, JFIF, JPE, JIF, etc.), добавить уменьшение качества и пр. Да и вообще, в сети достаточно разных оптимизаторов PNG, JPEG, CAF и других файлов, которые можно было бы использовать. К примеру, базы SQLite можно оптимизировать с помощью такой команды:
sqlite3 database.sqlite vacuum;

Команда пересоздаст базу со всеми данными, выбросив различный мусор, остатки старых транзакций и т.п. Интеграцию этой и других команд в скрипт оставим на усмотрение читателя.

На самом деле, есть еще как минимум два интересных метода уменьшения размера PNG файлов (если откинуть другие программы-оптимизаторы и ручное упрощение рисунка). Первый не совсем канонический и может быть воспринят как ересь, но факт есть факт: компилятор Android (точнее apktool) умеет отслеживать картинки с малым количеством цветов и переводить их в Paletted формат. Более того, даже полноцветные PNG могут стать еще меньше. Достаточно добавить нужные изображения в папку res/drawable рабочего проекта, собрать его и затем списать оптимизированные файлы из папки bin/res/drawable. Конечно, это требует некоторых навыков работы с Android SDK и не совсем относится к разработке под iOS.

Второй метод более универсальный: уменьшить цветовой охват файла с RGBA8888 до RGB565 или RGBA4444. При этом размер может как вырасти из-за dithering, так и заметно уменьшится в случае с рисованными изображениями. Для этих операция я написал собственную консольную утилиту, но ее рассмотрение лежит уже за пределами этой статьи.

P.S. Текст готового скрипта iphoneos-optimize: paste2.org/p/2045147

Автор: Nomad1

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js