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 can be obtained by reversing the Mach-O file's __DATA.__objc_classlist and __DATA.__objc_classrefs sections using otool to get all OC classes and referenced classes. The difference between the two sets is the set of unused classes, combined with nm -nm to get the addresses and corresponding class names symbolizing the unused class names
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 may 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 is it used in conjunction 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: the simple use 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 mainly consist of 3 parts:
- Mach Header: Describes the CPU architecture, file type, loading commands, and other information of 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, used to hold data and code.
Here are some common sections in Data, from Exploration of 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, where all pointers initially point to __stub_helper |
__DATA.nl_symbol_ptr | Pointer table for non-lazy binding, where each pointer in the table points to a symbol resolved during loading |
__DATA.__const | Uninitialized constants |
__DATA.__cfstring | Core Foundation strings (CFStringRefs) used in the program |
__DATA.__bss | BSS, stores 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 Use of otool Command#
For example, if the project name is TestClass, enter the folder where the TestClass exec is located.
-
- otool symbol formatting, output the class structure and defined methods of the project.
// View directly in the command line
otool -arch arm64 -ov TestClass
// Or output the corresponding information to a specified file, for example, export to otool.txt
otool -arch arm64 -ov TestClass > otool.txt
-
- Check which libraries are linked.
otool -L TestClass
-
- Filter whether a specific library is linked, such as CoreFoundation.
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, for example, 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, for example, 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, for example, 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
Up to this point, what is otool? What is __DATA.__objc_classlist
? Where does it come from? How is it used in conjunction with the otool command? These questions have been resolved. But next, how to obtain the difference? How to use regular expressions? How to solve this?
The article iOS Code Size Reduction Practice: Deleting Unused Classes implements the process using Python code. However, I took a different route, and I hope to share it here for everyone's 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, this posed a challenge. I compared multiple class structures to summarize a fixed pattern.
Referring to the symbolMapFromContent
method implementation of 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;
}
Thus, I realized that by following the same logic of reading line by line and using flags, I could also apply the same logic. Each time a line starting with 000000010
appears, it indicates the start of a new class, storing the corresponding address, setting a flag to store the name, and when reading the name, storing it in the format { classAddress: className }
, clearing the flag until the next line contains 000000010
, at which point resetting the flag to YES. The code is as follows:
static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQueryClassList = @"__objc_classlist";
// Get the class list
- (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 as follows, total %ld\n%@:", classListResults.count, classListResults);
return classListResults;
}
Then 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 set of logic. So, I decided to modify the internal implementation of LinkMap.
First, I commented out the checkContent: judgment in the analyze:
method and changed the call to symbolMapFromContent:
to classListFromContent:
. I set a breakpoint to check whether the classListFromContent:
method is correct. How to determine if this method is correct? The simplest way is to compare the count of NSMutableDictionary data returned by classListFromContent:
with the count of 000000010
in the Contents of (__DATA_CONST,__objc_classlist) section
of the otool.txt file. The specific steps are as follows:
-
- Remove all content from the
otool.txt
file except for theContents of (__DATA_CONST,__objc_classlist) section
, then search for000000010
to see how many there are.
- Remove all content from the
-
- Run the LinkMap project, select otool.txt, and set a breakpoint to check the output of the
classListFromContent:
method.
- Run the LinkMap project, select otool.txt, and set a breakpoint to check the output of the
-
- If the counts of the two results 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 code above. In the single line information, the latter part is either system information or class addresses. As shown:
Therefore, 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 as follows, 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 are the same, it indicates that 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 storage format for all classes is { classAddress: className }
, and for referenced classes, it is [classAddress]
. After deduplication, I traversed the deduplicated referenced classes and removed all addresses from all classes. Finally, 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 involved in refsSet
// The remaining are redundant classes
for (NSString *address in refsSet.allObjects) {
[classListDic setValue:nil forKey:address];
}
// Remove system classes, such as SceneDelegate or classes in Storyboard
NSLog(@"The redundant classes are as follows: %@", classListDic);
Finally, the test output results are as follows, where you can see the output structure. However, ViewController is referenced by Storyboard, and SceneDelegate 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 code for obtaining differences.
Analyzing Unused Methods#
The analysis of unused methods is slightly different from that of 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 in this section is relatively simple. The front part is the address, and the back part is the method name. Here, we can traverse each line of data and store it directly 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 as follows, total %ld\n%@:", selRefsResults.count, selRefsResults);
return selRefsResults;
}
Obtaining All Method Lists
This part is a bit tricky. I want to use the data from BaseMethods
, InstanceMethods
, and ClassMethods
in __objc_classlist
as the collection of all methods. So, let's take a 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
...
What rules can be derived from the above file? It’s quite a headache. I want to obtain the data from the name line after BaseMethods
, and I also hope to associate this method with the class name for easier lookup later.
The rules I summarized are as follows:
-
- Following the line-by-line reading logic, when I read
data
, I then readname
, wherename
is the class name.
- Following the line-by-line reading logic, when I read
-
- Then I read down to
baseMethods
,InstanceMethods
, orClass Methods
, and then readname
, wherename
contains the method name and method address.
- Then I read down to
-
- Then I read down to
data
, repeating step 1.
- Then I read down to
The implementation of this logic in code is to set two flags, one for marking the class name and one for marking 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 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 storage format for all methods is { className:{ address: methodName } }
, and for referenced methods, it is { methodAddress: methodName }
. I traversed the deduplicated referenced methods and removed all addresses from all methods. Finally, 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(@"The redundant methods are as follows: %@", resultDic);
Finally, the test output results are as follows, where you can see the output structure. However, the delegate methods of AppDelegate and SceneDelegate are identified as redundant methods. Therefore, it is important to confirm before deletion. You can also filter specified delegate methods in the above code for obtaining differences.
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 for improvement 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 described above. I have shared my experience, hoping to provide some insights. 😄
References#
- Using otool to analyze potentially 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
- Exploration of Mach-O File Format
- Common Commands for Binary File Analysis
- iOS Code Size Reduction Practice: Deleting Unused Classes