tinker の最大のハイライトの 1 つは、一連の dex diff および patch 関連のアルゴリズムを自社開発したことです。この記事の主な目的は、このアルゴリズムを分析することです。もちろん、分析の前提条件として、dex ファイルの形式をある程度理解する必要があることに注意してください。理解していないと混乱する可能性があります。
したがって、この記事では、最初に dex ファイル形式の簡単な分析を行い、いくつかの簡単な実験も行い、最後に dex diff およびパッチのアルゴリズム部分に入ります。
まず第一に、Dex ファイルについて簡単に理解しましょう。逆コンパイルすると、apk に 1 つ以上の *.dex ファイルが含まれることは誰もが知っています。このファイルには、作成したコードが保存されます。通常の状況では、変換ツールも使用します。それを jar に保存し、逆コンパイルしていくつかのツールで表示します。
jar ファイルはクラス ファイルの圧縮パッケージに似ていることを誰もが知っているはずです。通常の状況では、各クラス ファイルを直接解凍して表示できます。 dex ファイルを解凍しても内部クラス ファイルを取得できません。これは、dex ファイルが独自の特定の形式を持つことを意味します:
dex は Java クラス ファイルを再配置し、すべての JAVA クラス ファイルの定数プールを分解し、冗長な情報を削除し、それらを再結合して定数プールを形成します。すべてのクラス ファイルは同じ定数プールを共有し、同じ文字列と定数を作成します。DEX 内で 1 回だけ表示されます。ファイルを削除することにより、ファイル サイズが削減されます。
次に、dex ファイルの内部構造がどのようなものかを見てみましょう。
ファイルの構成を分析するには、分析用に最も単純な dex ファイルを作成するのが最善です。
まず、クラス Hello.java:
を作成します。 リーリー次にコンパイルします:
リーリー最後に dx 作業を通じて dex ファイルに変換します:
リーリーdx パスは、Android-sdk/build-tools/versionnumber/dx の下にあります。dx コマンドが認識できない場合は、パスを path の下に置くか、絶対パスを使用してください。
このようにして、非常に単純な dex ファイルを取得します。
まず、dex ファイルの大まかな内部構造図を示します。
もちろん、単に画像から説明するだけでは絶対に十分ではありません。後ほど diff と patch のアルゴリズムを学習するからです。理論的には、dex ファイルの各要素に至るまで、さらに詳細を知る必要があります。バイトは何を表しますか?
バイナリのようなファイルの場合、メモリに依存しないことが最善の方法です。幸いなことに、分析に役立つソフトウェアがあります:
ダウンロードしてインストールした後、dex ファイルを開くと、dex ファイルの解析テンプレートをインストールするように案内されます。
最終的なレンダリングは次のとおりです:
上部は dex ファイルの内容 (16 進形式で表示) を表し、下部は dex ファイルの各領域を示します。下部をクリックすると、対応するコンテンツ領域と内容が表示されます。
もちろん、dex ファイルについての理解を深めるために、いくつかの特別記事を読むことも強くお勧めします。
この記事では、dex ファイルの簡単な形式分析のみを行います。
dex_header
まず、dex_header の大まかな分析を行います。ヘッダーには次のフィールドが含まれます:
まず第一に、ヘッダーの役割を推測します。ヘッダーにはいくつかの検証関連フィールドと、dex ファイル全体のブロックのおおよその分布 (off はオフセット) が含まれていることがわかります。
この利点は、仮想マシンが dex ファイルを読み取るときに、ヘッダー部分を読み取るだけで dex ファイルのおおよそのブロック分布がわかり、ファイル形式が正しいかどうか、およびファイル形式が正しいかどうかを確認できることです。ファイルが改ざんされているなど
残りのほとんどは、ペアで表示されるサイズとオフであり、そのほとんどは、各ブロックに含まれる特定のデータ構造の数とオフセットを表します。例: string_ids_off は 112、つまり string_ids 領域がオフセット 112 から始まることを意味し、string_ids_size は 14、つまり string_id_item の数が 14 であることを意味します。他も同様なので紹介は省略します。
010Editorと組み合わせると、各エリアに含まれるデータ構造とそれに対応する値が確認できますので、ぜひご覧ください。
ヘッダーの他に、dex_map_list という重要な部分があります。最初に図を見てみましょう:
最初はmap_item_listの番号で、その後に各map_item_listの説明が続きます。
map_item_list は何に役立ちますか?
各map_list_itemには、列挙型、2バイトの未使用メンバー、現在の型の番号を示すサイズ、および現在の型のオフセットを示すオフセットが含まれていることがわかります。
次の例を見てみましょう:
残りは順番に推測できます~~
この場合、map_list により、完全な dex ファイルが固定領域 (この例では 13) に分割でき、各領域の開始とその領域に対応するデータ形式の数がわかることがわかります。 。
map_list を通じて各エリアの先頭を見つけます。各エリアは特定のデータ構造に対応します。010 Editor を通じて表示するだけです。
dex の基本的な形式を理解したので、dex diff と patch を行う方法を考えてみましょう。
最初に考慮すべきことは、私たちが持っているものです:
古い dex を使用したパッチ アルゴリズムを通じて新しい dex も生成できるパッチ ファイルを生成したいと考えています。
###ヘッダ###
ヘッダーは他のデータに基づいて生成できるため、処理する必要はありません;
マップリストの場合、主に必要なのは各エリアの開始(オフセット)です
この領域では、Hello を削除し、Android を追加したことがわかります。
その後、パッチは次のようにこの領域を記録できます:
「del Hello, add Android」(実際にはバイナリに変換する必要があります)。
アプリケーションで直接読み取ることができる古い dex について考えてみましょう。つまり、次のことがわかります。
この領域には、Hello、World、zhyが含まれていることが判明しました。
パッチのこの領域には次の内容が含まれます:「del Hello, add Android」
次に、新しい dex に次のものが含まれることを非常に簡単に計算できます:
アルゴリズムの一般的な概念を理解したら、ソース コードを見てみましょう。
3. Tinker DexDiff ソース コードの簡単な分析
ここにはコードを読むためのコツがあります。実際にはかなり多くのいじくりコードがあり、コードの束にはまってしまうことがよくあります。 diff アルゴリズムのように、入力パラメータは古い dex と新しい dex で、出力はパッチ ファイルです。
その場合、上記のパラメータを受け入れて出力するクラスまたはメソッドが存在する必要があります。実際、このクラスは DexPatchGenerator:
diff の API 使用コードは次のとおりです:
リーリーコードは tinker-build の tinker-patch-lib の下にあります。
単体テストまたはメイン メソッドを作成します。上記のコード行は diff アルゴリズムです。
コードを見るときは、ターゲットを絞る必要があります。たとえば、差分アルゴリズムを見る場合は、差分アルゴリズムへの入り口を見つけます。gradle プラグインでは心配する必要はありません。
渡した dex ファイルを Dex オブジェクトに変換します。
リーリーまずファイルを byte[] 配列として読み取り (これは非常にメモリを消費します)、次にそれを ByteBuffer でラップし、バイト順序をリトル エンディアンに設定します (ここでは ByteBuffer が非常に便利であることがわかります)。メソッドは、Dex オブジェクトの tableOfContents に値を割り当てます。
リーリーReadHeaderとreadMapは内部で実行されており、上記ではヘッダーとマップリストを大まかに分析しましたが、実際にはこの2つの領域はあるデータ構造に変換されて読み込まれ、メモリに格納されます。
最初に readHeader を見てみましょう:
リーリーここで 010 Editor を開いたり、前面の図を見てみると、実際にはヘッダー内のすべてのフィールドを定義し、応答バイトを読み取り、値を割り当てています。
次に、readMap を見てください:
リーリーここで注意していただきたいのは、ヘッダーを読み込む際に、実際にはマップリスト領域を除いたオフセットが読み込まれ、mapList.offに格納されるということです。したがって、マップ リストは実際にはこの位置から始まります。最初に読み取るのはmap_list_itemの番号であり、次に読み取るのは各map_list_itemに対応する実際のデータです。
type、unused、size、offset の順に読み込まれていることがわかります。前に map_list_item について説明した印象がまだ残っている方は、これがこれに相当し、対応するデータ構造は TableContents.Section オブジェクトです。
computeSizesFromOffsets() は主にセクションの byteCount (複数バイトを占有する) パラメータに値を割り当てます。
これで、dex ファイルから Dex オブジェクトへの初期化が完了しました。
Dex オブジェクトを 2 つ取得したら、diff 操作を実行する必要があります。
ソース コードに戻ります:
リーリー2 つの Dex オブジェクトのコンストラクターに直接:
リーリー最初に oldDex と newDex に値を割り当て、次に 15 個のアルゴリズムを順番に初期化することを確認してください。各アルゴリズムは各領域を表します。アルゴリズムの目的は前に説明したとおりです。「どのアルゴリズムが」を知る必要があります。どれが削除され、どれが追加されたか。"どれ";
引き続きコードを見てみましょう:
リーリーdexPatchGenerator オブジェクトでは、executeAndSaveTo メソッドを直接指します。
リーリーexecuteAndSaveTo メソッド:
リーリー15 個のアルゴリズムが関係しているため、コードは非常に長くなります。ここでは説明するためにアルゴリズムのうちの 1 つだけを使用します。
各アルゴリズムは、execute メソッドと SimulatePatchOperation メソッドを実行します:
まず実行を見てみましょう:
リーリーoldDex と newDex の対応する領域のデータが最初に読み込まれ、それぞれ調整されたOldIndexedItemsとadjustedNewIndexedItemsに並べ替えられることがわかります。
次にトラバースが始まります。else 部分を直接見てください:
現在のカーソルに従って、oldItem と newItem をそれぞれ取得し、それらの値のペアを比較します。
コードの後半に進みます:
リーリー
先ほど、execute() に加えて、各アルゴリズムには SimulatePatchOperation() があると言いました
リーリー
渡されるオフセットはデータ領域のオフセットです。リーリー
oldIndex と newIndex をトラバースし、それぞれ、indexToAddOperationMap、indexToReplaceOperationMap、indexToDelOperationMap を検索します。ここに注意してください。最終的な結果は、patchedOffset-baseOffset によって取得される this.patchedSectionSize です。
patchedOffset =itemSize:
が発生する状況はいくつかあります。
この時点で、アルゴリズムが実行されました。
このようなアルゴリズムの後、PatchOperationList と対応する領域のセクションサイズを取得します。すべてのアルゴリズムを実行した後、各アルゴリズムの PatchOperationList と各領域のセクションサイズを取得する必要があります。各領域のセクションサイズは実際には各領域のオフセットに変換されます。
各領域のアルゴリズム、実行、およびシミュレートPatchOperationのコードは再利用されているため、その他の部分には小さな変更しかありません。ご自身で確認してください。
次に、すべてのアルゴリズムを実行した後の writeResultToStream メソッドを確認します。
ここではまだ stringDataSectionDiffAlg アルゴリズムのみを確認します。
リーリーまず、patchOperationList を DEL、ADD、REPLACE に対応する 3 つの OpIndexList に変換し、すべての項目を newItemList に保存します。
次に、順番に書きます:
インデックスはここで実行されます (インデックス - lastIndex 操作はここで実行されます)
他のアルゴリズムも同様の操作を実行します。
生成したパッチがどのようなものかを確認するのが最善です:
このように見ると、Patch のロジックは次のように推測されます:
つまり、newDex の特定の領域には次のものが含まれます:
リーリーこれは非常に明確です。以下のコードを見てみましょう~
diff と同様に、古い dex ファイルとパッチ ファイルを受け入れ、最終的に新しい Dex を生成するクラスまたはメソッドが必要です。大量のセキュリティ検証コードや APK 解凍コードに引っかからないようにしてください。
このクラスは、tinker-commons では DexPatchApplier と呼ばれています。
パッチに関連するコードは次のとおりです:
リーリーdiff コードと似ていることがわかります。以下のコードを参照してください。
oldDex は Dex オブジェクトに変換されます。これは主に readHeader と readMap で上で分析されました。patchFile が DexPatchFile オブジェクトに変換されることに注意してください。
リーリーまずパッチ ファイルを byte[] として読み取り、次に init を呼び出します
リーリーパッチをどのように書いたかまだ覚えていますか? 私たちは最初に MAGIC と Version を書いてファイルがパッチ ファイルであることを確認し、次に patchedDexSize とさまざまなオフセットに値を割り当て、最後にデータ領域 (firstChunkOffset) を見つけました。書くときは、このフィールドが 4 番目の位置にあることに注意してください。
位置特定後、後から読み込むのがデータであり、保存時には以下の形式で保存されます。
リーリー
oldDex と patchFile に加えて、patchedDex も最終出力 Dex オブジェクトとして初期化されます。構築が完了すると、executeAndSaveTo メソッドが直接実行されます。
リーリー
executeAndSaveTo(os) に直接移動します。このメソッドのコードは比較的長いので、3 つの段落で説明します。 リーリー実際には、ここで、patchFileに記録された値が読み取られ、patchedDexのTableOfContent内のさまざまなSection(マップリストの各map_list_itemにほぼ対応)に割り当てられます。
並べ替えの次に、byteCount などのフィールド情報を設定します。
###続く:### リーリーこの部分は明らかに多数のアルゴリズムを初期化し、それらを個別に実行します。分析には引き続き stringDataSectionPatchAlg を使用します。
リーリー書くときのルールを投稿しましょう:
del 操作の数、各 del のインデックス
追加操作の数、各追加のインデックス
に格納されます。
追加の数、追加のすべてのインデックスは int[];これで次のようになります:
del数とインデックス
add count インデックスを追加
新しいデータ
代替データ
項目はコードを通じて記述されており、次のことがわかります:
まず、patchIndex が addIndices に含まれているかどうかを確認し、含まれている場合はそれを書き込みます。
さらに、replicaIndices にあるかどうかを判断し、含まれているかどうかを書き込みます;次に、oldIndex が削除または置換されたかどうかを判断し、直接スキップします。
これでstringData領域のパッチアルゴリズムが完成します。
残りの 14 個のアルゴリズムの実行コードは同じ (親クラス) であり、実行される操作も同様であり、パッチ アルゴリズムのすべての部分が完了します。
すべての領域が復元されると、残っているのはヘッダーとマップリストだけになるので、すべてのアルゴリズムの実行が完了した場所に戻ります。
リーリーヘッダー領域を見つけてヘッダー関連データを書き込み、マップ リスト領域を見つけてマップ リスト関連データを書き込みます。両方が完了したら、ヘッダーに 2 つの特別なフィールド (signature と checkSum) を記述する必要があります。これら 2 つのフィールドはマップ リストに依存するため、マップ リストの後に記述する必要があります。
これで完全な dex リカバリが完了し、最後にメモリ内のすべてのデータがファイルに書き込まれます。
Hello.dex ができたので、別のクラスを作成しましょう:
リーリー次に、このクラスをコンパイルして dx ファイルに入力します。
リーリーこのようにして、Hello.dex と World.dex という 2 つの dex を準備しました。
010 エディターを使用して 2 つの dex をそれぞれ開きます。主に string_id_item に焦点を当てます;
両側に 13 個の文字列があります。上で紹介した diff アルゴリズムによれば、次の操作が得られます:
両側の文字列のトラバースと比較を開始します:
その後、インデックスに従って並べ替えますが、変更はありません;
次に、すべての操作を繰り返し、一貫したインデックスと隣接する DEL と ADD を使用して操作を置換します。
リーリー最後に、書き込み時にトラバーサルが実行され、操作が DEL、ADD、REPLACE に従って分類され、表示される項目が newItemList に配置されます。
リーリーnewItemList は次のようになります:
リーリー次に書き込みます。書き込み順序は次のようになります:
リーリーここでは、DexPatchGenerator の writeResultToStream の関連位置に直接ログインします。 リーリー
出力は次のようになります:リーリー
上記の分析結果と一致しています ~~その後、他の領域も同様の方法で検証でき、パッチも同様であるため、詳細は説明しません。
以上がAndroid ホットフィックス Tinker ソースコード分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。