「iOS 知識体系の全貌を体験する」このシリーズでは、著者「戴铭」が極客時間のコラム「iOS 開発高手課」での学びを共有します。コラムは 1)基礎篇、2)応用篇、3)原理篇、4)ネイティブとフロントエンドの共演篇の 4 つのセクションに分かれています。
私も著者の区分に従って自分の学習ノートを整理しています。この段階では、コラムのエッセンスを吸収することに重点を置き、今後の段階で自分のアウトプットを徐々に増やしていき、自分の成長を証明したいと思います。
今日は最初のセクション:基礎篇を共有します。
00 | 開篇#
2007 年、ジョブズは初代 iPhone を発表しました —— それは多くの人々の携帯電話に対する認識を再定義し、同時にモバイルインターネット時代の始まりでもありました。
2008 年 7 月、WWDC で Apple は App Store を正式にオープンすると発表しました —— これは開発者のためのモバイルインターネット時代が本格的に始まったことを意味します。
次のモバイル端末のホットトピックを探すよりも、これまでの数年間の波が残した重要な技術をしっかりと消化し、その基礎の上でさまざまな「新技術」を理解することが、必ずやスムーズに進むでしょう。
著者戴铭は共有を愛し、日常の学びや仕事の経験を戴铭のブログに共有し、いくつかの技術的なまとめをコードとして戴铭の GitHubに投稿しています。
01 | あなた自身の iOS 開発知識体系を構築する#
iOS の知識体系について、著者は基礎、原理、アプリ開発、ネイティブとフロントエンドの 4 つのセクションに分け、マインドマップを提供しています:
全体的に、何でも見たら学ぶのではなく、目的を持って体系的に学ぶべきであり、その方が効果的で、技術の更新に対処する際も余裕を持てるようになります。
02 | App の起動速度をどう最適化し、監視するか?#
App の起動プロセス#
一般的に、App の起動は冷起動と温起動に分かれます。
- 冷起動:完全な起動プロセス。App をクリックして起動する前に、そのプロセスはシステムに存在せず、システムが新たにプロセスを作成して起動する状況。
- 温起動:表面的に App を開くプロセス。App のプロセスがシステムにまだ存在する場合、ユーザーが再度 App を起動する状況。
ユーザーが感じる起動の遅さは、すべてメインスレッド上で発生します。App の冷起動は主に 3 つの段階に分かれます:
- main () 関数の実行前;
- main () 関数の実行後;
- 初回画面のレンダリング完了後。
main () 関数実行前#
主なプロセス:
- 実行可能ファイル、つまり App の.o ファイルの集合をロード;
- 動的リンクライブラリをロードし、リベースポインタの調整とシンボルのバインドを行う;
- Objc ランタイムの初期処理、Objc 関連クラスの登録、カテゴリ登録、セレクタの一意性チェックなど;
- 初期化、+load () メソッドの実行、attribute ((constructor)) 修飾の関数の呼び出し、C++ 静的グローバル変数の作成。
最適化ポイント:
- 動的ライブラリのロードを減らす(数について、Apple は最大 6 つの非システム動的ライブラリの使用を推奨);
- 起動後に使用しないクラスやメソッドのロードを減らす;
- +load () メソッドの内容は初回画面のレンダリング完了後に実行するか、+initialize () メソッドで置き換える(+load () メソッド内でのランタイムメソッドの置き換え操作は 4 ミリ秒の消費をもたらし、蓄積されると起動速度への影響が大きくなる);
- C++ グローバル変数の数を制御する。
main () 関数実行後#
主なプロセス:main () 関数の実行開始から appDelegate の didFinishLaunchingWithOptions メソッド内の初回画面レンダリング関連メソッドの実行完了まで。この段階では、初回画面のビジネスコードが実行され、初回画面の初期化に必要な設定ファイルの読み書き、初回画面リストの大データの読み込み、初回画面レンダリングの大量計算などの操作が含まれます。
最適化の考え方:初回画面レンダリングに必要な初期化機能と、App 起動に必要な初期化機能、特定の機能が使用される時にのみ初期化が必要なものを整理します。
初回画面レンダリング完了後#
主なプロセス:didFinishLaunchingWithOptions メソッドのスコープ内で初回画面レンダリング後のすべてのメソッドの実行が完了。主に非初回画面の他のビジネスサービスモジュールの初期化、リスナーの登録、設定ファイルの読み込みなどの操作を行います。
最適化ポイント:メインスレッドをブロックするメソッドを優先的に処理しないと、ユーザーのその後のインタラクションに影響を与えます。
App 起動段階で完了すべき作業を理解した後、次に機能レベルとメソッドレベルの起動最適化を行います。
機能レベルの起動最適化#
簡単に言えば、main () 関数の実行開始から初回画面レンダリング完了前の段階では、初回画面に関連するビジネスのみを処理し、他の非初回画面ビジネスの初期化、リスナー登録、設定ファイルの読み込みなどは初回画面レンダリング完了後に行います。
メソッドレベルの起動最適化#
初回画面レンダリング完了前のメインスレッド上でどのメソッドが時間を要するかを確認し、不要な時間を要するメソッドを遅延または非同期で実行します。
メソッドの時間を監視するには、主に2 つの手段があります:
1)メインスレッド上のメソッド呼び出しスタックを定期的に取得し、一定の時間内に各メソッドの時間を計算します。Xcode ツールスイートに内蔵されている Time Profiler は、この方法を採用しています。精度は高くありませんが、十分です。
2)objc_msgSend メソッドをフックして、すべてのメソッドの実行時間を把握します。この方法は非常に正確ですが、Objective-C のメソッドにのみ対応しています(C メソッドやブロックについては、libffi の ffi_call を使用してフックすることも可能ですが、関連ツールの作成と維持のハードルは高いです)。
第 2 の方法に基づく時間監視の完全なコードは、GCDFetchFeed(オープンソースプロジェクト)で確認できます。その使用方法:
時間を検出する必要がある場所で [SMCallTrace start] を呼び出し、終了時に stop と save を呼び出すことで、メソッドの呼び出し階層と時間を印刷できます。また、最大深度と最小時間検出を設定して、不要な情報をフィルタリングできます。
附objc_msgSend関連知識:
- ソースコードはApple のオープンソースサイトで確認できます;
- これは Objective-C でメソッドを実行するための必経路であり、すべての Objective-C メソッドを制御できるため、フックすればすべての Objective-C メソッドをフックできます;
- 実行ロジック:まずオブジェクトに対応するクラスの情報を取得し、次にメソッドのキャッシュを取得し、メソッドのセレクタに基づいて関数ポインタを検索し、例外エラー処理を経て、最後に対応する関数の実装にジャンプします。
- アセンブリ言語で書かれている理由:1)呼び出し頻度が最も高く、これに対するパフォーマンス最適化が App のライフサイクル全体のパフォーマンスを向上させることができ、アセンブリ言語はパフォーマンス最適化において原子レベルの最適化が可能で、最適化を極限まで行うことができる;2)他の言語では未知のパラメータを任意の関数ポインタにジャンプさせる機能を実現するのが難しい;
- それをフックする方法については、Facebook のオープンソースライブラリfishhookを参照できます ——iOS 上で実行される Mach-O バイナリファイル内でシンボルを動的に再バインドすることを実現します。
参考資料:
アセンブリ言語入門チュートリアル—— 阮一峰
03 | Auto Layout の紹介#
Cassowaryは Auto Layout で使用されるレイアウトアルゴリズムです。
Auto Layout を使用する際は、Compression Resistance Priority と Hugging Priority を多用し、優先度の設定を利用してレイアウトをより柔軟にし、コードを少なくし、メンテナンスを容易にすることが重要です。関連するデモはAuto Layout 関連の Demoを参照してください。
フロントエンドに Flexbox という高度なレスポンシブレイアウトの考え方が登場した後、Apple は Auto Layout に基づいて Flexbox に似た UIStackView を封装し、iOS 開発におけるレスポンシブレイアウトの使いやすさを向上させました。
PS:現在のプロジェクトでは、一般的に Auto Layout に基づいて封装されたサードパーティライブラリ、例えばMasonryが使用されています —— 関連ブログ。
04 | プロジェクトが大きくなり、人数が増えた場合、アーキテクチャをどのように設計すればより合理的か?#
目標:ビジネスを完全にデカップリングし、共通機能を下に沈め、各ビジネスを独立した Git リポジトリにし、各ビジネスが Pod ライブラリを生成できるようにし、最終的に統合します。
シンプルなアーキテクチャから大規模プロジェクトアーキテクチャへの進化には、3 つの問題を解決する必要があります:
- モジュールの粒度はどのように分けるべきか?iOS のようなオブジェクト指向プログラミングの開発モデルでは、まず SOLID 原則に従うべきです。
- どのように層を分けるか?3 層を超えないことをお勧めします:下層はビジネスに関係のない基盤コンポーネント(ネットワークやストレージなど)で、ミドル層は一般的なビジネスコンポーネント(アカウント、埋め込み、支払い、ショッピングカートなど)、最上層はイテレーションビジネスコンポーネントで、更新頻度が最も高いです。
- 複数のチームがどのように協力するか?チームの分業は柔軟で、メンバーを隔離して固定化しないようにし、作成したものが互いに使われないようにします。そして、具体的なビジネスに基づいて機能モジュールを抽出し、重複した構築の問題を解決し、その基礎の上で抽出したモジュールを精緻化し、堅実にします。
著者が考える良いアーキテクチャ:
コンポーネント間の関係は調整されているが固定の基準はなく、調整の良し悪しがアーキテクチャの良し悪しを測る基本的な基準となります。
実践の中で、一般的に 2 つのアーキテクチャ設計の選択肢があります:
1)プロトコル式:プロトコル指向のプログラミング思考を採用し、コンパイルレベルでプロトコルを定義して規範を実現し、異なる場所で分散管理とコンポーネントのメンテナンスを達成します。この方法は依存関係の逆転原則にも従い、良いオブジェクト指向プログラミングの実践です。
2)ミドルマン:ミドルマンによる統一管理の方法を採用し、App 全体のライフサイクルにおけるコンポーネント間の呼び出し関係を制御します。
アーキテクチャ設計を考える際、私たちはより多くの機能ロジックとコンポーネントの分割において同じレベルのデカップリングを実現し、上下層の依存関係を明確にする必要があります。このような構造により、上層コンポーネントは容易に差し替え可能で、下層コンポーネントはより安定します。そして、ミドルマンアーキテクチャパターンはこの構造を維持しやすく、ミドルマンの管理の容易さと拡張性により、全体のアーキテクチャが長期的に健全さと活力を保つことができます。したがって、ミドルマンアーキテクチャは著者が考える良いアーキテクチャです。
ケース共有:
ArchitectureDemo——Github、これはミドルマンアーキテクチャの基礎の上にミドルウェア、状態機械、オブザーバー、ファクトリーパターンのサポートを追加し、さらに使用上でもチェーン呼び出しをサポートしています。
参考資料:
iOS アプリケーションアーキテクチャについての序章——Casatwy
05 | リンカー:シンボルはどのようにアドレスにバインドされるのか?#
疑問を持って学ぶ:自分が参加しているプロジェクトでは、なぜあるものはコンパイルが速く、あるものは遅いのか;コンパイルが完了した後、なぜあるものは起動が速く、あるものは遅いのか?
この章ではリンカーについて説明します。リンカーの主な役割は、シンボルをアドレスにバインドすることです。まずはコンパイラから説明しましょう~
iOS 開発でコンパイラを使用する理由#
iOS で書かれたコードは、まずコンパイラを使用してコードを機械語にコンパイルし、その後 CPU 上で直接実行されます。
インタプリタを使用しない理由は、Apple がプログラムの実行効率を高め、実行速度を速くしたいと考えているからです。
逆に、インタプリタは実行時にコードを実行でき、このプロセスは動的であり、プログラムが実行された後にコードを更新してプログラムのロジックを随時変更できます。
現在、Apple が使用しているコンパイラはLLVMで、Xcode 5 以前に使用されていた GCC に比べてコンパイル速度が 3 倍向上しました。また、Apple は LLVM の発展を主導し、LLVM が Apple のハードウェアに対してより多くの最適化を行えるようにしました。
LLVM は本質的にコンパイラツールチェーン技術の集合です:コンパイラは各ファイルをコンパイルし、Mach-O(実行可能ファイル)を生成します;リンカー(LLVM の lld プロジェクト)はプロジェクト内の複数の Mach-O ファイルを 1 つに統合します。
コンパイルプロセス:
- まず、コードを書いた後、LLVM はコードを前処理し、マクロを対応する位置に埋め込みます。
- 次に、LLVM はコードを字句解析と構文解析し、AST(抽象構文木)を生成します(構造的にコードよりも簡潔で、トラバースが速い)。
- 最後に、AST は IR(中間表現)を生成します。これは機械語に近い言語であり、プラットフォームに依存しないため、IR を通じて異なるプラットフォームに適した機械語を生成できます。iOS システムの場合、IR から生成される実行可能ファイルは Mach-O です。
コンパイル時リンカーの役割#
リンカーの役割は、変数、関数名とそのアドレスのバインドを完了することです。冒頭で述べたシンボルは、変数名と関数名と理解できます。
さらに、リンカーは関数のシンボル呼び出し関係を整理する際に、呼び出されていない関数を明確にし、自動的に削除します。このプロセスでは、リンカーは main 関数を起点にして各参照を追跡し、それを live としてマークします。追跡が完了した後、live としてマークされていない関数は無用な関数となります。リンカーは Dead code stripping のスイッチをオンにすることで、無用なコードを自動的に削除する機能を有効にできます(このスイッチはデフォルトでオンになっています)。
動的ライブラリのリンク#
リンクされる共有ライブラリは静的ライブラリと動的ライブラリに分かれます:
- 静的ライブラリはコンパイル時にリンクされるライブラリで、Mach-O ファイルにリンクする必要があり、更新が必要な場合は再コンパイルが必要で、動的にロードや更新はできません;
- 動的ライブラリは実行時にリンクされるライブラリで、dyld を使用して動的にロードできます。
dyld を使用して動的ライブラリをロードする方法は 2 つあります:1)プログラム起動時にバインドされる、2)シンボルが初めて使用される時にバインドされる。起動時間を短縮するために、大部分の動的ライブラリは第 2 の方法を採用しています。
一般的なツール:nm ツール - シンボルテーブルを表示、otool ツール - シンボルに必要なライブラリを探す。
このシリーズ「iOS 知識体系の全貌を体験する」を通じて、今後の深掘りは自分次第ですので、浅く触れるだけではいけませんよ~
投票:春節の休暇中、あなたは主に何をしますか?
A. 親戚を訪ねる
B. 旅行
C. 集まりを開く
D. ドラマを見る
E. 読書
F. コードを書く
G. その他