Mach-O File Analysis of Redundant Classes and Methods.md#
Background#
Recently, while optimizing package size and project code, one of the processes involved analyzing Mach-O files. Many articles online suggest using otool to analyze Mach-O files to obtain __objc_classrefs, __objc_classlist, etc., and then identify unused classes and methods.
For example: Unused classes are obtained by reversing the Mach-O file's __DATA.__objc_classlist and __DATA.__objc_classrefs sections using otool, with the difference between the two sets being the set of unused classes. Combining with nm -nm gives the addresses and corresponding class names symbolized for unused classes
from Practical Tips! JD Mall iOS App Size Reduction
Or By combining the __TEXT.__text of the LinkMap file and using a regular expression ([+|-][.+\s(.+)]) we can extract all objc class methods and instance methods (SelectorsAll) in the current executable file. Then, using the otool command otool -v -s __DATA __objc_selrefs to reverse the __DATA.__objc_selrefs section, we can extract the method names referenced in the executable file (UsedSelectorsAll), allowing us to roughly analyze which methods in SelectorsAll are not referenced (SelectorsAll-UsedSelectorsAll)
from iOS WeChat Installation Package Size Reduction
Those statements seem simple, but I encountered many difficulties during the operation. First, what is otool? Then, what is __DATA.__objc_classlist? Where does it come from? How to use it with the otool command? How to obtain the difference? How to use regular expressions, etc.? Without guidance from experts, I could only navigate through it step by step.
So, over the past few days, I practiced on my own and created a tool similar to LinkMap for analysis—OtoolAnalyse. I would like to share the specific implementation process and principles.
It mainly involves two parts: simple usage of the otool command and the implementation principles of OtoolAnalyse.
Principles#
First, let's look at what Mach-O
is. Mach-O
is short for Mach Object
, a file format that records executable files, object code, shared libraries, dynamically loaded code, and memory dumps.
Mach-O files consist of three main parts:
- Mach Header: Describes the CPU architecture, file type, loading commands, and other information of the Mach-O.
- Load Command: Describes the specific organizational structure of data in the file, with different data types represented by different loading commands.
- Data: Each segment's data is stored here, which is used to hold data and code.
Here are some common sections in Data, from Exploring Mach-O File Format
Header | Header |
---|---|
Section | Purpose |
__TEXT.__text | Main program code |
__TEXT.__cstring | C language strings |
__TEXT.__const | Constants modified by the const keyword |
__TEXT.__stubs | Placeholder code for Stubs, often referred to as stub code. |
__TEXT.__stubs_helper | Final pointer when the Stub cannot find the actual symbol address |
__TEXT.__objc_methname | Objective-C method names |
__TEXT.__objc_methtype | Objective-C method types |
__TEXT.__objc_classname | Objective-C class names |
__DATA.__data | Initialized mutable data |
__DATA.__la_symbol_ptr | Pointer table for lazy binding, with pointers initially pointing to __stub_helper |
__DATA.nl_symbol_ptr | Pointer table for non-lazy binding, with each pointer pointing to a symbol resolved during loading |
__DATA.__const | Uninitialized constants |
__DATA.__cfstring | Core Foundation strings (CFStringRefs) used in the program |
__DATA.__bss | BSS, storing uninitialized global variables, commonly referred to as static memory allocation |
__DATA.__common | Uninitialized symbol declarations |
__DATA.__objc_classlist | Objective-C class list |
__DATA.__objc_protolist | Objective-C prototypes |
__DATA.__objc_imginfo | Objective-C image information |
__DATA.__objc_selrefs | Objective-C method references |
__DATA.__objc_protorefs | Objective-C prototype references |
__DATA.__objc_superrefs | Objective-C superclass references |
Implementation#
Obtaining Mach-O files: Change the suffix of the Xcode packaged IPA to .zip, then unzip it to get the payload folder, which contains xxx.app. Right-click to show package contents, where the xxx exec file is the Mach-O file.
Simple Usage of otool Command#
For example, if the project name is TestClass, navigate to the folder where the TestClass exec is located.
-
- otool symbol formatting, outputting the project's class structure and defined methods
// View directly in the command line
otool -arch arm64 -ov TestClass
// Or output the corresponding information to a specified file, e.g., export to otool.txt
otool -arch arm64 -ov TestClass > otool.txt
-
- Check which libraries are linked
otool -L TestClass
-
- Filter to see if a specific library, such as CoreFoundation, is linked
otool -L TestClass | grep CoreFoundation
-
- View all class collections in Mach-O
// View directly in the command line
otool -arch arm64 -v -s __DATA __objc_classlist TestClass
// Or output the corresponding information to a specified file, e.g., export to classlist.txt
otool -arch arm64 -v -s __DATA __objc_classlist TestClass > classlist.txt
-
- View all used class collections in Mach-O
// View directly in the command line
otool -arch arm64 -v -s __DATA __objc_classrefs TestClass
// Or output the corresponding information to a specified file, e.g., export to classrefs.txt
otool -arch arm64 -v -s __DATA __objc_classrefs TestClass > classrefs.txt
-
- View all used method collections in Mach-O
// View directly in the command line
otool -arch arm64 -v -s __DATA __objc_selrefs TestClass
// Or output the corresponding information to a specified file, e.g., export to selrefs.txt
otool -arch arm64 -v -s __DATA __objc_selrefs TestClass > selrefs.txt
-
- View C language strings
otool -v -s __TEXT __cstring TestClass
At this point, what is otool? What is __DATA.__objc_classlist? Where does it come from? How to use it with the otool command? These questions have been resolved. But the next questions, how to obtain the difference? How to use regular expressions? How to solve them?
The article iOS Code Size Reduction Practice: Deleting Unused Classes implements the process using Python code. However, I took a different approach, and I would like to share it here, hoping for your guidance.
Implementation Principles of OtoolAnalyse#
First, refer to the otool command otool -arch arm64 -ov TestClass > otool.txt
to generate otool.txt.
Open otool.txt and search for Contents of (__DATA
, and you will find:
Contents of (__DATA_CONST,__objc_classlist) section
orContents of (__DATA,__objc_classlist) section
Contents of (__DATA,__objc_classrefs) section
Contents of (__DATA,__objc_superrefs) section
Contents of (__DATA,__objc_catlist) section
Contents of (__DATA_CONST,__objc_protolist) section
orContents of (__DATA,__objc_protolist) section
Contents of (__DATA,__objc_selrefs) section
Contents of (__DATA_CONST,__objc_imageinfo) section
By combining with the table below, you can understand what each section represents.
Header | Header |
---|---|
Section | Purpose |
__DATA.__objc_classlist | Objective-C class list |
__DATA.__objc_classrefs | Objective-C class references |
__DATA.__objc_superrefs | Objective-C superclass references |
__DATA.__objc_catlist | Objective-C category list |
__DATA.__objc_protolist | Objective-C prototypes |
__DATA.__objc_selrefs | Objective-C method references |
__DATA.__objc_imginfo | Objective-C image information |
Analyzing Unused Classes#
Obtaining __objc_classlist
Let's look at the section where __objc_classlist
is located.
0000000100008028 0x10000d450 // The address 0x10000d450 is the unique address of the class
isa 0x10000d478
superclass 0x0 _OBJC_CLASS_$_UIViewController // Parent class
cache 0x0 __objc_empty_cache
vtable 0x0
data 0x10000c0b8
flags 0x90
instanceStart 8
instanceSize 8
reserved 0x0
ivarLayout 0x0
name 0x1000073cd SecondViewController // Class name
baseMethods 0x1000064f0
entsize 12 (relative)
count 1
name 0x6ed8 (0x10000d3d0 extends past end of file)
types 0xf6a (0x100007466 extends past end of file)
imp 0xfffffbb8 (0x1000060b8 extends past end of file)
baseProtocols 0x0
ivars 0x0
weakIvarLayout 0x0
baseProperties 0x0
Here, we can see from the structure of a single class's information that it contains the class's address, class name, and parent class's address. I want to obtain class information through fixed code and store it in a dictionary until the __objc_classlist
section ends, thus obtaining all class names and addresses.
How to do this? Since the file is not in a fixed JSON format, it posed a challenge. I compared multiple class structures and hoped to summarize a fixed pattern.
Referring to the symbolMapFromContent method implementation from the LinkMap project, I found that its matching reads the file line by line, sets flags, and parses the corresponding information. The code is as follows:
- (NSMutableDictionary *)symbolMapFromContent:(NSString *)content {
NSMutableDictionary <NSString *,SymbolModel *>*symbolMap = [NSMutableDictionary new];
// Symbol file list
NSArray *lines = [content componentsSeparatedByString:@"\n"];
BOOL reachFiles = NO;
BOOL reachSymbols = NO;
BOOL reachSections = NO;
for(NSString *line in lines) {
if([line hasPrefix:@"#"]) {
if([line hasPrefix:@"# Object files:"])
reachFiles = YES;
else if ([line hasPrefix:@"# Sections:"])
reachSections = YES;
else if ([line hasPrefix:@"# Symbols:"])
reachSymbols = YES;
} else {
if(reachFiles == YES && reachSections == NO && reachSymbols == NO) {
NSRange range = [line rangeOfString:@"]"];
if(range.location != NSNotFound) {
SymbolModel *symbol = [SymbolModel new];
symbol.file = [line substringFromIndex:range.location+1];
NSString *key = [line substringToIndex:range.location+1];
symbolMap[key] = symbol;
}
} else if (reachFiles == YES && reachSections == YES && reachSymbols == YES) {
NSArray <NSString *>*symbolsArray = [line componentsSeparatedByString:@"\t"];
if(symbolsArray.count == 3) {
NSString *fileKeyAndName = symbolsArray[2];
NSUInteger size = strtoul([symbolsArray[1] UTF8String], nil, 16);
NSRange range = [fileKeyAndName rangeOfString:@"]"];
if(range.location != NSNotFound) {
NSString *key = [fileKeyAndName substringToIndex:range.location+1];
SymbolModel *symbol = symbolMap[key];
if(symbol) {
symbol.size += size;
}
}
}
}
}
}
return symbolMap;
}
Therefore, I realized that by following the same logic of reading line by line + flagging, I could use the same logic, i.e., each time a line starting with 000000010
indicates the start of a new class, store the corresponding address, set the flag to store the name, and when reading the name, store it in the format { classAddress: className }
, clearing the flag until the next line contains 000000010
, at which point reset the flag to YES. The code is as follows:
static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQueryClassList = @"__objc_classlist";
// Get classes from classList
- (NSMutableDictionary *)classListFromContent:(NSString *)content {
// Symbol file list
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:@" "];
NSString *address = [components lastObject];
addressStr = address;
canAddName = YES;
}
else {
if (canAddName && [line containsString:@"name"]) {
NSArray *components = [line componentsSeparatedByString:@" "];
NSString *className = [components lastObject];
[classListResults setValue:className forKey:addressStr];
addressStr = @"";
canAddName = NO;
}
}
}
}
NSLog(@"__objc_classlist summary: total %ld\n%@:", classListResults.count, classListResults);
return classListResults;
}
Now, how to debug whether this code is correct?
At this point, I thought of leveraging the UI of LinkMap since it also requires file selection and reading, and I also wanted to display the analysis results and output the final results to a file, a complete logic. Therefore, I considered modifying the internal implementation of LinkMap.
First, I commented out the checkContent: judgment, then in the analyze: method, I changed the call from symbolMapFromContent: to classListFromContent:, set a breakpoint to see if the classListFromContent: method was correct. How to determine if this method is correct? The simplest way is to compare the count of NSMutableDictionary returned by classListFromContent: with the count of 000000010
in the Contents of (__DATA_CONST,__objc_classlist) section
of the otool.txt file. If they match, the code is considered correct. The specific steps are as follows:
-
- Remove all content from the otool.txt file except for the
Contents of (__DATA_CONST,__objc_classlist) section
, then search for000000010
to see how many there are.
- Remove all content from the otool.txt file except for the
-
- Run the LinkMap project, select otool.txt, and set a breakpoint to check the output of the classListFromContent: method.
-
- If the counts match, I consider the code to be running correctly.
Obtaining __objc_classrefs
Let's look at the section where __objc_classrefs
is located.
Contents of (__DATA,__objc_classrefs) section
000000010000d410 0x0 _OBJC_CLASS_$_UIColor
000000010000d418 0x10000d450
000000010000d420 0x0 _OBJC_CLASS_$_UISceneConfiguration
000000010000d428 0x10000d568
Similarly, let's analyze the above code. In the single line information, the latter part is either system information or class addresses. As shown in the image:
Thus, I adopted the same processing logic, reading the contents of the Contents of (__DATA,__objc_classrefs) section
line by line, determining if it contains 0x100
, indicating a class address, and storing it in an array. The implementation is as follows:
static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQueryClassRefs = @"__objc_classrefs";
// Get class references
- (NSArray *)classRefsFromContent:(NSString *)content {
// Symbol file list
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]; }
}
}
NSLog(@"\n\n__objc_refs summary: total %ld\n%@:", classRefsResults.count, classRefsResults);
return classRefsResults;
}
Then, to verify the correctness of the above method, I removed all content except for the Contents of (__DATA,__objc_classrefs) section
, then searched for the count of 0x100
, and compared it with the count returned by the classRefsFromContent: method. If they match, it indicates the method is correct.
Obtaining the Difference to Identify Unused Classes
In the analyze:
method of LinkMap, I called classListFromContent: and classRefsFromContent: to obtain all classes and referenced classes. The class storage is { classAddress: className }
, and the referenced classes are stored in [classAddress]
. After deduplication, I traversed the deduplicated referenced classes and removed all addresses from the class list. The remaining classes are the unused classes. The code is as follows:
// All classList classes and class names
NSDictionary *classListDic = [self classListFromContent:content];
// All referenced classes
NSArray *classRefs = [self classRefsFromContent:content];
// // All referenced parent classes
// NSArray *superRefs = [self superRefsFromContent:content];
// First deduplicate the class and parent class arrays
NSMutableSet *refsSet = [NSMutableSet setWithArray:classRefs];
// [refsSet addObjectsFromArray:superRefs];
// All in refsSet are used, traverse classList and remove classes in refsSet
// The remaining classes are the redundant classes
for (NSString *address in refsSet.allObjects) {
[classListDic setValue:nil forKey:address];
}
// Remove system classes, such as SceneDelegate or classes in Storyboard
NSLog(@"Redundant classes are as follows: %@", classListDic);
Finally, the test output results are as follows, where you can see that the output structure includes ViewController, which is referenced by Storyboard, and SceneDelegate, which is configured in the Info.plist file, but both are identified as unused classes. Therefore, it is important to confirm before deletion. You can also filter specified classes in the above difference obtaining code.
Analyzing Unused Methods#
The analysis of unused methods is slightly different from classes because there is no direct way to obtain all methods. The __objc_selrefs
section contains all referenced methods. Therefore, I thought of using the data from BaseMethods, InstanceMethods, and ClassMethods in __objc_classlist
as the collection of all methods, and then performing a difference with the referenced methods to finally obtain the unused methods.
Obtaining __objc_selrefs
Let's look at the section where __objc_selrefs
is located.
Contents of (__DATA,__objc_selrefs) section
0x100006647 Tapped:
0x1000067e5 application:didFinishLaunchingWithOptions:
0x1000070f9 application:configurationForConnectingSceneSession:options:
0x100007135 application:didDiscardSceneSessions:
0x10000717d scene:willConnectToSession:options:
0x1000071a1 sceneDidDisconnect:
0x1000071b5 sceneDidBecomeActive:
0x1000071cb sceneWillResignActive:
0x1000071e2 sceneWillEnterForeground:
0x1000071fc sceneDidEnterBackground:
0x10000715a window
0x100007161 setWindow:
0x10000739d .cxx_destruct
0x1000065e4 viewDidLoad
0x1000065f0 purpleColor
0x1000065fc view
0x100006601 setBackgroundColor:
0x100006615 navigationController
0x10000662a pushViewController:animated:
0x10000664f role
0x100006654 initWithName:sessionRole:
As you can see, the data here is relatively simple; the front part is the address, and the back part is the method name. Here, we traverse each line of data and directly store it in the format { methodAddress: methodName }
. The code is as follows:
static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQuerySelRefs = @"__objc_selrefs";
// Get the set of used methods
- (NSMutableDictionary *)selRefsFromContent:(NSString *)content {
// Symbol file list
NSArray *lines = [content componentsSeparatedByString:@"\n"];
NSMutableDictionary *selRefsResults = [NSMutableDictionary dictionary];
BOOL selRefsBegin = NO;
for(NSString *line in lines) {
if ([line containsString:kConstPrefix] && [line containsString:kQuerySelRefs]) {
selRefsBegin = YES;
continue;;
}
else if (selRefsBegin && [line containsString:kConstPrefix]) {
selRefsBegin = NO;
break;
}
if(selRefsBegin) {
NSArray *components = [line componentsSeparatedByString:@" "];
if (components.count > 2) {
NSString *methodName = [components lastObject];
NSString *methodAddress = components[components.count - 2];
[selRefsResults setValue:methodName forKey:methodAddress];
}
}
}
NSLog(@"\n\n__objc_selrefs summary: total %ld\n%@:", selRefsResults.count, selRefsResults);
return selRefsResults;
}
Obtaining the Complete Method List
This part is slightly more complicated. I wanted to use the data from BaseMethods, InstanceMethods, and ClassMethods in __objc_classlist
as the collection of all methods. So, let's first look at the file structure and summarize the rules.
00000001007c1c20 0x100935c98
isa 0x100935c70
superclass 0x0 _OBJC_CLASS_$_NSObject
cache 0x0 __objc_empty_cache
vtable 0x0
data 0x1007c4fc8
flags 0x90
instanceStart 8
instanceSize 8
reserved 0x0
ivarLayout 0x0
name 0x1006fb54a ColorManager
baseMethods 0x0
baseProtocols 0x0
ivars 0x0
weakIvarLayout 0x0
baseProperties 0x0
Meta Class
isa 0x0 _OBJC_METACLASS_$_NSObject
superclass 0x0 _OBJC_METACLASS_$_NSObject
cache 0x0 __objc_empty_cache
vtable 0x0
data 0x1007c4f80
flags 0x91 RO_META
instanceStart 40
instanceSize 40
reserved 0x0
ivarLayout 0x0
name 0x1006fb54a ColorManager
baseMethods 0x1007c4f18
entsize 24
count 4
name 0x100689e19 primaryTextColor
types 0x1007038cd @16@0:8
imp 0x100004810
name 0x100689e2a secondaryTextColor
types 0x1007038cd @16@0:8
imp 0x10000482c
name 0x100689e3d primaryTintColor
types 0x1007038cd @16@0:8
imp 0x100004848
name 0x100689e4e backgroundColor
types 0x1007038cd @16@0:8
imp 0x100004878
baseProtocols 0x0
ivars 0x0
weakIvarLayout 0x0
baseProperties 0x0
00000001007c1c28 0x100935ce8
isa 0x100935cc0
superclass 0x0 _OBJC_CLASS_$_NSObject
cache 0x0 __objc_empty_cache
vtable 0x0
data 0x1007c5648
flags 0x194 RO_HAS_CXX_STRUCTORS
instanceStart 8
instanceSize 152
reserved 0x0
ivarLayout 0x1006fb56a
layout map 0x15 0x21 0x12
name 0x1006fb55a SectionModel
baseMethods 0x1007c5078
entsize 24
count 31
name 0x100689eac groupName
types 0x1007038cd @16@0:8
imp 0x100004948
name 0x100689eb6 setGroupName:
types 0x1007038d5 v24@0:8@16
imp 0x100004954
name 0x100689ec4 name
types 0x1007038cd @16@0:8
imp 0x10000495c
name 0x100689ec9 setName:
types 0x1007038d5 v24@0:8@16
imp 0x100004968
name 0x100689ed2 menuId
types 0x1007038cd @16@0:8
imp 0x100004970
name 0x100689ed9 setMenuId:
types 0x1007038d5 v24@0:8@16
...
From the above file, what rules can be observed? It’s quite complex, and I want to obtain the data from the name line after BaseMethods, and I also want to associate this method with the class for easier lookup in the final output.
The rules I summarized are as follows:
-
- Following a line-by-line reading logic, when I read
data
, the first name I read is the class name.
- Following a line-by-line reading logic, when I read
-
- Then I continue reading, and when I read
baseMethods
,InstanceMethods
, orClass Methods
, the name I read next is the method name and method address.
- Then I continue reading, and when I read
-
- Then I continue reading, and when I read
data
, I repeat step 1.
- Then I continue reading, and when I read
The implementation in code logic is to set two flags, one for the class name and one for the method; when I read data
, I set the first flag to YES, and when I read name
with the first flag set to YES, I update the class name. When I read a line containing Methods, I set the first flag to NO and the second flag to YES, and when I read name
with the second flag set to YES, I store the method name and method address. The final data is stored in the format { className:{ address: methodName } }
. The code is as follows:
static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQueryClassList = @"__objc_classlist";
// Get all method collections { className:{ address: methodName } }
- (NSMutableDictionary *)allSelRefsFromContent:(NSString *)content {
// Symbol file list
NSArray *lines = [content componentsSeparatedByString:@"\n"];
NSMutableDictionary *allSelResults = [NSMutableDictionary dictionary];
BOOL allSelResultsBegin = 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]) {
allSelResultsBegin = YES;
continue;
}
else if (allSelResultsBegin && [line containsString:kConstPrefix]) {
allSelResultsBegin = NO;
break;
}
if (allSelResultsBegin) {
if ([line containsString:@"data"]) {
if (methodDic.count > 0) {
[allSelResults setValue:methodDic forKey:className];
methodDic = [NSMutableDictionary dictionary];
}
// The first name after data is the class name
canAddName = YES;
canAddMethods = NO;
continue;
}
if (canAddName && [line containsString:@"name"]) {
// Update the class name for the format { className:{ address: methodName } }
NSArray *components = [line componentsSeparatedByString:@" "];
className = [components lastObject];
continue;
}
if ([line containsString:@"methods"] || [line containsString:@"Methods"]) {
// The name after methods is the method name and method address
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 setValue:methodName forKey:methodAddress];
}
continue;
}
}
}
return allSelResults;
}
Obtaining the Difference to Identify Unused Methods
In the analyze:
method of LinkMap, I called allSelRefsFromContent: and selRefsFromContent: to obtain all methods and referenced methods. The method storage is { className:{ address: methodName } }
, and the referenced methods are stored in { methodAddress: methodName }
. I traversed the deduplicated referenced methods and removed all addresses from the method list. The remaining methods are the unused methods. The code is as follows:
NSMutableDictionary *methodsListDic = [self allSelRefsFromContent:content];
NSMutableDictionary *selRefsDic = [self selRefsFromContent:content];
// Traverse selRefs to remove from methodsListDic, the remaining are unused
for (NSString *methodAddress in selRefsDic.allKeys) {
for (NSDictionary *methodDic in methodsListDic.allValues) {
[methodDic setValue:nil forKey:methodAddress];
}
}
// Traverse to remove empty elements
NSMutableDictionary *resultDic = [NSMutableDictionary dictionary];
for (NSString *classNameStr in methodsListDic.allKeys) {
NSDictionary *methodDic = [methodsListDic valueForKey:classNameStr];
if (methodDic.count > 0) {
[resultDic setValue:methodDic forKey:classNameStr];
}
}
NSLog(@"Redundant methods are as follows: %@", resultDic);
Finally, the test output results are as follows, where you can see the output structure, including AppDelegate and SceneDelegate's delegate methods, which are identified as redundant methods. Therefore, it is important to confirm before deletion. You can also filter specified delegate methods in the above difference obtaining code.
Conclusion#
The complete project address is OtoolAnalyse. Using this method, I analyzed the unused classes and methods in the project, and it is important to confirm before deletion. There are still areas to improve in the project, such as filtering system methods and judging base classes, etc., which will be supplemented later. But the overall analysis logic is as above; I have shared it after crossing the river, 😄.
References#
- otool tool analysis of possibly unused Objective-C classes
- LinkMap
- iOS Optimization | In-depth Understanding of Link Map File
- iOS Stack Information Parsing (Mach-O)
- Package Size: Size Reduction
- Mach-O Learning
- Exploring Mach-O File Format
- Common Commands for Binary File Analysis
- iOS Code Size Reduction Practice: Deleting Unused Classes