今是昨非

今是昨非

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

iOS mach-o檔案分析多餘的類和方法

mach-o 檔案分析多餘的類和方法.md#

背景#

最近做包大小優化,在做專案程式碼優化時,其中有一個過程是分析 Mach-O 檔案,看網上很多文章都說通過 otool 分析 Mach-O,獲取__objc_classrefs、__objc_classlist 等,然後找出無用類和無用方法。

比如:無用類通過 otool 逆向Mach-O檔案 __DATA.__objc_classlist段和__DATA.__objc_classrefs 段獲取所有 OC 類和被引用的類,兩個集合差值為無用類集合,結合 nm -nm 得到地址和對應類名符號化無用類類名來自幹貨!京東商城 iOS App 瘦身實踐

又或者結合LinkMap檔案的__TEXT.__text,通過正則表達式([+|-][.+\s(.+)]),我們可以提取當前可執行檔案裡所有objc類方法和實例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可執行檔案裡引用到的方法名(UsedSelectorsAll),我們可以大致分析出SelectorsAll裡哪些方法是沒有被引用的(SelectorsAll-UsedSelectorsAll)來自iOS 微信安裝包瘦身

上面那些話看起來簡簡單單的,但是筆者操作起來確遇到了很多困難,首先 otool 是什麼?然後__DATA.__objc_classlist 是什麼?哪裡來的?怎麼跟 otool 命令結合起來使用?怎麼獲取差值?怎麼結合使用正則表達式,等等?筆者在沒有大佬帶領的情況下,只能是一步步趟過來。

於是筆者這兩天就自己小馬過河,實踐了一下,做成了一個類似LinkMap分析的工具 ——OtoolAnalyse,分享一下具體的實現過程和原理。

主要涉及到 otool 命令的簡單使用、OtoolAnalyse的實現原理兩部分。

原理#

首先來看Mach-O是什麼,Mach-OMach Object檔案格式的縮寫,是一種記錄可執行檔案、物件程式碼、共享庫、動態加載程式碼和記憶體轉儲的檔案格式。

Mach-O 檔案主要由 3 部分組成:

  • Mach Header: 描述 Mach-O 的 CPU 架構、檔案類型、加載命令等信息
  • Load Command: 描述檔案中數據等具體組織結構,不同數據類型使用不同等加載命令表示
  • Data: Data 中每一個段 (Segment) 的數據保存在此,段用來存放數據和程式碼

列舉 Data 常見的 Section,來自Mach-O 檔案格式探索

表頭表頭
Section用途
__TEXT.__text主程式碼
__TEXT.__cstringC 語言字串
__TEXT.__constconst 關鍵字修飾的常量
__TEXT.__stubs用於 Stub 的佔位程式碼,很多地方稱之為樁程式碼。
__TEXT.__stubs_helper當 Stub 無法找到真正的符號地址後的最終指向
__TEXT.__objc_methnameObjective-C 方法名稱
__TEXT.__objc_methtypeObjective-C 方法類型
__TEXT.__objc_classnameObjective-C 類名稱
__DATA.__data初始化過的可變數據
__DATA.__la_symbol_ptrlazy binding 的指標表,表中的指標一開始都指向 __stub_helper
__DATA.nl_symbol_ptr非 lazy binding 的指標表,每個表項中的指標都指向一個在裝載過程中,被動態鏈機器搜索完成的符號
__DATA.__const沒有初始化過的常量
__DATA.__cfstring程式中使用的 Core Foundation 字串(CFStringRefs)
__DATA.__bssBSS,存放為初始化的全域變數,即常說的靜態記憶體分配
__DATA.__common沒有初始化過的符號聲明
__DATA.__objc_classlistObjective-C 類列表
__DATA.__objc_protolistObjective-C 原型
__DATA.__objc_imginfoObjective-C 鏡像信息
__DATA.__objc_selrefsObjective-C 方法引用
__DATA.__objc_protorefsObjective-C 原型引用
__DATA.__objc_superrefsObjective-C 超類引用

實現#

Mach-O 檔案獲取:Xcode 打包好的 iPA,改後綴名為.zip,然後解壓縮得到 payload 資料夾,其中有 xxx.app,右鍵顯示包內容,其中有 xxx 的 exec 檔案,即是 Mach-O 檔案。

otool 命令簡單使用#

比如專案名字為 TestClass,進入 TestClass exec 所在的資料夾

    1. otool 符號格式化,輸出專案的類結構及定義的方法

// 直接在命令行查看
otool -arch arm64 -ov TestClass

// 或者輸出對應信息到指定檔案,比如導出到otool.txt
otool -arch arm64 -ov TestClass > otool.txt

    1. 查看鏈接了哪些庫

otool -L TestClass

    1. 篩選是否鏈接了某個指定的庫,比如 CoreFoundation

otool -L TestClass | grep CoreFoundation

    1. 查看 Mach-O 所有類集合

// 直接在命令行查看
otool -arch arm64 -v -s __DATA __objc_classlist TestClass

// 或者輸出對應信息到指定檔案,比如導出到classlist.txt
otool -arch arm64 -v -s __DATA __objc_classlist TestClass > classlist.txt

    1. 查看 Mach-O 所有使用類的集合

// 直接在命令行查看
otool -arch arm64 -v -s __DATA __objc_classrefs TestClass

// 或者輸出對應信息到指定檔案,比如導出到classrefs.txt
otool -arch arm64 -v -s __DATA __objc_classrefs TestClass > classrefs.txt

    1. 查看 Mach-O 所有使用方法的集合

// 直接在命令行查看
otool -arch arm64 -v -s __DATA __objc_selrefs TestClass

// 或者輸出對應信息到指定檔案,比如導出到classrefs.txt
otool -arch arm64 -v -s __DATA __objc_selrefs TestClass > selrefs.txt

    1. 查看 c 語言字串

otool -v -s __TEXT __cstring TestClass

到這裡為止,otool 是什麼?__DATA.__objc_classlist 是什麼?哪裡來的?怎麼跟 otool 命令結合起來使用?這幾個問題解決了。但是接下來的,怎麼獲取差值?怎麼結合使用正則表達式?要怎麼解決呢?

iOS 程式碼瘦身實踐:刪除無用的類這篇文章裡使用 python 程式碼有實現的過程。但是筆者走了另一條路,這裡分享一下,希望大家多多指點。

OtoolAnalyse的實現原理#

首先,參考 otool 的命令otool -arch arm64 -ov TestClass > otool.txt,生成 otool.txt

打開 otool.txt,搜索Contents of (__DATA,會發現

  • Contents of (__DATA_CONST,__objc_classlist) section 或者 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 或者 Contents of (__DATA,__objc_protolist) section
  • Contents of (__DATA,__objc_selrefs) section
  • Contents of (__DATA_CONST,__objc_imageinfo) section

結合下面的表格來看,就能知道每個 section 代表的含義是什麼了。

表頭表頭
Section用途
__DATA.__objc_classlistObjective-C 類列表
__DATA.__objc_classrefsObjective-C 類引用
__DATA.__objc_superrefsObjective-C 超類引用
__DATA.__objc_catlistObjective-C category 列表
__DATA.__objc_protolistObjective-C 原型
__DATA.__objc_selrefsObjective-C 方法引用
__DATA.__objc_imginfoObjective-C 鏡像信息

分析無用類#

獲取__objc_classlist

來看__objc_classlist所在的 section


0000000100008028 0x10000d450 // 後面的地址0x10000d450,是class的唯一地址
    isa        0x10000d478
    superclass 0x0 _OBJC_CLASS_$_UIViewController // 父類
    cache      0x0 __objc_empty_cache
    vtable     0x0
    data       0x10000c0b8
        flags          0x90
        instanceStart  8
        instanceSize   8
        reserved       0x0
        ivarLayout     0x0
        name           0x1000073cd SecondViewController // 類名
        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

這裡我們通過單個類的信息結構,可以看出其中包含類的地址、類的名字、父類的地址,而筆者想做的是通過固定的程式碼獲取類的信息,然後放到字典中,直到__objc_classlis這個 section 結束,然後就獲取了所有類名字和地址。

那要怎麼做呢?由於檔案不是固定的 json 格式,所以這裡難住了,沒辦法取對應的信息。筆者對比多個類結構,希望能總結出來固定的規律。

WX20210511-182718.png

參考LinkMap專案的 symbolMapFromContent 方法實現,筆者發現,它的匹配是讀取檔案,然後單行單行,匹配文案,設置標記位,從而解析對應信息。程式碼如下


- (NSMutableDictionary *)symbolMapFromContent:(NSString *)content {
    NSMutableDictionary <NSString *,SymbolModel *>*symbolMap = [NSMutableDictionary new];
    // 符號檔案列表
    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;
}

所以,筆者發現,如果按照同樣的邏輯,單行讀取 + 標記位時,同樣的邏輯也可以使用,即每次000000010開頭時,說明是一個新類的開始,存儲對應的地址,設置可以存儲名字標記位,然後讀取到 name 時,就用{ classAddress: className }的格式存儲下來,並把標識位清除,直到下一行包含000000010時,再重置標識位為 YES。程式碼如下:


static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQueryClassList = @"__objc_classlist";

// 獲取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:@" "];
                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總結如下,共有%ld個\n%@:", classListResults.count, classListResults);
    return classListResults;
}

然後怎麼調試這個程式碼的正確與否?

筆者這時候想到了借助LinkMap的 UI,因為同樣都是需要選擇檔案,讀取檔案,而且筆者也想做分析之後結果顯示,外加最後輸出結果到檔案,一整套的邏輯。所以,筆者就想到了把LinkMap的內部實現改掉。

首先第一步,註釋掉checkContent:的判斷,然後analyze:方法中把調用symbolMapFromContent:的地方改為調用classListFromContent:,斷點調試看classListFromContent:方法是否正確?那如何判斷這個方法是否正確呢?最簡單的方法根據個數來,經過classListFromContent:得到的 NSMutableDiction 的數據的個數,和直接搜索otool.txt檔案中Contents of (__DATA_CONST,__objc_classlist) section部分000000010的個數一致,就說明沒有問題。具體如下:

    1. 筆者把otool.txt檔案中除去Contents of (__DATA_CONST,__objc_classlist) section部分刪掉,然後搜索000000010看有多少個。
    1. 運行 LinkMap 專案,選擇 otool.txt,然後斷點看classListFromContent:方法的輸出
    1. 兩個結果個數一致,筆者認為程式碼運行正確。

獲取__objc_classrefs

來看__objc_classrefs所在的 section


Contents of (__DATA,__objc_classrefs) section
000000010000d410 0x0 _OBJC_CLASS_$_UIColor
000000010000d418 0x10000d450
000000010000d420 0x0 _OBJC_CLASS_$_UISceneConfiguration
000000010000d428 0x10000d568

同樣,先來分析上述程式碼,可以看到單行信息中,後面的部分要不是系統信息,要不是類地址。如下:

WX20210511-194536.png

所以,筆者採取同樣的處理邏輯,讀取Contents of (__DATA,__objc_classrefs) section的內容,單行單行讀取,判斷如果包含0x100,說明是類地址,存儲到數組裡。實現如下


static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQueryClassRefs = @"__objc_classrefs";

// 獲取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];            }
        }
    }

    NSLog(@"\n\n__objc_refs總結如下,共有%ld個\n%@:", classRefsResults.count, classRefsResults);
    return classRefsResults;
}

然後校驗上面方法的正確與否,去除除了Contents of (__DATA,__objc_classrefs) section的之外的內容,然後搜索0x100的個數,與classRefsFromContent:方法返回的個數對比,相同則說明方法無錯誤。

取差值,獲取無用類

在 LinkMap 中的analyze:方法中,調用classListFromContent:classRefsFromContent:,獲取到了所有類和已引用類後,所有類存儲是{ classAddress: className },已引用類存儲的是[classAddress],去重後,遍歷去重後的已引用類然後把所有在已引用的地址從所有類中移除。最後所有類中剩下的就是無用的類。程式碼如下


    // 所有classList類和類名字
    NSDictionary *classListDic = [self classListFromContent:content];
    // 所有引用的類
    NSArray *classRefs = [self classRefsFromContent:content];
//        // 所有引用的父類
//        NSArray *superRefs = [self superRefsFromContent:content];
    
    // 先把類和父類數組做去重
    NSMutableSet *refsSet = [NSMutableSet setWithArray:classRefs];
//        [refsSet addObjectsFromArray:superRefs];
    
    // 所有在refsSet中的都是已使用的,遍歷classList,移除refsSet中涉及的類
    // 餘下的就是多餘的類
    for (NSString *address in refsSet.allObjects) {
        [classListDic setValue:nil forKey:address];
    }

    // 移除系統類,比如SceneDelegate,或者Storyboard中的類
    
    NSLog(@"多餘的類如下:%@", classListDic);


最後測試輸出結果如下,可以看到輸出結果的結構,但是其中 ViewController 是 Storyboard 引用的,SceneDelegate 是 Info.plist 檔案中配置的,但是都被識別為無使用類。所以結果打印出來後,刪除前需要確認。也可以在上面的獲取差值程式碼中過濾指定的類。

WX20210512-084919.png

分析無用方法#

無用方法的分析與類稍有不同,因為沒有直接獲取所有方法的地方,__objc_selrefs是所有引用到的方法,因此筆者想到的是,用__objc_classlist中的 BaseMethods、InstanceMethods 以及 ClassMethods 中的數據,作為所有方法的集合,然後和引用的方法做差值,最終得到無用方法。

獲取__objc_selrefs

來看__objc_selrefs所在的 section


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:

可以看到,這部分的數據比較簡單,前面是地址,後面是方法名字,這裡遍歷每一行數據,然後直接以{ methodAddress: methodName }的方式存起來。程式碼如下:


static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQuerySelRefs = @"__objc_selrefs";

// 獲取已使用的方法集合
- (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: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總結如下,共有%ld個\n%@:", selRefsResults.count, selRefsResults);
    return selRefsResults;
}

獲取所有方法列表

這部分稍有麻煩,筆者想的是用__objc_classlist中的 BaseMethods、InstanceMethods 以及 ClassMethods 中的數據,作為所有方法的集合,所以先來看檔案結構,總結出來規律


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

上面的檔案能看出來什麼規律?腦殼疼,筆者想獲取的是 BaseMethods 後面的 name 行的數據,而且筆者還希望能把這個方法跟類關聯起來,這樣最後輸出查找的時候也比較方便。

WX20210512-103154.png

筆者總結出來的規律如下

    1. 按照一行行的讀取邏輯來,讀到了 data,然後讀到了 name,這時候 name 是類名字。
    1. 再接著往下讀,讀到了 baseMethods 或者 InstanceMethods 或者 Class Methods,再然後讀到了 name,這時候 name 中是方法名字和方法地址。
    1. 再接著往下讀,讀到了 data,重複步驟 1

用程式碼邏輯實現就是,設置兩個標誌位,一個標記是類名,一個標記是方法;讀到了 data 之後,把第一個標記置為 YES,然後判斷第一個標記位 YES 時,讀到了 name 就更新類名;讀到了包含 Methods 之後,把第一個標記置為 NO,第二個標記置為 YES,然後判斷是第二個標記位 YES 時,就存儲方法名和方法地址。最終數據以{ className:{ address: methodName } }存儲。程式碼如下


static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQueryClassList = @"__objc_classlist";

// 獲取所有方法集合 { className:{ address: methodName } }
- (NSMutableDictionary *)allSelRefsFromContent:(NSString *)content {
    // 符號檔案列表
    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];
                }
                // data之後第一個的name,是類名
                canAddName = YES;
                canAddMethods = NO;
                continue;
            }
            
            if (canAddName && [line containsString:@"name"]) {
                // 更新類名,用作標記{ className:{ address: methodName } }
                NSArray *components = [line componentsSeparatedByString:@" "];
                className = [components lastObject];
                continue;
            }
            
            if ([line containsString:@"methods"] || [line containsString:@"Methods"]) {
                // method之後的name是方法名,和方法地址
                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;
}

取差值,獲取無用方法

在 LinkMap 中的analyze:方法中,調用allSelRefsFromContent:selRefsFromContent:,獲取到了所有方法和已引用方法後,所有方法存儲是{ className:{ address: methodName } },已引用方法存儲的是{ methodAddress: methodName },遍歷去重後的已引用方法,然後把所有在已引用的地址從所有方法中移除。最後所有方法中剩下的就是無用的方法。程式碼如下


NSMutableDictionary *methodsListDic = [self allSelRefsFromContent:content];
NSMutableDictionary *selRefsDic = [self selRefsFromContent:content];

// 遍歷selRefs移除methodsListDic,剩下的就是未使用的
for (NSString *methodAddress in selRefsDic.allKeys) {
    for (NSDictionary *methodDic in methodsListDic.allValues) {
        [methodDic setValue:nil forKey:methodAddress];
    }
}

// 遍歷移除空的元素
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(@"多餘的方法如下%@", resultDic);


最後測試輸出結果如下,可以看到輸出結果的結構,其中 AppDelegate 和 SceneDelegate 的代理方法被識別為多餘方法。所以結果打印出來後,刪除前需要確認。也可以在上面的獲取差值程式碼中過濾指定的代理方法。

WX20210512-101907.png

最後#

完整的專案地址OtoolAnalyse,筆者用這樣方法,分析出來了專案中無用的類、無用的方法,刪除前要注意先確認。專案還有待完善的地方,比如系統方法的過濾,基類的判斷邏輯,等等,留待後續補充。但整體分析的邏輯如上,筆者趟過了河,先分享為敬,😄。

參考#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。