今是昨非

今是昨非

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

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クラスと参照されているクラスを取得します。2つの集合の差分が無用なクラスの集合となり、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の実装原理の 2 つの部分に関わります。

原理#

まず、Mach-Oとは何かを見てみましょう。Mach-OMach Objectファイル形式の略で、実行可能ファイル、オブジェクトコード、共有ライブラリ、動的にロードされるコード、メモリダンプを記録するファイル形式です。

Mach-O ファイルは主に 3 つの部分で構成されています:

  • Mach Header: Mach-O の CPU アーキテクチャ、ファイルタイプ、ロードコマンドなどの情報を記述します。
  • Load Command: ファイル内のデータなどの具体的な組織構造を記述し、異なるデータタイプには異なるロードコマンドが使用されます。
  • Data: Data 内の各セグメント (Segment) のデータが保存され、セグメントはデータとコードを格納するために使用されます。

Data の一般的なセクションを列挙します。これはMach-O ファイル形式探索からの引用です。

表頭表頭
セクション用途
__TEXT.__text主プログラムコード
__TEXT.__cstringC 言語文字列
__TEXT.__constconst キーワードで修飾された定数
__TEXT.__stubsStub 用のプレースホルダーコード、これをスタブコードと呼ぶことが多いです。
__TEXT.__stubs_helperStub が本当のシンボルアドレスを見つけられない場合の最終指向
__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

// または、対応する情報を指定したファイルに出力します。例えば、selrefs.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

以下の表を参照すると、各セクションが何を表しているのかがわかります。

表頭表頭
セクション用途
__DATA.__objc_classlistObjective-C クラスリスト
__DATA.__objc_classrefsObjective-C クラス参照
__DATA.__objc_superrefsObjective-C スーパークラス参照
__DATA.__objc_catlistObjective-C カテゴリリスト
__DATA.__objc_protolistObjective-C プロトタイプ
__DATA.__objc_selrefsObjective-C メソッド参照
__DATA.__objc_imginfoObjective-C イメージ情報

無用クラスの分析#

__objc_classlist の取得

__objc_classlistが存在するセクションを見てみましょう。


0000000100008028 0x10000d450 // 後ろのアドレス0x10000d450はクラスのユニークアドレス
    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_classlistセクションが終了するまで続け、すべてのクラス名とアドレスを取得したいと考えています。

では、どうすればよいのでしょうか?ファイルは固定の 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を含むまで続けます。コードは以下の通りです。


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. 2 つの結果の個数が一致すれば、筆者はコードが正しく実行されていると考えます。

__objc_classrefs の取得

__objc_classrefsが存在するセクションを見てみましょう。


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

同様に、上記のコードを分析すると、行情報の後半部分はシステム情報かクラスアドレスです。以下のように処理ロジックを採用し、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が存在するセクションを見てみましょう。


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 行のデータであり、さらにこのメソッドをクラスに関連付けたいと思っています。これにより、最終的に出力を検索する際も便利です。

筆者がまとめた規則は以下の通りです。

    1. 行ごとの読み取りロジックに従い、data に到達したら、最初の name はクラス名です。
    1. 次に、baseMethods または InstanceMethods または Class Methods に到達し、その後 name に到達したら、ここにはメソッド名とメソッドアドレスが含まれています。
    1. 次に、data に到達し、ステップ 1 を繰り返します。

コードロジックを実装すると、2 つのフラグを設定します。1 つはクラス名のフラグ、もう 1 つはメソッドのフラグです。data に到達したら、最初のフラグを YES に設定し、最初のフラグが YES のときに name に到達したら、クラス名を更新します。次に、Methods を含む行に到達したら、最初のフラグを NO に設定し、2 番目のフラグを YES に設定します。2 番目のフラグが 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です。筆者はこの方法を用いて、プロジェクト内の無用なクラスと無用なメソッドを分析しました。削除前には必ず確認してください。プロジェクトには、システムメソッドのフィルタリング、基底クラスの判断ロジックなど、改善の余地がまだありますので、今後の補足を待っています。しかし、全体的な分析のロジックは上記の通りです。筆者は河を渡り、まずは敬意を表して共有します。😄

参考#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。