author: Kamil Burczyk
After previous 2 tutorials we have a CocoaPods project that can be easily built for multiple architectures and is merged into one static library, but...
What happens if user adds AFNetworking to his app (it is already a dependency in our static library)?
He gets this:
You can try this on your own by cloning ReposBrowserStatic repository and adding our built library to it.
It is caused by the structure of library
.a file. Simple explanation is that each class from
.m file is compiled into an object
.o file that contains a set of defined symbols and then all object files are linked (merged) together into one static library
.a which extracts those symbols and places them in flat common space. You can read more about it e.g. on Wikipedia.
The same thing occurs when you include a library in your app - all the symbols are merged together into a common space. That's why we get those duplication errors.
We can investigate those symbols using
nm command. If you walk into your
Derived Data subfolder and type something like:
$ nm libNetworkLib-debug-0.1.a | grep AFN
You will get a list of all symbols connected to AFNetworking:
000000000000bea0 s -[UIWebView(_AFNetworking) af_HTTPRequestOperation].eh 0000000000000030 t -[UIWebView(_AFNetworking) af_setHTTPRequestOperation:] 000000000000bec8 s -[UIWebView(_AFNetworking) af_setHTTPRequestOperation:].eh 0000000000000190 t ___44-[UIWebView(AFNetworking) requestSerializer]_block_invoke 000000000000bf18 s ___44-[UIWebView(AFNetworking) requestSerializer]_block_invoke.eh
How can we avoid those collisions? I came up with 3 or even 4 solutions:
- Pretend they don't exist ;) - it may not be the solution but if you use some rare libraries and you are 100% sure they will not be used in your or your customer's project you can just leave it like this.
- Write in docs which dependencies are required - you can explicitly say that you require e.g.
1.3.3and basically force user of your library to provide such version during build time but it is just mean (if they don't their code will not compile at all). You force someone else to do your job and you don't leave them any choice about version of those dependencies.
- Export all dependencies' headers - alongside your own headers you can export headers of dependencies. Users of your library will get them basically for free - they can just import what's necessary and start development. The only major disadvantage is that it forces them to use version specified in your library. They cannot upgrade those dependencies without new version of your lib.
- Build library with prefixed symbols - in my opinion the best way. We would like to add a prefix to all of the symbols in final
.afile so that e.g.
SIGMAPOINT_AFHTTPSessionManager. Your version of dependency is not connected to version used by developer and no collisions occurs. The only disadvantage is bigger output file because when someone uses the same library the final file will contain the same symbols: once prefixed and once not prefixed. But the whole build process will work smooth and without errors. This is the solution we will investigate further in this article.
When I faced this problem I used solution described in this blog.
The concept is to export all the symbols from built library, generate some macros that will replace original symbols with prefixed versions and place those macros in build process in such way that final library contains only prefixed symbols.
We will use this script to export symbols and generate macros.
The most important part of the script is to find only those symbols that should be replaced. It's done by lines similar to this one:
nm $CODESIGNING_FOLDER_PATH -j | sort | uniq | grep "_OBJC_CLASS_\$_" | grep -v "\$_AGSGT" | grep -v "\$_CL" | grep -v "\$_NS" | grep -v "\$_UI" | sed -e 's/_OBJC_CLASS_\$_\(.*\)/#ifndef \1\'$'\n''#define \1 __NS_SYMBOL(\1)\'$'\n''#endif\'$'\n''/g' >> $header
$CODESIGNING_FOLDER_PATHis set by Xcode during compilation,
-jprints only names, without addresses etc.,
grep "_OBJC_CLASS_\$_"selects all the symbols containing that string,
grep -v ...rejects given string, so here we will reject all compiled symbols with
NS(NextStep) etc. Those are the symbols we don't want to prefix. If you use some other Apple frameworks you should extend that script with the symbols you want to preserve.
- in the end
sedcommand generates a macro and places it in
So let's generate it! All you have to do is to select Pods project in Xcode, select Pods-NetworkLib target and add Run Script phase at the end. As a script you just paste this file.
Your configured environment should look like this:
If you hit ⌘B you should get
NamespacedDependencies.h file in your Pods directory. You can copy it somewhere and remove Run Script phase because we don't actually need to generate it during each build. Although you must generate this file again if you change something in Podfile like e.g. you add new dependency.
Macros in that file look like this:
#ifndef AFHTTPSessionManager #define AFHTTPSessionManager ADD_PREFIX(AFHTTPSessionManager) #endif
they simply state: if you find a symbol
ADD_PREFIX(...) macro which replaces that symbol with
Now the trick is that if we add that file in the end of
Pods-NetworkLib-environment.h it will be included in each CocoaPods dependency, because this header works as standard
.pch file from Xcode project!
When you add it like this:
and compile your lib again you can check which symbols are generated inside e.g.
libPods-NetworkLib.a file by running:
nm libPods-NetworkLib.a | grep SIGMAPOINT
It should give you output similar to the one below:
000000000000b720 s l_OBJC_$_CATEGORY_ SIGMAPOINT_AFURLConnectionOperation_$__UIProgressView 000000000000b6d8 s l_OBJC_$_PROP_LIST_ SIGMAPOINT_AFURLConnectionOperation_$__UIProgressView U _OBJC_CLASS_$_ SIGMAPOINT_AFHTTPRequestOperation U _OBJC_CLASS_$_ SIGMAPOINT_AFHTTPRequestSerializer U _OBJC_CLASS_$_ SIGMAPOINT_AFHTTPResponseSerializer
which lists all symbols prefixed with
From this point it's only one step to have fully-functional prefixed library - you just need to include generated macros in your
Project-Prefix.pch file so they are visible globally in your code.
NamespacedDependencies.h to our NetworkLib project and edit
NetworkLib-Prefix.pch file so it looks similar to the one below:
You can visually check if generated macros by checking the color of each third party symbol - if they are brown and if they link to a particular macro when ⌘-clicked then your configuration is fine (being a visualizer helps in that case too ;) ).
Hit ⌘B to build a library using AggregateLib target and check final symbols by running
Derived Data/NetworkLib directory:
nm -j libNetworkLib-debug-0.1.a | grep SIGMAPOINT
_OBJC_CLASS_$_SIGMAPOINT_AFURLConnectionOperation l_OBJC_$_CATEGORY_SIGMAPOINT_AFURLConnectionOperation_$__UIProgressView l_OBJC_$_PROP_LIST_SIGMAPOINT_AFURLConnectionOperation_$__UIProgressView _OBJC_CLASS_$_SIGMAPOINT_AFHTTPRequestOperation _OBJC_CLASS_$_SIGMAPOINT_AFHTTPRequestSerializer _OBJC_CLASS_$_SIGMAPOINT_AFHTTPResponseSerializer
With such prepared library our ReposBrowserStatic project which includes
AFNetworking and our compiled library should compile and run without errors even with our own AFNetworking dependency!