Analyzing Mach-O Files with Otool to Identify Unused Classes and Methods in iOS Apps
This article explains how to use the otool command to parse Mach-O sections, extract __objc_classlist and __objc_classrefs, compute their differences, and locate unused Objective‑C classes and methods for iOS app size optimization, providing sample Objective‑C code and detailed implementation steps.
Background
When optimizing iOS app bundle size, developers often need to analyze Mach‑O files to find unused Objective‑C classes and methods. Public articles mention using otool to read sections such as __objc_classrefs and __objc_classlist, then computing the set difference to identify dead code.
Principle
Mach‑O (Mach Object) is the binary format for executables, libraries, and memory dumps on Apple platforms. It consists of a Mach Header, Load Commands, and Data sections. Important sections for Objective‑C analysis include __TEXT.__text, __DATA.__objc_classlist, __DATA.__objc_classrefs, __DATA.__objc_selrefs, among others.
Implementation
The workflow is:
Obtain the Mach‑O file from an .ipa (rename to .zip, unzip, locate the executable inside the .app bundle).
Use otool to dump class lists, class references, and selector references.
Parse the output to build dictionaries of all classes, referenced classes, all methods, and referenced methods.
Compute set differences to get unused classes and methods.
Using otool
Examples:
otool -arch arm64 -ov TestClass > otool.txt otool -arch arm64 -L TestClass otool -arch arm64 -v -s __DATA __objc_classlist TestClass > classlist.txt otool -arch arm64 -v -s __DATA __objc_classrefs TestClass > classrefs.txt otool -v -s __DATA __objc_selrefs TestClass > selrefs.txtParsing __objc_classlist
Read lines after the "Contents of (__DATA,__objc_classlist)" marker. When a line starts with 000000010 , capture the address; the following line containing name provides the class name. Store as {address: className} .
static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQueryClassList = @"__objc_classlist";
- (NSMutableDictionary *)classListFromContent:(NSString *)content {
NSArray *lines = [content componentsSeparatedByString:@"\n"];
BOOL canAddName = NO;
NSMutableDictionary *classListResults = [NSMutableDictionary dictionary];
NSString *addressStr = @"";
BOOL classListBegin = NO;
for (NSString *line in lines) {
if ([line containsString:kConstPrefix] && [line containsString:kQueryClassList]) {
classListBegin = YES; continue;
} else if ([line containsString:kConstPrefix]) { classListBegin = NO; break; }
if (classListBegin) {
if ([line containsString:@"000000010"]) {
NSArray *components = [line componentsSeparatedByString:@" "];
addressStr = [components lastObject];
canAddName = YES;
} else if (canAddName && [line containsString:@"name"]) {
NSArray *components = [line componentsSeparatedByString:@" "];
NSString *className = [components lastObject];
[classListResults setValue:className forKey:addressStr];
addressStr = @""; canAddName = NO;
}
}
}
return classListResults;
}Parsing __objc_classrefs
Similar logic, but keep only addresses that start with 0x100 (real class addresses).
static NSString *kQueryClassRefs = @"__objc_classrefs";
- (NSArray *)classRefsFromContent:(NSString *)content {
NSArray *lines = [content componentsSeparatedByString:@"\n"];
NSMutableArray *classRefsResults = [NSMutableArray array];
BOOL classRefsBegin = NO;
for (NSString *line in lines) {
if ([line containsString:kConstPrefix] && [line containsString:kQueryClassRefs]) { classRefsBegin = YES; continue; }
else if (classRefsBegin && [line containsString:kConstPrefix]) { classRefsBegin = NO; break; }
if (classRefsBegin && [line containsString:@"000000010"]) {
NSArray *components = [line componentsSeparatedByString:@" "];
NSString *address = [components lastObject];
if ([address hasPrefix:@"0x100"]) { [classRefsResults addObject:address]; }
}
}
return classRefsResults;
}Parsing __objc_selrefs (used selectors)
- (NSMutableDictionary *)selRefsFromContent:(NSString *)content {
NSArray *lines = [content componentsSeparatedByString:@"\n"];
NSMutableDictionary *selRefsResults = [NSMutableDictionary dictionary];
BOOL selRefsBegin = NO;
for (NSString *line in lines) {
if ([line containsString:kConstPrefix] && [line containsString:@"__objc_selrefs"]) { selRefsBegin = YES; continue; }
else if (selRefsBegin && [line containsString:kConstPrefix]) { selRefsBegin = NO; break; }
if (selRefsBegin) {
NSArray *components = [line componentsSeparatedByString:@" "];
if (components.count > 2) {
NSString *methodAddress = components[components.count-2];
NSString *methodName = [components lastObject];
selRefsResults[methodAddress] = methodName;
}
}
}
return selRefsResults;
}Parsing all methods from __objc_classlist
After each data block, the next name line gives the class name. When a line contains "Methods" (BaseMethods, InstanceMethods, ClassMethods), subsequent name lines contain method names together with their addresses. Store as {className:{address:methodName}} .
- (NSMutableDictionary *)allSelRefsFromContent:(NSString *)content {
NSArray *lines = [content componentsSeparatedByString:@"\n"];
NSMutableDictionary *allSelResults = [NSMutableDictionary dictionary];
BOOL allSelBegin = NO;
BOOL canAddName = NO;
BOOL canAddMethods = NO;
NSString *className = @"";
NSMutableDictionary *methodDic = [NSMutableDictionary dictionary];
for (NSString *line in lines) {
if ([line containsString:kConstPrefix] && [line containsString:kQueryClassList]) { allSelBegin = YES; continue; }
else if (allSelBegin && [line containsString:kConstPrefix]) { break; }
if (allSelBegin) {
if ([line containsString:@"data"]) {
if (methodDic.count) { allSelResults[className] = methodDic; methodDic = [NSMutableDictionary dictionary]; }
canAddName = YES; canAddMethods = NO; continue;
}
if (canAddName && [line containsString:@"name"]) {
NSArray *components = [line componentsSeparatedByString:@" "];
className = [components lastObject];
continue;
}
if ([line containsString:@"methods"] || [line containsString:@"Methods"]) {
canAddName = NO; canAddMethods = YES; continue;
}
if (canAddMethods && [line containsString:@"name"]) {
NSArray *components = [line componentsSeparatedByString:@" "];
if (components.count > 2) {
NSString *methodAddress = components[components.count-2];
NSString *methodName = [components lastObject];
methodDic[methodAddress] = methodName;
}
continue;
}
}
}
return allSelResults;
}Finding Unused Classes
Compute the set difference between all classes (address→name) and referenced classes (addresses). Remove the referenced addresses from the class dictionary; the remaining entries are dead classes.
NSDictionary *classListDic = [self classListFromContent:content];
NSArray *classRefs = [self classRefsFromContent:content];
NSMutableSet *refsSet = [NSMutableSet setWithArray:classRefs];
for (NSString *addr in refsSet) { [classListDic setValue:nil forKey:addr]; }
NSLog(@"Unused classes: %@", classListDic);Finding Unused Methods
Obtain the full method map per class and the set of used selectors, then delete used method entries from the full map. The leftover entries represent dead methods.
NSMutableDictionary *methodsListDic = [self allSelRefsFromContent:content];
NSMutableDictionary *selRefsDic = [self selRefsFromContent:content];
for (NSString *addr in selRefsDic.allKeys) {
for (NSMutableDictionary *methodDic in methodsListDic.allValues) {
methodDic[addr] = nil;
}
}
NSMutableDictionary *resultDic = [NSMutableDictionary dictionary];
for (NSString *cls in methodsListDic) {
NSDictionary *md = methodsListDic[cls];
if (md.count) resultDic[cls] = md;
}
NSLog(@"Unused methods: %@", resultDic);Conclusion
The provided Objective‑C project (OtoolAnalyse) automates the extraction of unused classes and methods from an iOS binary. Before deleting any identified code, developers should manually verify that the classes are not referenced by storyboards, XIBs, or system configuration files. Future improvements may include automatic filtering of system classes, superclass handling, and more sophisticated UI‑resource analysis.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
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.