Optimizing Resources for iOS Apps
With a few simple optimizations and a couple of tools from MacPorts, you can achieve a substantial reduction in the size of PNG and JPG images in the final app, and other kinds of data too if you want.
Reconnaissance
Let’s start by looking at what the original iphoneos-optimize actually is. It’s a small Perl script located in /Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/. For Xcode 4.3 and later, that’s /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/
The main part of the script is quite simple and easy to read:
print "$SCRIPT_NAME: Converting plists to binary in $dstroot\n";
find( { wanted => \&optimizePlists }, $dstroot );
exit(0) if defined $options and $options =~ /-skip-PNGs/;
print "$SCRIPT_NAME: Optimizing PNGs in $dstroot\n";
find( { wanted => \&optimizePNGs }, $dstroot );
Here it walks every file under dstroot and feeds them to the optimizePlists and OptimizePNGs functions. PNG optimization uses Apple’s modified version of pngcrush with the -iphone flag:
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
We can’t know exactly what the -iphone flag does, but honestly it doesn’t matter that much. Looking closely, you can see that if pngcrush returns an error or fails to shrink the file, the temporary file gets deleted and the original is left untouched. In my case this happened with a certain browser.png file:
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)
Without the -iphone flag things were better and the file actually shrank:
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)
But there’s another way — the GPL utility optipng. It installs cleanly from MacPorts with the command
sudo port install optipng
Here’s the result of running the utility on that same 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)
As you can see, the file ends up even smaller than with pngcrush, and it runs noticeably faster. In some other cases the gap is even more pronounced.
Most importantly, the resulting PNG files open perfectly on iPhone and iPad — there’s no visual distortion, and no noticeable (or any) difference in load time.
Integrating optipng into the script is fairly simple: at the top of the script we change the variable that points to the PNG optimizer and its path:
my $PNGCRUSH_NAME = "optipng";
my $PNGCRUSH = "/opt/local/bin/$PNGCRUSH_NAME";
In the body of the optimizePNGs function, only the line with the parameters needs to change. I got the best results with the -o2 and -f0 parameters:
my @args = ( $PNGCRUSH, "-o2", "-f0", $name, "-out", $crushedname );
Of course, don’t forget to back up the script, and you’ll need administrator rights to edit it.
JPG optimization
JPEG files often contain EXIF data, and sometimes assorted junk that’s not needed on the phone. There’s also a difference depending on whether progressive mode and other settings are used. The most convenient tool is jpegoptim, which strips out the unnecessary bits on its own, optimizes the Huffman tables, and picks the best settings at the same quality level. If you want, you can pass parameters to have the utility reduce image quality, in which case it will recompress at the specified quality. It can also be installed from MacPorts:
sudo port install jpegoptim
All that’s left is to add a call to this program in iphoneos-optimize.
In the header:
my $JPGCRUSH_NAME = "jpegoptim";
my $JPGCRUSH = "/opt/local/bin/$JPGCRUSH_NAME";
In the body:
print "$SCRIPT_NAME: Optimizing jpgs in $dstroot\n";
find( { wanted => \&optimizeJPGs }, $dstroot );
New function:
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: $name\n";
}
}
I deliberately didn’t reproduce the program’s output here, since when optimization fails it simply leaves the original file unchanged.
The utility runs very fast and prints minimal output:
jpegoptim --strip-all english.jpg
english.jpg 320x480 24bit JFIF [OK] 66466 --> 59686 bytes (10.20%), optimized.
Now all you need to do is rebuild the project in Xcode, and the resource optimization pass with iphoneos-optimize will run noticeably faster, with a result that’s 10-15% smaller.
Other optimizations
You could add other extensions to the script (JPEG, JFIF, JPE, JIF, etc.), add quality reduction, and so on. And really, there are plenty of different PNG, JPEG, CAF and other file optimizers out there that could be used. For example, SQLite databases can be optimized with a command like this:
sqlite3 database.sqlite vacuum;
This command rebuilds the database with all its data, discarding various junk, leftovers from old transactions, and so on. Integrating this and other commands into the script is left as an exercise for the reader.
Actually, there are at least two more interesting methods for shrinking PNG file size (setting aside other optimizer tools and manual art simplification). The first isn’t exactly canonical and might be seen as heresy, but it’s a fact: the Android compiler (or rather apktool) can detect images with a small number of colors and convert them to Paletted format. Moreover, even full-color PNGs can end up smaller this way. Just add the images you need to the res/drawable folder of a working project, build it, and then grab the optimized files from the bin/res/drawable folder. Of course, this requires some familiarity with the Android SDK and isn’t really related to iOS development.
The second method is more universal: reduce the file’s color depth from RGBA8888 to RGB565 or RGBA4444. Depending on the case, this can either increase the size due to dithering, or noticeably shrink it for hand-drawn images. I wrote my own console utility for this, but covering it is beyond the scope of this article.
P.S. The finished iphoneos-optimize script: paste2.org/p/2045147
Originally published on Habr, 2012-06-09.
