How to Shrink iOS App Size: Resource & Binary Optimization Techniques
This article explains practical methods to reduce iOS app package size by removing unused resources, analyzing Xcode LinkMap files to identify dead code and selectors, slimming protobuf implementations, and applying compiler and build‑setting optimizations.
Premise
WeChat has accumulated redundant code and unused resources after many version iterations; ARC support added in October increased the executable size by about 20%, and the mandatory 64‑bit requirement doubled the binary, making package‑size optimization urgent.
Resource slimming
Resource slimming removes useless assets and compresses needed ones. Unused resources are those present in the project but never referenced in code; they can be found by searching for the resource name (excluding @2x/@3x) in the source. Compression focuses on lossless PNG optimization using ImageOptim or the compress command; lossy compression is discouraged.
Xcode's Link Map File
The LinkMap file, generated when Write Link Map File is enabled, describes the composition of the executable, including code (__TEXT) and data (__DATA) sections. It can be found at the path set in the project settings after a build.
Each LinkMap consists of three parts (example shown for WeChat):
1. Object files:
[ 0] linker synthesized [ 1] /xxxx/WCPayInfoItem.o [ 2] /xxxx/GameCenterFriendRankCell.o [ 3] /xxxx/WloginTlv_0x168.o ...
2. Sections:
This part lists the segment table, showing each segment’s offset, size, type, and name (e.g., __text for machine code, __cstring for string constants). See Apple’s “OS X ABI Mach-O File Format Reference” for details.
3. Symbols:
# Address Size File Name 0x100005A50 0x00000074 [ 1] +[WCPayInfoItem initialize] 0x10231C120 0x00000018 [ 1] literal string: I16@?0@"WCPayInfoItem"8 0x10252A41A 0x0000000E [ 1] literal string: WCPayInfoItem ...
Executable file slimming
LinkMap analysis helps locate optimization points in the binary.
1. Find unused selectors
Objective‑C’s dynamic nature includes all classes and methods in the binary. By extracting all selectors from the __TEXT.__text section with the regex ([+|-][.+\s(.+)]) and comparing them with selectors referenced in the __DATA.__objc_selrefs segment (via otool -v -s __DATA __objc_selrefs), unused selectors can be identified. System‑API protocols should be whitelisted.
2. Find unused Objective‑C classes
Search for patterns like [ClassName alloc, ClassName *, or [ClassName class] in the code, or use otool to list all classes in __DATA.__objc_classlist and __objc_classrefs; the difference yields unused classes.
3. Scan duplicate code
Tools such as Simian can detect duplicated code, though the results may be noisy and refactoring can be costly.
4. Protobuf slimming
Google’s protobuf generates verbose code. By abstracting common serialization methods into a base class and letting derived classes provide field metadata, the binary size can be reduced.
field number
field label (optional, required, repeated)
wire type (double, float, int, etc.)
whether packed
repeated data type
<code>typedef struct { Byte _fieldNumber; Byte _fieldLabel; Byte _fieldType; BOOL _isPacked; int _enumInitValue; union { __unsafe_unretained NSString* _messageClassName; __unsafe_unretained Class _messageClass; // ClassName对应的Class IsEnumValidFunc _isEnumValidFunc; // 检测枚举值是否合法函数指针 }; } PBFieldInfo;</code>
Unused selectors reveal protobuf property getters/setters that are never called. Replacing @synthesize with @dynamic and removing unused ivars further shrinks the binary.
Base class adds an ivarValues array (similar to objc_class ivars) to store property values as objects; primitives are wrapped in NSValue.
Override methodSignatureForSelector: to return signatures for getters/setters.
Override forwardInvocation: to handle dynamic getter/setter calls.
Override setValue:forUndefinedKey: and valueForUndefinedKey: to avoid KVO crashes.
Performance tweaks: cache hash of property names, store types, replace std::map with arrays, use MRC instead of ARC where appropriate.
<code>class PBClassInfo { public: PBClassInfo(Class cls, PBFieldInfo* fieldInfo); ~PBClassInfo(); public: unsigned int _numberOfProperty; std::string* _propertyNames; size_t* _propertyNameHashes; std::string* _getterObjCTypes; std::string* _setterObjCTypes; PBFieldInfo* _fieldInfos; }; @interface WXPBGeneratedMessage () { uint32_t _has_bits_[3]; // 最多96个属性,表示属性是否有赋值 int32_t _serializedSize; PBClassInfo* _classInfo; id* _ivarValues; } - (NSMethodSignature*) methodSignatureForSelector:(SEL) aSelector; - (void) forwardInvocation:(NSInvocation*) anInvocation; - (void) setValue:(id) value forUndefinedKey:(NSString*) key; - (id) valueForUndefinedKey:(NSString*) key; @end</code>
After removing redundant code, the GameResourceReq protobuf class shrank from 127 lines to 8 lines, saving 8.8 MB in the binary and 2.5 MB by using @dynamic for properties.
<code>message GameResourceReq { required BaseRequest BaseRequest = 1; required int32 PropsCount = 2; repeated uint32 PropsIdList = 3[packed=true]; }</code>
<code>// Old implementation @implementation GameResourceReq @synthesize hasBaseRequest; @synthesize baseRequest; @synthesize hasPropsCount; @synthesize propsCount; @synthesize mutablePropsIdListList; @dynamic propsIdList; - (id) init {...} - (void) SetBaseRequest:(BaseRequest*) value {...} - (void) SetPropsCount:(int32_t) value {...} - (NSArray*) propsIdListList {...} - (NSMutableArray*)propsIdList {...} - (void)setPropsIdList:(NSMutableArray*) values {...} - (BOOL) isInitialized {...} - (void) writeToCodedOutputStream:(PBCodedOutputStream*) output {...} - (int32_t) serializedSize {...} + (GameResourceReq*) parseFromData:(NSData*) data {...} - (GameResourceReq*) mergeFromCodedInputStream:(PBCodedInputStream*) input {...} - (void) addPropsIdList:(uint32_t) value {...} - (void) addPropsIdListFromArray:(NSArray*) values {...} @end</code>
<code>// New implementation @implementation GameResourceReq PB_PROPERTY_TYPE baseRequest; PB_PROPERTY_TYPE opType; PB_PROPERTY_TYPE brandUserName; + (void) initialize { static PBFieldInfo _fieldInfoArray[] = { {1, FieldLabelRequired, FieldTypeMessage, NO, 0, ._messageClassName = STRING_FROM(BaseRequest)}, {2, FieldLabelRequired, FieldTypeInt32, NO, 0, 0}, {3, FieldLabelRepeated, FieldTypeUint32, NO, 0, 0}, }; initializePBClassInfo(self, _fieldInfoArray); } @end</code>
5. Compiler option optimization
Set Strip Link Product to YES – reduces WeChatWatch binary by 0.3 MB.
Enable Make Strings Read‑Only – cuts ~3 MB.
Disable C++ and Objective‑C exceptions (set to NO, add -fno-exceptions) – saves 27 MB (17.3 MB from __gcc_except_tab, 9.7 MB from __text). Exceptions can be re‑enabled per file if needed.
6. Other explored ways
iOS 8 Embed‑Framework: extract common code from WeChatWatch, ShareExtension, and main app to reduce binary by >5 MB (requires iOS 8+).
iOS 9 App Thinning: lets the App Store deliver only the needed architecture and resources per device, reducing installed size.
7. Establish monitoring
By analyzing LinkMap files across versions, the size contribution of each module can be tracked, enabling detection of growth spikes.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
WeChat Client Technology Team
Official account of the WeChat mobile client development team, sharing development experience, cutting‑edge tech, and little‑known stories across Android, iOS, macOS, Windows Phone, and Windows.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
