14 September, 2017 · 7 minute read.
Recently I came across this Hacker news article, where the goal was to bring down the size of the apk, that can be installed on an android phone. People are usually familiar with Code golf. This is somewhat similar, and this gave me the idea to try this for an iOS application.
So, first I started out with a basic Single View application, that comes up whenever we choose a new project from Xcode, but I chose Swift as my programming language, since I have been used to it and developed few applications on it. To make things fast and easy, I decided not to export the archive built into an IPA, (mostly because I do not have a developer account and it’s too costly for me to afford it now). So to get the ipa file, I found out where the bundle is present from Xcode and tried zipping the bundle into a zip and rename it to a .ipa file. Theoritically, this is what Xcode also does, it creates your bundle file and compresses it to an ipa file. To verify if the ipa file will also be of the same size or not, I used iTunes to get the ipa file and verified it’s size. Both files were of same size. So building the ipa file now is easy, even though we don’t have the developer account. Of course I am not denying the features that we get having an Apple developer account, like App thinning would not be available to us, but all that can come later, once we have optimized things that we can control.
So getting back to the Swift project, just building that sample application, which did not have any source code written by me in it, apart from the auto generated
ViewController files, the size of the application was around 28Mb. On inspecting the app size, I got the respective sizes of it’s contents.
As you can see the
Frameworks folder itself takes up almost all the space. This folder contains the Swift runtime libraries. Swift apps contain these libraries because Swift is not ABI stable yet. If a language is not ABI stable, it means that you cannot guarantee that the binaries compiled on a certain version of that language will work perfectly with binaries compiled on another version of that language. You can read about what is ABI and why does it’s stability matters for a programming language and how Swift is proposing to achieve it over here. I can try app thinning and other approaches recommended by Apple, but that would require Apple developer account. So I have decided to move away from Swift and try to work on Objective C. Doing the same initial step of choosing a single view application and building the app, the archive was at 26623 bytes. So for now, until the Swift’s ABI is stable, Objective C binaries might be of smaller size. This is what the binary contains
As you can see no more frameworks included. Now I started looking at the build settings of the app for settings that would help in reducing the binary size. First thing I found was Generate debug Symbols in Code generation column of Build settings. Debug symbols are just mappings from the instructions in the binary to the lines in the source code, these are necessary to make sense of crash reports or while attaching a debugger and running a step through execution of your binary. For iOS, these debug symbols are created in a file called dSYM and zipped with the app. I wanted to disable this for debug binaries, but a better way to do this was to build Release builds on running them. This is because, there is an optimization setting just below the Generate debug symbols section. This has different values set for both release and debug. For debug the value is set to
None, and Release is set to
None as specified does not do any other optimizations on the compile phase, other than reducing the cost of compilation (which by default almost all compilers do that). This makes the statements in the binary independent, so that new variables can be assigned or change the program counter after setting a breakpoint and I didn’t want to make all these changes and when debugging would be useful in the future it would be difficult to undo all these changes. So I decided to make release builds on running the app from xcode. The size was now down to 23432 bytes.
The next thing I wanted to try was to restricting the architecture. Older iphones like iPhone 3GS, iPod touch were on
armv7 architecture. Phones like iPhone 5C were on
armv7s architecture. The newer phones are on
arm64. So I decided to restrict the architecture to
arm64 because then my binary won’t include instructions that are necessary for those devices. I think this is where Apple’s
ENABLE_BIT_CODE is helpful. When you build from
BIT_CODE, your binary basically is compatible with any kind of architecture. When you submit to the app store, Apple decides which architecture to install on your phone and then sends the particular architecture’s binary to be installed on that phone. Of course, this would result in a heavy binary being built before you submit to the App store. Anyhow, setting supported architecture to
arm64 did reduce the size but not by much. Size was not at 21006 bytes.
Now I decided to look at the contents of my archive again. I found these
Launch.storyboard files present. These storyboard files represent your app’s view. Now my app does not need two storyboards, because it will be showing up an empty screen and the UI is not my problem for this app. So I decided to edit my
Info.plist to use the same storyboard for both launch and main and then I deleted one of the storyboard files that is not being used. After this, the size reduced to 20013 bytes. I tried few things after this like disabling
Code coverage support and removing some extra contents in my
Info.plist. Finally the size reduced to 19642 bytes.
The highest contributors to size now were the app binary, storyboard files and the provisioning profile. I wanted to see if I could get rid of the storyboard file from my app. On looking at the
ViewController class, I saw that it was an interface that implements the class
UIViewController. So you really don’t need the
ViewController class if you can replace all it’s call sites with
UIViewController. All I had to do was change the class name for the scene in the storyboard to
UIViewController. That didn’t help. Of course my source code was small, but my binary was still around the same size. Now I wanted to see if I could remove the
AppDelegate file and see if that helped in reducing the size of the binary. On looking up where the
AppDelegate class is referenced in the source code, it was inside the
main.m file as
So if I did the same thing again here, I can get rid of my
AppDelegate file. So I removed
AppDelegate from the above piece of code and used
UIResponder which is what the
AppDelegate inherits from. Now the code looked something like this.
I removed the all the AppDelegate files and tried running the app. The app was launching but this time there was a black screen, instead of the white screen when it launched and was throwing me a warning saying that
Basically I have no window that shows the UI now, which is still fine because I do not care about the UI since it is not the main goal of this project, and no UI meant no storyboard files. So I removed all the contents of the
Main.storyboard and built the app now. It worked although it was showing me the same warning as above. This did help a lot, my app’s size was now reduced to 16443 bytes. The app binary still taking up half of the space and ther other half by the provisioning profile. Now I just print out the whole binary to see what does it contain. Most of it looked like Apple’s root certificate and my developer certificate and then some references to classes like
UIResponder. The only thing I can get rid of now is all these unused class definitions in the binary. Their presence would mean that there is some code which is referencing the header file that contains these definitions. And here is only one file which contains any kind of code (atleast that makes sense to me) and that is the main.m file. As suspected it was importing the
UIKit.h file and removing this would throw an error because I was still referencing
UIResponder from my main function.
That’s when it hit me, this is like any other C programme. It has a main function which is the starting point of entry for the execution process. I have been ignoring my basic concepts so far. I looked at the function definition of
UIApplicationMain that is called in the function and it says that this is the main entry to the app and once your app enters this function it never returns it, even when it is in the background. It does have an
int return type, but it never reaches it’s end. This function would be responsible for starting your event loop and doing all other initialisations that come in the app’s launch cycle. So if it is like any other C programme, then the following code should work.
This code did not have the UIKit header file included. It compiled and when I ran it, the app launched and exited. So it worked. Now the app size was at 15807 bytes. There was no function definitions that were there before in the binary. It just contains the certificates now.
This was where I gave up, I had no clue onto how to edit these things, because they are essential for the app to be installed on the phone. I could neither get rid of the provisioning profile which took the other half of the size. The only thing now left was to enroll into the developer program and then build an ipa file that is ready for app store distribution and then try different things provided by the Apple developer account.
I have pushed the current app to iOSGolf repository. Feel free to fork it or submit PR to it if you think I have missed out on something that would generate in an app binary of lesser size.
Build with Jekyll and true minimal theme