今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

Analysis of Excess Classes and Methods in iOS Mach-O Files

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

HeaderHeader
SectionPurpose
__TEXT.__textMain program code
__TEXT.__cstringC language strings
__TEXT.__constConstants modified by the const keyword
__TEXT.__stubsPlaceholder code for Stubs, often referred to as stub code.
__TEXT.__stubs_helperFinal pointer when the Stub cannot find the actual symbol address
__TEXT.__objc_methnameObjective-C method names
__TEXT.__objc_methtypeObjective-C method types
__TEXT.__objc_classnameObjective-C class names
__DATA.__dataInitialized mutable data
__DATA.__la_symbol_ptrPointer table for lazy binding, with pointers initially pointing to __stub_helper
__DATA.nl_symbol_ptrPointer table for non-lazy binding, with each pointer pointing to a symbol resolved during loading
__DATA.__constUninitialized constants
__DATA.__cfstringCore Foundation strings (CFStringRefs) used in the program
__DATA.__bssBSS, storing uninitialized global variables, commonly referred to as static memory allocation
__DATA.__commonUninitialized symbol declarations
__DATA.__objc_classlistObjective-C class list
__DATA.__objc_protolistObjective-C prototypes
__DATA.__objc_imginfoObjective-C image information
__DATA.__objc_selrefsObjective-C method references
__DATA.__objc_protorefsObjective-C prototype references
__DATA.__objc_superrefsObjective-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.

    1. 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

    1. Check which libraries are linked

otool -L TestClass

    1. Filter to see if a specific library, such as CoreFoundation, is linked

otool -L TestClass | grep CoreFoundation

    1. 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

    1. 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

    1. 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

    1. 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 or Contents 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 or Contents 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.

HeaderHeader
SectionPurpose
__DATA.__objc_classlistObjective-C class list
__DATA.__objc_classrefsObjective-C class references
__DATA.__objc_superrefsObjective-C superclass references
__DATA.__objc_catlistObjective-C category list
__DATA.__objc_protolistObjective-C prototypes
__DATA.__objc_selrefsObjective-C method references
__DATA.__objc_imginfoObjective-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.

WX20210511-182718.png

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:

    1. Remove all content from the otool.txt file except for the Contents of (__DATA_CONST,__objc_classlist) section, then search for 000000010 to see how many there are.
    1. Run the LinkMap project, select otool.txt, and set a breakpoint to check the output of the classListFromContent: method.
    1. 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:

WX20210511-194536.png

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.

WX20210512-084919.png

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.

WX20210512-103154.png

The rules I summarized are as follows:

    1. Following a line-by-line reading logic, when I read data, the first name I read is the class name.
    1. Then I continue reading, and when I read baseMethods, InstanceMethods, or Class Methods, the name I read next is the method name and method address.
    1. Then I continue reading, and when I read data, I repeat step 1.

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.

WX20210512-101907.png

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#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.