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-O
はMach Object
ファイルフォーマットの略で、実行可能ファイル、オブジェクトコード、共有ライブラリ、動的にロードされるコード、メモリダンプを記録するファイルフォーマットです。
Mach-O ファイルは主に 3 つの部分で構成されています:
- Mach Header: Mach-O の CPU アーキテクチャ、ファイルタイプ、ロードコマンドなどの情報を記述します。
- Load Command: ファイル内のデータなどの具体的な組織構造を記述し、異なるデータタイプには異なるロードコマンドが使用されます。
- Data: Data 内の各セグメント (Segment) のデータがここに保存され、セグメントはデータとコードを格納するために使用されます。
Data の一般的なセクションを列挙します。これはMach-O ファイル形式の探求からの引用です。
表頭 | 表頭 |
---|---|
Section | 用途 |
__TEXT.__text | 主プログラムコード |
__TEXT.__cstring | C 言語の文字列 |
__TEXT.__const | const キーワードで修飾された定数 |
__TEXT.__stubs | Stub のためのプレースホルダーコード、ここでは多くの場所でスタブコードと呼ばれています。 |
__TEXT.__stubs_helper | Stub が真のシンボルアドレスを見つけられない場合の最終的な指向 |
__TEXT.__objc_methname | Objective-C メソッド名 |
__TEXT.__objc_methtype | Objective-C メソッドタイプ |
__TEXT.__objc_classname | Objective-C クラス名 |
__DATA.__data | 初期化された可変データ |
__DATA.__la_symbol_ptr | lazy binding のポインタテーブル、テーブル内のポインタは最初にすべて__stub_helper を指します |
__DATA.nl_symbol_ptr | 非 lazy binding のポインタテーブル、各テーブル項目のポインタは、ロード中に動的にリンクされたマシンによって検索されたシンボルを指します |
__DATA.__const | 初期化されていない定数 |
__DATA.__cfstring | プログラムで使用される Core Foundation 文字列(CFStringRefs) |
__DATA.__bss | BSS、初期化されていないグローバル変数を格納します。これは一般的に静的メモリ割り当てと呼ばれます。 |
__DATA.__common | 初期化されていないシンボル宣言 |
__DATA.__objc_classlist | Objective-C クラスリスト |
__DATA.__objc_protolist | Objective-C プロトタイプ |
__DATA.__objc_imginfo | Objective-C イメージ情報 |
__DATA.__objc_selrefs | Objective-C メソッド参照 |
__DATA.__objc_protorefs | Objective-C プロトタイプ参照 |
__DATA.__objc_superrefs | Objective-C スーパークラス参照 |
実装#
Mach-O ファイルの取得:Xcode でパッケージ化された iPA の拡張子を.zip に変更し、解凍して payload フォルダを取得します。その中に xxx.app があり、右クリックしてパッケージの内容を表示すると、xxx の exec ファイルがあり、これが Mach-O ファイルです。
otool コマンドの簡単な使用法#
例えば、プロジェクト名が TestClass の場合、TestClass exec があるフォルダに移動します。
-
- otool シンボルのフォーマット化、プロジェクトのクラス構造と定義されたメソッドを出力します。
// コマンドラインで直接確認
otool -arch arm64 -ov TestClass
// または、指定したファイルに出力します。例えばotool.txtにエクスポートします。
otool -arch arm64 -ov TestClass > otool.txt
-
- どのライブラリがリンクされているかを確認します。
otool -L TestClass
-
- 特定のライブラリ、例えば CoreFoundation がリンクされているかをフィルタリングします。
otool -L TestClass | grep CoreFoundation
-
- Mach-O のすべてのクラス集合を確認します。
// コマンドラインで直接確認
otool -arch arm64 -v -s __DATA __objc_classlist TestClass
// または、指定したファイルに出力します。例えばclasslist.txtにエクスポートします。
otool -arch arm64 -v -s __DATA __objc_classlist TestClass > classlist.txt
-
- Mach-O のすべての使用クラスの集合を確認します。
// コマンドラインで直接確認
otool -arch arm64 -v -s __DATA __objc_classrefs TestClass
// または、指定したファイルに出力します。例えばclassrefs.txtにエクスポートします。
otool -arch arm64 -v -s __DATA __objc_classrefs TestClass > classrefs.txt
-
- Mach-O のすべての使用メソッドの集合を確認します。
// コマンドラインで直接確認
otool -arch arm64 -v -s __DATA __objc_selrefs TestClass
// または、指定したファイルに出力します。例えばselrefs.txtにエクスポートします。
otool -arch arm64 -v -s __DATA __objc_selrefs TestClass > selrefs.txt
-
- 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 | 用途 |
__DATA.__objc_classlist | Objective-C クラスリスト |
__DATA.__objc_classrefs | Objective-C クラス参照 |
__DATA.__objc_superrefs | Objective-C スーパークラス参照 |
__DATA.__objc_catlist | Objective-C カテゴリリスト |
__DATA.__objc_protolist | Objective-C プロトタイプ |
__DATA.__objc_selrefs | Objective-C メソッド参照 |
__DATA.__objc_imginfo | Objective-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 形式ではないため、対応する情報を取得するのは難しいです。筆者は複数のクラス構造を比較し、固定の規則をまとめようとしました。
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
の個数を比較することです。具体的には以下の通りです。
-
- 筆者は
otool.txt
ファイルからContents of (__DATA_CONST,__objc_classlist) section
部分を削除し、000000010
の個数を検索します。
- 筆者は
-
- LinkMap プロジェクトを実行し、otool.txt を選択してブレークポイントを設定し、
classListFromContent:
メソッドの出力を確認します。
- LinkMap プロジェクトを実行し、otool.txt を選択してブレークポイントを設定し、
-
- 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 ファイルで構成されていますが、これらはすべて無使用クラスとして認識されました。したがって、結果が印刷された後、削除する前に確認する必要があります。上記の差分取得コードで特定のクラスをフィルタリングすることもできます。
無用メソッドの分析#
無用メソッドの分析はクラスとは少し異なります。なぜなら、すべてのメソッドを直接取得する場所がないからです。__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 行のデータです。また、筆者はこのメソッドをクラスに関連付けたいと考えています。そうすれば、最終的に出力を検索する際に便利です。
筆者がまとめた規則は以下の通りです。
-
- 行ごとの読み取りロジックに従い、data に到達したら、最初の name はクラス名です。
-
- 次に、baseMethods または InstanceMethods または Class Methods に到達し、その後 name に到達したら、name にはメソッド名とメソッドアドレスが含まれています。
-
- 次に、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 の代理メソッドが無用なメソッドとして認識されました。したがって、結果が印刷された後、削除する前に確認する必要があります。上記の差分取得コードで特定の代理メソッドをフィルタリングすることもできます。
最後に#
完全なプロジェクトのアドレスはOtoolAnalyseです。筆者はこの方法を用いて、プロジェクト内の無用なクラスと無用なメソッドを分析し、削除する前に確認する必要があります。プロジェクトには、システムメソッドのフィルタリング、基底クラスの判断ロジックなど、改善の余地がある部分が残っています。全体的な分析のロジックは上記の通りです。筆者は河を渡り、まずは敬意を表して共有します。😄