今日共有するのは:基礎篇(デバッグテストとリリース段階)。
06 | 動的ライブラリを注入して高速コンパイルデバッグを実現?#
部分的にコードをバイナリにコンパイルしてプロジェクトに統合することで、毎回全量コンパイルを避けてコンパイル速度を上げることはできますが、毎回のコンパイルではApp を再起動する必要があります。再度デバッグプロセスを経る必要があります。
では、ネイティブコードはどのように動的な高速デバッグを実現するのでしょうか?まず、どのようなツールが動的デバッグを実現しているのか見てみましょう:
1)Swift Playground、コードの変更がリアルタイムでフィードバックされます;
2)Flutter Hot Reload、Flutter は VSCode のデバッグバーの reload ボタンをクリックした後、前回のコンパイル以降に変更されたコードを確認し、関連するコードライブラリを再コンパイルします。これらの再コンパイルされたライブラリはすべてカーネルファイルに変換され、Dart VM に送信され、Dart VM は新しいカーネルファイルを再読み込みし、Flutter フレームワークはすべての Widgets と Render Objects を再構築、再配置、再描画します。
では、Cocoa フレームワークは高速デバッグを実現するために何をすべきでしょうか?
Xcode 用の Injection#
Injectionツールは、実行中のプログラム内で Swift または Objective-C のコードを動的に実行し、デバッグ速度を向上させることができ、プログラムを再起動する必要はありません。
動作原理:Injection はソースコードファイルの変更を監視し、ファイルが変更されると Injection Server が rebuildClass を実行して再コンパイルし、動的ライブラリ、つまり.dylib ファイルにパッケージ化します。そして、writeSting メソッドを使用して Socket を通じて実行中の App に通知します。原理の概略図は以下の通りです:
07 | OCLint、Clang、Infer、静的分析ツールの比較#
まず、3 つの一般的な複雑度指標を紹介します。静的分析ツールはこれらを利用してコードが最適化やリファクタリングが必要かどうかを分析します。
- サイクル複雑度、モジュールを遍歴する際の複雑度を指し、この複雑度は if、for などの分岐文や &&、|| などの演算子によって決まります。一般的に、サイクル複雑度が 11 以上になると非常に高くなり、リファクタリングを検討する必要があります。そうでないと、テストケースの数が多すぎてメンテナンスが難しくなります。
- NPath 度、メソッドが実行可能なすべてのパスの数を指します。一般的に 200 を超えると複雑度を下げることを検討する必要があります。
- NCSS 度、コメントを含まないソースコードの行数を指します。NCSS 度が大きすぎると、メソッドやクラスが行っていることが多すぎて、コードのメンテナンス性や可読性に影響を与え、分割やリファクタリングが必要です。一般的にメソッドの行数は 100 行を超えず、クラスの行数は 1000 行を超えません。
静的分析ツールを使用する際の 2 つの欠点を事前に明示します:
- より長い時間がかかる必要があります。コンパイルプロセスと比較して、静的分析自体はコンパイルで最も時間がかかる IO と構文解析段階を含んでおり、深層のプログラムエラーが発見された場合、現在分析中のメソッド、パラメータ、変数をプロジェクト全体の関連コードと一緒に分析します。
- 特別に設計された、検索可能なエラーしか検出できません。特定のタイプのエラー分析には、開発者が自分の能力でプラグインを作成して追加する必要があります。
以下に 3 つの一般的な静的分析ツールを正式に比較します:
- OCLint は Clang Tooling に基づいて開発された静的分析ツールです。
- Clang 静的分析器は C++ で開発されたオープンソースツールで、Clang プロジェクトの一部であり、Clang と LLVM の上に構築されています。
- Infer は OCaml 言語で書かれた Facebook のオープンソース静的分析ツールです。
静的分析ツール | 長所 | 短所 |
---|---|---|
OCLint | チェックルールが多く、カスタマイズ性が高い | カスタマイズ度が高すぎて、使いやすさが低い |
Clang 静的分析器 | Xcode との統合度が高く、コマンドラインをサポート | チェックルールが少なく、チェック粒度が粗い |
Infer | 効率が高く、インクリメンタル分析をサポートし、小範囲分析も可能 | カスタマイズ性が中程度 |
総合的に見ると、Infer は正確性、性能効率、ルール、拡張性、使いやすさの全体的なバランスが最も優れており、試してみる価値があります。
参考資料:
08 | Clang を利用して App の品質を向上させるには?#
以前に述べた Clang 静的分析ツールに加えて、Clang に基づいて App の品質を保証するシステムプラットフォームを開発することも可能です。例えば、CodeCheckerは、コードのインクリメンタル分析、コードの可視化、コード品質レポート生成などの機能を備えています。また、Mozilla が開発したオンラインウェブコードナビゲーションツールDXRもあり、ポータブルデバイスでの操作や問題分析が便利です。
Clang とは?#
まず、iOS 開発の完全なコンパイルフローチャートを見てみましょう:
左側の黒いブロック Clang は C、C++、Objective-C のコンパイラフロントエンドであり、Swift には独自のコンパイラフロントエンド SIL オプティマイザーがあります。
Clang は C++ で開発されており、そのソースコードの品質は非常に高く、学ぶべき点が多くあります。例えば、ディレクトリが明確で、機能のデカップリングがうまく行われており、分類が明確で組み合わせや再利用が容易で、コードスタイルが統一されており規範的で、コメントが多く読みやすいなどです。また、プロジェクトコード量が膨大であるだけでなく、ツールも非常に多く、相互関係が複雑ですが、Clang は使いやすいブラックボックスドライバーを提供しており、フロントエンドコマンドとツールチェインコマンドをカプセル化し、その使いやすさを大幅に向上させています。
Clang は何をしているのか?#
1)コードを字句解析し、トークンに分割します。
トークンタイプは 4 種類に分かれます:
- キーワード:構文内のキーワード、例えば if、else、while、for など;
- 識別子:変数名;
- リテラル:値、数字、文字列;
- 特殊記号:加減乗除などの記号。
2)次に構文解析を行い、トークンを組み合わせて意味生成ノードを作成し、抽象構文木(AST)を構成します。
ノードは主に Type タイプ、Decl 宣言、Stmt 陳述の 3 種類に分かれ、これら 3 種類のノードを拡張することで無限のコード形態を表現できます。
Clang はどのような能力を提供しているのか?#
Clang は、コードの構文や意味情報を分析する必要があるツールに基盤を提供します:LibClang、Clang Plugin、LibTooling。
LibClang#
LibClang は Clang の上層の高度な抽象にアクセスする能力を提供します。例えば、すべてのトークンを取得したり、構文木を遍歴したり、コード補完を行ったりします。API は非常に安定しており、Clang のバージョン更新がその影響を大きく受けることはありません。しかし、LibClang は Clang AST 情報に完全にアクセスすることはできません。
Clang Plugins#
Clang Plugins は、実行時にコンパイラによってロードされる動的ライブラリで、AST 上で操作を行い、コンパイルに統合してコンパイルの一部となることができます。
LibTooling#
LibTooling は C++ インターフェースで、LibTooling を通じて独立して実行される構文チェックやコードリファクタリングツールを作成できます。
LibClang と比較して、そのインターフェースはそれほど安定しておらず、AST の API のアップグレードに注意を払う必要があり、すぐに使えるわけではありません;Clang Plugins と比較して、コンパイルプロセスに影響を与えることはできません。しかし、Clang AST を完全に制御でき、独立して実行できます。
関連資料:
Tutorial for building tools using LibTooling and LibASTMatchers(LibTooling を使用して言語変換ツールを構築する)
09 | 非侵入型の埋点ソリューションはどのように実現するのか?#
iOS 開発において、埋点は2 つの大きな問題を解決できます:1)ユーザーが App を使用する行動を理解すること、2)オンラインの問題分析の難易度を下げること。
一般的な埋点方法は主に 3 つあります:
1)コード埋点:手動でコードを書いて埋点を行います。特徴は埋点が正確でデバッグが容易ですが、開発とメンテナンスの作業量が大きいです。
2)可視化埋点:埋点の追加と変更作業を可視化します。特徴は埋点体験を向上させます。
3)無埋点:より正確には「全埋点」で、埋点コードはビジネスコードに現れません。特徴は管理とメンテナンスが容易ですが、一般的な埋点要求にしか対応できません。
可視化埋点と無埋点は、どちらも非侵入型の埋点ソリューションに分類されますが、どのように実現するのでしょうか?
実行時メソッド置換#
iOS 開発で最も一般的な 3 つの埋点は、ページの入場回数、ページの滞在時間、クリックイベントの埋点です。
これら 3 つの一般的な状況について、実行時メソッド置換技術を使用して埋点コードを挿入し、非侵入型の埋点方法を実現できます:
1)まず、実行時メソッド置換のクラス SMHook を作成し、置換メソッド hookClass:fromSelectorを追加します。
2)埋点タイプに基づいて置換するメソッドと識別子を決定し、+load () メソッド内で SMHook を使用してメソッドを置換します。
以下は非侵入型埋点 - 実行時メソッド置換情報対照表で、参考用です:
非侵入型埋点-実行時メソッド置換情報埋点タイプ | 置換方法 | 識別子 |
---|---|---|
ページ入場回数、ページ滞在時間 | UIViewController のライフサイクル | NSStringFromClass([self class]) |
UITableView(特例) | setDelegate | NSStringFromClass([self class]) |
クリックイベント | クリックイベントのメソッド | NSStringFromSelector(action) + NSStringFromClass([target class]) |
ジェスチャーイベント | initWithTarget:action: | NSStringFromSelector(action) + NSStringFromClass([target class]) |
イベントのユニーク識別子#
同じ UIButton の異なるインスタンスは、単に「action セレクタ名」+「ビュークラス名」の組み合わせだけでは区別できません。この場合、異なるイベントを区別するためのユニーク識別子が必要です。
各サブビューは親ビュー内で自分のインデックスを持ち、これを組み合わせてイベントのユニーク識別子を生成できます。特例:UITableViewCell は indexPath を使用して各 Cell のユニーク性を決定できます;UIAlertController は内容を使用してそのユニーク識別子を決定できます...
ただし、イベントのユニーク識別子の正確性は保証されにくいです(例えば、ビュー階層が実行時に変更される場合、要求の反復によりページが頻繁に更新されるため)、したがって、実行時メソッド置換を使用した非侵入型埋点ソリューションは、一般的に機能やビューが安定している場所でのみ使用されます。
考察:Clang AST のインターフェースを使用して、ビルド時に AST を遍歴し、必要な埋点コードを追加することは可能でしょうか。
10 | パッケージサイズ:リソースとコードの観点からのスリム化#
Apple は iOS App のサイズに厳しい制限を設けています:ダウンロードサイズ(200 MB)を超えると、ユーザーがセルラーネットワークで App をダウンロードするのを妨げ、新規ユーザーの転換に影響を与えます;実行可能ファイルの text セクションサイズ(iOS 7-:80MB、iOS 7 - 8:60MB、iOS 9+:500MB)を超えると、App の審査が拒否され、App の上架に影響を与えます;さらに、App のパッケージサイズが大きすぎると、ユーザーのアップグレード率にも影響を与えます。
したがって、パッケージサイズを制御することは非常に重要です。次に、一般的なパッケージサイズのスリム化方法を紹介します。
公式 App Thinning#
App Thinning は Apple が提供する App のダウンロードプロセスを改善する新技術で、主にユーザーが App をダウンロードする際のデータ使用量が高すぎること、及び iOS デバイスに占めるストレージスペースが大きすぎることを解決します。
2 つのスリム化ポイント:
- 画像リソースのサイズ。iOS デバイスの画面サイズに応じて異なるサイズのリソースをマッチングします。例えば、iPhone 6 は 2x 解像度の画像リソースのみをダウンロードし、iPhone 6plus は 3x 解像度の画像リソースのみをダウンロードします。
- チップ命令セットアーキテクチャファイル。ユーザーがダウンロードする際、適切なデバイスのチップ命令セットアーキテクチャファイルのみがダウンロードされます。App には 32 ビット、64 ビットの異なるチップアーキテクチャの最適化バージョンもあります。
3 つのスリム化方法:
- App Slicing:iTunes Connect に App をアップロードした後、App を切り分け、異なるバリアントを作成して異なるデバイスに適用します。
- On-Demand Resources:主にゲームの多レベルシーンにサービスを提供し、ユーザーのレベル進行に応じて次のいくつかのレベルのリソースをダウンロードし、クリアしたリソースも削除されるため、初回インストール時の App のパッケージサイズを減少させることができます。
- Bitcode:特定のデバイスに対してパッケージサイズの最適化を行い、最適化の効果は明確ではありません。
どのように使用するのか?
ここでの大部分の作業は Xcode と App Store が行ってくれます~
あなたは Xcode を通じて xcassets ディレクトリを追加するだけです(File > New File > Asset Catalog)、そして 2x 解像度と 3x 解像度の画像を追加します。
チップ命令セットアーキテクチャファイルはデフォルト設定に従い、App Store はデバイスに応じて異なるバリアントを作成し、各バリアントには現在のデバイスに必要な命令セットファイルのみが含まれます。
不要な画像リソース#
推奨されるオープンソースツール:LSUnusedResourcesは、直接ルールを追加して処理でき、以下の 4 つのステップを実行します:
- find コマンドを使用してプロジェクト内のすべての画像リソースファイルを取得します;
- find コマンドと正規表現を使用してソースコード内で使用されているリソース名を見つけます;
- 上記 2 つの差集合を取ることで、不要なリソースを得ます;
- 最後に、システムクラス NSFileManger を使用して不要なリソースを削除します。
画像リソースの圧縮#
推奨される圧縮方法: WebPフォーマットに変換すること(Google のオープンソースプロジェクト;圧縮率が高く、有損と無損の 2 つの圧縮モードをサポート;Alpha 透明と 24 ビットの色数をサポートし、PNG8 のように色が不足して毛羽立ちが発生することはありません)。
関連する圧縮ツール:cwebp(Google、コマンドライン)、iSparta(Tencent、GUI)。
考慮すべき点:WebP 画像を表示するには libwebp を使用して解析する必要があります(libwebp Demo)、その CPU 消費とデコード時間は PNG の 2 倍になるため、空間と時間のトレードオフになります。
著者の提案:
1)画像サイズが 100KB を超える場合、WebP の使用を検討してください;
2)それ以外の場合は、TinyPng(ウェブツール)またはImageOptim(GUI ツール)を使用して画像を圧縮してください。
コードのスリム化#
App のインストールパッケージは主にリソースと実行可能ファイルで構成されています。
リソースのスリム化について話した後、次は実行可能ファイルのスリム化、つまり不要なコードを見つけて削除するプロセスについてです。これは不要な画像リソースを見つける 4 つのステップと似た考え方です。
1)まず、LinkMap を使用してメソッドとクラスの全集を見つけます;
LinkMap を取得するには:プロジェクトファイル > Targets > Build Setting の Write Link Map File を Yes に設定し、Path to Link Map File のパスを指定すると、毎回のコンパイル後に LinkMap ファイルを得ることができます。
LinkMap ファイルの構成は以下の図のようになります:
- Object File:コードプロジェクトのすべてのファイル;
- Section:生成された Mach-O ファイル内のコードセクションのオフセット位置とサイズ;
- Symbols:各メソッド、クラス、ブロック、およびそれらのサイズ。
2)次に、使用されたメソッドとクラスを見つけます;3)両者の差集合を取って不要なコードを得ます;4)最後に、人工的に不要なコードを確認した後、削除します。これらのプロセスには 3 つの方法があります:
A. MachOViewソフトウェアを使用して Mach-O ファイルを表示します。
Section 情報の中で、__objc_selrefs には呼び出されたメソッドが、__objc_classrefs には呼び出されたクラスが、__objc_superrefs には super を呼び出したクラスが含まれています。
ただし、これらには実行時に動的に呼び出されたメソッドは含まれません(Objective-C の動的性質のため)、二次確認が必要です。
B. 直接AppCodeソフトウェアを使用して不要なコードを見つけます。
前提条件:プロジェクトコード量が百万行に達していないこと。
AppCode > Code > Inspect Code を使用して分析した後、Unused code にすべての不要なコードが表示されます。
ただし、いくつかの状況では不要と誤判定されることもあり、人工的に二次確認が必要です。例えば:
- performSelector 方式で呼び出されたメソッド、例えば [self performSelector:@selector (xxx)];
- サブクラスで使用される親クラスメソッド;
- 実行時に宣言されたクラス、例えば NSClassFromString で呼び出されたクラス、[[self class] xxx] のようにクラス名を指定せずに使用されるクラス、registerClass を使用した UITableView のカスタム Cell;
- ドット方式で使用されるプロパティ;
- JSONModel で定義された未使用のプロトコル...
C. 実行時にクラスが本当に使用されているかどうかを確認します。
方法 1 と 2 を使用して不要なコードを見つけて削除しましたが、パッケージ内にはまだ不要なコードが存在する可能性があります。例えば、静的チェック時に使用されたコードが、オンラインではその入口すら存在しない場合があり、ユーザーが使用することはありません。
Objective-C の runtime ソースコードには、クラスが初期化されているかどうかを判断する関数 isInitializedがあり、その結果はメタクラス(class_rw_t 構造体の flags 情報の第 1<<29 ビット)に保存されます。
具体的に実行時の不要なクラスチェックツールを作成する際には、この初期化情報を使用します:
- まず、オフラインテスト段階ですべてのクラスをチェックし、初期化されていないクラスを見つけます;
- 次に、オンライン後に初期化されていないクラスを多バージョンで観察します;
- 本当に使用されていないクラスを確認した後、削除します。
11 | ホットな問題の Q&A(一):基礎モジュールの問題 Q&A#
動的ライブラリのロード方法#
2 つの方法があります:
- プログラムが開始されるときに dyld によって動的にロードされます。dyld によってロードされた動的ライブラリは、コンパイル時にリンクする必要があり、リンク時にマークが付けられ、バインドされたアドレスはロード後に決定されます。
- 明示的な実行時リンク、つまり実行時に動的リンクライブラリの API である dlopen と dlsym を使用してロードします。この方法では、コンパイル時にリンクに参加する必要はありません。ただし、この実行時にリモート動的ライブラリをロードする App は、Apple によって App Store にアップロードされることは許可されていないため、オフラインデバッグ段階でのみ使用できます。
App の起動速度に関する問題#
アセンブリ言語をどの程度学ぶのが適切か?
- もし仕事が逆向きやセキュリティ分野に関与していない場合、アセンブリコードを理解できるだけで非常に良いです;
- アセンブリ言語を学びたい場合は、手を動かしてコードを書いたりデバッグしたりし、Xcode ツールを併用してください。
参考資料:
Mike Ash の「ARM64 上の objc_msgSend を解剖する」は、objc_msgSend のソースコードを分析し、その中の ARM64 アセンブリコードについて詳しく説明しています。
追加の推奨:
ある特定の知識を習得して初めて、問題に直面したときにその知識を使って解決策を考えることができるので、少なくともこれらの知識を理解しておいてください~