《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)その呼び出し頻度が最も高く、これに対するパフォーマンス最適化はアプリ全体のライフサイクルのパフォーマンスを向上させることができ、アセンブリ言語はパフォーマンス最適化において原子レベルの最適化を行うことができるため、最適化を極限まで行うことができる;2)他の言語では未知のパラメータを任意の関数ポインタにジャンプさせる機能を実現するのが難しい;
- それをフックする方法については、Facebook のオープンソースライブラリfishhookを参考にできます ——iOS 上で実行される Mach-O バイナリファイル内で動的にシンボルを再バインドすることを実現しています。
参考資料:
アセンブリ言語入門チュートリアル—— 阮一峰
03 | Auto Layout の紹介#
Cassowaryは Auto Layout で使用されるレイアウトアルゴリズムです。
Auto Layout を使用する際は、Compression Resistance Priority と Hugging Priority を多用し、優先度の設定を利用してレイアウトをより柔軟にし、コードを少なく、メンテナンスを容易にすることが重要です。Auto Layout 関連のデモを参考にできます。
フロントエンドに 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. その他