ホームページ  >  記事  >  システムチュートリアル  >  Android ホットフィックス Tinker ソースコード分析

Android ホットフィックス Tinker ソースコード分析

王林
王林転載
2024-03-25 09:20:24932ブラウズ

tinker の最大のハイライトの 1 つは、一連の dex diff および patch 関連のアルゴリズムを自社開発したことです。この記事の主な目的は、このアルゴリズムを分析することです。もちろん、分析の前提条件として、dex ファイルの形式をある程度理解する必要があることに注意してください。理解していないと混乱する可能性があります。

したがって、この記事では、最初に dex ファイル形式の簡単な分析を行い、いくつかの簡単な実験も行い、最後に dex diff およびパッチのアルゴリズム部分に入ります。

1. Dex ファイル形式の簡単な分析

まず第一に、Dex ファイルについて簡単に理解しましょう。逆コンパイルすると、apk に 1 つ以上の *.dex ファイルが含まれることは誰もが知っています。このファイルには、作成したコードが保存されます。通常の状況では、変換ツールも使用します。それを jar に保存し、逆コンパイルしていくつかのツールで表示します。

jar ファイルはクラス ファイルの圧縮パッケージに似ていることを誰もが知っているはずです。通常の状況では、各クラス ファイルを直接解凍して表示できます。 dex ファイルを解凍しても内部クラス ファイルを取得できません。これは、dex ファイルが独自の特定の形式を持つことを意味します:
dex は Java クラス ファイルを再配置し、すべての JAVA クラス ファイルの定数プールを分解し、冗長な情報を削除し、それらを再結合して定数プールを形成します。すべてのクラス ファイルは同じ定数プールを共有し、同じ文字列と定数を作成します。DEX 内で 1 回だけ表示されます。ファイルを削除することにより、ファイル サイズが削減されます。
次に、dex ファイルの内部構造がどのようなものかを見てみましょう。

ファイルの構成を分析するには、分析用に最も単純な dex ファイルを作成するのが最善です。

(1) dex を生成するコードを記述します

まず、クラス Hello.java:

を作成します。 リーリー

次にコンパイルします:

リーリー

最後に dx 作業を通じて dex ファイルに変換します:

リーリー

dx パスは、Android-sdk/build-tools/versionnumber/dx の下にあります。dx コマンドが認識できない場合は、パスを path の下に置くか、絶対パスを使用してください。
このようにして、非常に単純な dex ファイルを取得します。

(2) dexファイルの内部構造を確認する

まず、dex ファイルの大まかな内部構造図を示します。

Android 热修复 Tinker 源码分析

もちろん、単に画像から説明するだけでは絶対に十分ではありません。後ほど diff と patch のアルゴリズムを学習するからです。理論的には、dex ファイルの各要素に至るまで、さらに詳細を知る必要があります。バイトは何を表しますか?

バイナリのようなファイルの場合、メモリに依存しないことが最善の方法です。幸いなことに、分析に役立つソフトウェアがあります:

  • ソフトウェア名:010エディター

ダウンロードしてインストールした後、dex ファイルを開くと、dex ファイルの解析テンプレートをインストールするように案内されます。

最終的なレンダリングは次のとおりです:

Android 热修复 Tinker 源码分析

上部は dex ファイルの内容 (16 進形式で表示) を表し、下部は dex ファイルの各領域を示します。下部をクリックすると、対応するコンテンツ領域と内容が表示されます。

もちろん、dex ファイルについての理解を深めるために、いくつかの特別記事を読むことも強くお勧めします。

  • DEX ファイル形式の分析
  • Android リバース エンジニアリングの旅 - コンパイルされた Dex ファイル形式の解析

この記事では、dex ファイルの簡単な形式分析のみを行います。

(3) dexファイルの内部構造の簡易解析

dex_header
まず、dex_header の大まかな分析を行います。ヘッダーには次のフィールドが含まれます:

Android 热修复 Tinker 源码分析

まず第一に、ヘッダーの役割を推測します。ヘッダーにはいくつかの検証関連フィールドと、dex ファイル全体のブロックのおおよその分布 (off はオフセット) が含まれていることがわかります。

この利点は、仮想マシンが dex ファイルを読み取るときに、ヘッダー部分を読み取るだけで dex ファイルのおおよそのブロック分布がわかり、ファイル形式が正しいかどうか、およびファイル形式が正しいかどうかを確認できることです。ファイルが改ざんされているなど

  • ファイルが dex ファイルであることを証明できる
  • チェックサムと署名は主にファイルの整合性を検証するために使用されます
  • file_size は dex ファイルのサイズです
  • head_size はヘッダー ファイルのサイズです
  • endian_tag のデフォルト値は 12345678 で、ロゴのデフォルトはリトルエンディアン (自己検索) です。

残りのほとんどは、ペアで表示されるサイズとオフであり、そのほとんどは、各ブロックに含まれる特定のデータ構造の数とオフセットを表します。例: string_ids_off は 112、つまり string_ids 領域がオフセット 112 から始まることを意味し、string_ids_size は 14、つまり string_id_item の数が 14 であることを意味します。他も同様なので紹介は省略します。

010Editorと組み合わせると、各エリアに含まれるデータ構造とそれに対応する値が確認できますので、ぜひご覧ください。

dex_map_list

ヘッダーの他に、dex_map_list という重要な部分があります。最初に図を見てみましょう:

Android 热修复 Tinker 源码分析

最初はmap_item_listの番号で、その後に各map_item_listの説明が続きます。

map_item_list は何に役立ちますか?

Android 热修复 Tinker 源码分析

各map_list_itemには、列挙型、2バイトの未使用メンバー、現在の型の番号を示すサイズ、および現在の型のオフセットを示すオフセットが含まれていることがわかります。

次の例を見てみましょう:

  • 最初は TYPE_HEADER_ITEM タイプで、1 つのヘッダー (サイズ = 1) が含まれ、オフセットは 0 です。
  • 次は TYPE_STRING_ID_ITEM で、これには 14 個の string_id_item (size=14) が含まれており、オフセットは 112 です (覚えていると思いますが、ヘッダーの長さは 112 で、その後にヘッダーが続きます)。

残りは順番に推測できます~~

この場合、map_list により、完全な dex ファイルが固定領域 (この例では 13) に分割でき、各領域の開始とその領域に対応するデータ形式の数がわかることがわかります。 。

map_list を通じて各エリアの先頭を見つけます。各エリアは特定のデータ構造に対応します。010 Editor を通じて表示するだけです。

2. 分析前の考察

dex の基本的な形式を理解したので、dex diff と patch を行う方法を考えてみましょう。

最初に考慮すべきことは、私たちが持っているものです:

  1. 古いデックス
  2. 新しいデックス

古い dex を使用したパッチ アルゴリズムを通じて新しい dex も生成できるパッチ ファイルを生成したいと考えています。

    ###だから何をすべきか?
上記の分析に基づいて、dex ファイルには大まかに 3 つの部分があることがわかります (ここでの 3 つの部分は主に分析に使用されます。真剣に考えないでください)。

###ヘッダ###
    さまざまな分野
  1. マップリスト
  2. ヘッダーは実際に次のデータに基づいてその内容を決定でき、112 の固定長です。各領域については後述します。マップ リストは実際に各領域の開始位置を特定できます。
最終的に、上記の 3 つの部分に対して古い dex -> 新しい dex; にパッチを適用します。

ヘッダーは他のデータに基づいて生成できるため、処理する必要はありません;

マップリストの場合、主に必要なのは各エリアの開始(オフセット)です
  • 各領域のオフセットがわかったら、新しい dex を生成するときに、各領域の開始位置と終了位置を特定できるので、各領域にデータを書き込むだけで済みます。
  • 次に、領域の差分を見てみましょう。主に文字列を格納するために使用される文字列領域があるとします。
  • 古い dex のこの領域の文字列は次のとおりです: Hello、World、zhy
  • 新しい dex のこの領域の文字列は次のとおりです: Android、World、zhy

この領域では、Hello を削除し、Android を追加したことがわかります。

その後、パッチは次のようにこの領域を記録できます:
「del Hello, add Android」(実際にはバイナリに変換する必要があります)。

アプリケーションで直接読み取ることができる古い dex について考えてみましょう。つまり、次のことがわかります。

この領域には、Hello、World、zhyが含まれていることが判明しました。
パッチのこの領域には次の内容が含まれます:「del Hello, add Android」

次に、新しい dex に次のものが含まれることを非常に簡単に計算できます:
  • Android、世界、zhy。
  • このようにして、1 つの領域の大まかな diff と patch のアルゴリズムが完成しましたが、他の領域の diff と patch も上記と同様です。
こうして見ると、diff や patch のアルゴリズムはそれほど複雑ではないと思いませんか? 実際、tinker のアプローチも上記と同様です。実際の状況は上記の説明よりも複雑になる可能性がありますが、基本的には次のとおりです。同じ。

アルゴリズムの一般的な概念を理解したら、ソース コードを見てみましょう。

3. Tinker DexDiff ソース コードの簡単な分析

ここにはコードを読むためのコツがあります。実際にはかなり多くのいじくりコードがあり、コードの束にはまってしまうことがよくあります。 diff アルゴリズムのように、入力パラメータは古い dex と新しい dex で、出力はパッチ ファイルです。

その場合、上記のパラメータを受け入れて出力するクラスまたはメソッドが存在する必要があります。実際、このクラスは DexPatchGenerator:

diff の API 使用コードは次のとおりです:

リーリー

コードは tinker-build の tinker-patch-lib の下にあります。

単体テストまたはメイン メソッドを作成します。上記のコード行は diff アルゴリズムです。

コードを見るときは、ターゲットを絞る必要があります。たとえば、差分アルゴリズムを見る場合は、差分アルゴリズムへの入り口を見つけます。gradle プラグインでは心配する必要はありません。

(1)dex ファイル => Dex
リーリー

渡した 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)デックス差

ソース コードに戻ります:

リーリー

2 つの Dex オブジェクトのコンストラクターに直接:

リーリー

最初に oldDex と newDex に値を割り当て、次に 15 個のアルゴリズムを順番に初期化することを確認してください。各アルゴリズムは各領域を表します。アルゴリズムの目的は前に説明したとおりです。「どのアルゴリズムが」を知る必要があります。どれが削除され、どれが追加されたか。"どれ";

引き続きコードを見てみましょう:

リーリー

dexPatchGenerator オブジェクトでは、executeAndSaveTo メソッドを直接指します。

リーリー

executeAndSaveTo メソッド:

リーリー

15 個のアルゴリズムが関係しているため、コードは非常に長くなります。ここでは説明するためにアルゴリズムのうちの 1 つだけを使用します。

各アルゴリズムは、execute メソッドと SimulatePatchOperation メソッドを実行します:

まず実行を見てみましょう:

リーリー

oldDex と newDex の対応する領域のデータが最初に読み込まれ、それぞれ調整されたOldIndexedItemsとadjustedNewIndexedItemsに並べ替えられることがわかります。

次にトラバースが始まります。else 部分を直接見てください:

現在のカーソルに従って、oldItem と newItem をそれぞれ取得し、それらの値のペアを比較します。

    >0 の場合、newItem は新しく追加されたとみなされ、PatchOperation.OP_ADD として記録され、newItem のインデックスと値が PatchOperation オブジェクトに記録され、patchOperationList に追加されます。
  • =0 の場合、PatchOperation は生成されません。
上記の後、patchOperationList オブジェクトを取得しました。

コードの後半に進みます:

リーリー

    まず、インデックスに従って patchOperationList を並べ替えます。インデックスが一貫している場合は、最初に DEL を実行し、次に ADD を実行します。
  1. すべての操作の次の反復では、主に、一貫したインデックスと連続性を持つ DEL と ADD を REPLACE 操作に変換します。
  2. 最後に、patchOperationList は 3 つのマップ、つまり、indexToDelOperationMap、indexToAddOperationMap、indexToReplaceOperationMap に変換されます。
わかりました。実行が完了すると、私たちの主な製品は 3 つのマップで、oldDex のどのインデックスを削除する必要があるか、どの項目を newDex に追加したか、どの項目を新しい項目に置き換える必要があるかをそれぞれ記録します。

先ほど、execute() に加えて、各アルゴリズムには SimulatePatchOperation() があると言いました

リーリー

渡されるオフセットはデータ領域のオフセットです。

リーリー

oldIndex と newIndex をトラバースし、それぞれ、indexToAddOperationMap、indexToReplaceOperationMap、indexToDelOperationMap を検索します。

ここに注意してください。最終的な結果は、patchedOffset-baseOffset によって取得される this.patchedSectionSize です。

patchedOffset =itemSize:
が発生する状況はいくつかあります。

    indexToAddOperationMap には patchIndex が含まれています
  1. indexToReplaceOperationMap には patchIndex が含まれています
  2. oldDex.
  3. これは、indexToDelOperationMap および IndexToReplaceOperationMap にありません
実は、わかりやすいのですが、このpatchedSectionSizeというのが、実はnewDexのこの領域のサイズに相当するのです。そのため、OLD ITEMSには、追加が必要な案件、置換される案件、削除または置換されていない案件が含まれます。この3つを加えたものがnewDexのitemListです。

この時点で、アルゴリズムが実行されました。

このようなアルゴリズムの後、PatchOperationList と対応する領域のセクションサイズを取得します。すべてのアルゴリズムを実行した後、各アルゴリズムの PatchOperationList と各領域のセクションサイズを取得する必要があります。各領域のセクションサイズは実際には各領域のオフセットに変換されます。

各領域のアルゴリズム、実行、およびシミュレートPatchOperationのコードは再利用されているため、その他の部分には小さな変更しかありません。ご自身で確認してください。

次に、すべてのアルゴリズムを実行した後の writeResultToStream メソッドを確認します。

(3) パッチファイルの生成
リーリー
  • 最初に MAGIC を書きました。CURRENT_VERSION は主に、ファイルが合法的な修正パッチ ファイルであることを確認するために使用されます。
  • 次に、patchedDexSizeを記述します
  • 4 番目のビットはデータ領域のオフセットに書き込まれますが、最初に 0 局の位置が使用され、マップ リストに関連するすべてのオフセットが書き込まれた後、現在位置が書き込まれていることがわかります。
  • 次に、マップリストの各領域に関連するすべてのオフセットを書き込みます (ここでは各領域の順序は重要ではありません。同じように読み書きするだけです)
  • その後、各アルゴリズムを実行して、対応する領域に情報を書き込みます
  • 最後にパッチファイルを生成します

ここではまだ stringDataSectionDiffAlg アルゴリズムのみを確認します。

リーリー

まず、patchOperationList を DEL、ADD、REPLACE に対応する 3 つの OpIndexList に変換し、すべての項目を newItemList に保存します。

次に、順番に書きます:

  1. del 操作の数、各 del のインデックス
  2. 追加操作の数、各追加のインデックス
  3. 置換操作の数、置換が必要な各インデックス
  4. 最後にnewItemListを書きます。

インデックスはここで実行されます (インデックス - lastIndex 操作はここで実行されます)

他のアルゴリズムも同様の操作を実行します。

生成したパッチがどのようなものかを確認するのが最善です:

  1. 最初に、あなたが改造パッチであることを証明するためにいくつかのフィールドを含めます
  2. newDex の各領域を生成するためのオフセットが含まれています。つまり、newDex を複数の領域に分割して開始点に配置できます。
  3. newDex の各領域に、削除されたインデックス (oldDex)、新しいインデックスと値、およびアイテムの置換されたインデックスと値が含まれます

このように見ると、Patch のロジックは次のように推測されます:

  1. まず各エリアのオフセットに基づいて各エリアの開始点を決定します
  2. oldDex の各領域の項目を読み取り、パッチに従って oldDex で削除および置換する必要がある項目を削除し、新しい項目と置換された項目を追加して、の領域の項目を形成します新しい古い。

つまり、newDex の特定の領域には次のものが含まれます:

リーリー

これは非常に明確です。以下のコードを見てみましょう~

4. Tinker DexPatch ソース コードの簡単な分析
(1) 入り口を探す

diff と同様に、古い dex ファイルとパッチ ファイルを受け入れ、最終的に新しい Dex を生成するクラスまたはメソッドが必要です。大量のセキュリティ検証コードや APK 解凍コードに引っかからないようにしてください。

このクラスは、tinker-commons では DexPatchApplier と呼ばれています。

パッチに関連するコードは次のとおりです:

リーリー

diff コードと似ていることがわかります。以下のコードを参照してください。

(2) ソースコード解析
リーリー

oldDex は Dex オブジェクトに変換されます。これは主に readHeader と readMap で上で分析されました。patchFile が DexPatchFile オブジェクトに変換されることに注意してください。

リーリー

まずパッチ ファイルを byte[] として読み取り、次に init を呼び出します

リーリー

パッチをどのように書いたかまだ覚えていますか? 私たちは最初に MAGIC と Version を書いてファイルがパッチ ファイルであることを確認し、次に patchedDexSize とさまざまなオフセットに値を割り当て、最後にデータ領域 (firstChunkOffset) を見つけました。書くときは、このフィールドが 4 番目の位置にあることに注意してください。

位置特定後、後から読み込むのがデータであり、保存時には以下の形式で保存されます。

    del 操作の数、各 del のインデックス
  1. 追加操作の数、各追加のインデックス
  2. 置換操作の数、置換が必要な各インデックス
  3. 最後にnewItemListを書きます。
ソースコード分析を続けていることを簡単に思い出してください。

リーリー

oldDex と patchFile に加えて、patchedDex も最終出力 Dex オブジェクトとして初期化されます。

構築が完了すると、executeAndSaveTo メソッドが直接実行されます。

リーリー

executeAndSaveTo(os) に直接移動します。このメソッドのコードは比較的長いので、3 つの段落で説明します。 リーリー

実際には、ここで、patchFileに記録された値が読み取られ、patchedDexのTableOfContent内のさまざまなSection(マップリストの各map_list_itemにほぼ対応)に割り当てられます。

並べ替えの次に、byteCount などのフィールド情報を設定します。

###続く:### リーリー

この部分は明らかに多数のアルゴリズムを初期化し、それらを個別に実行します。分析には引き続き stringDataSectionPatchAlg を使用します。

リーリー

書くときのルールを投稿しましょう:

del 操作の数、各 del のインデックス

追加操作の数、各追加のインデックス
  1. 置換操作の数、置換が必要な各インデックス
  2. 最後にnewItemListを書きます。
  3. コードを見ると、読み取り順序は次のとおりです。
del の数、del のすべてのインデックスは int[];

に格納されます。

追加の数、追加のすべてのインデックスは int[];
    に格納されます。
  1. 置換の数、置換のすべてのインデックスは int[];
  2. に格納されます。
  3. それは書かれたときと同じですか?
  4. 続行して、oldDex の oldItems と oldItemCount を取得します。

これで次のようになります:

del数とインデックス

add count インデックスを追加
  1. カウントとインデックスを置き換える
  2. oldItems と oldItemCount
  3. 今あるものをそのまま使用して、doFullPatch の実行を続けます
  4. リーリー
  5. 全体を見てみましょう。ここでの目的は、patchedDex の stringData 領域にデータを書き込むことです。書き込まれたデータは、理論的には次のようになります。

新しいデータ

代替データ

    oldDex の新しいデータと置換されたデータ
  1. もちろん、それらは連続して書き込む必要があります。
  2. コードを見ると、最初に newItemCount=oldItemCount addCount - delCount を計算してから走査を開始します。走査条件は 0~oldItemCount または 0~newItemCount です。
私たちが期待しているのは、対応する項目が patchIndex に 0 から newItemCount まで書き込まれることです。

項目はコードを通じて記述されており、次のことがわかります:

まず、patchIndex が addIndices に含まれているかどうかを確認し、含まれている場合はそれを書き込みます。

さらに、replicaIndices にあるかどうかを判断し、含まれているかどうかを書き込みます;

次に、oldIndex が削除または置換されたかどうかを判断し、直接スキップします。
    その後、最後のインデックスは、削除と置換が行われていない oldIndex を参照します。これは、newDex の項目と同じ部分です。
  1. 1.2.4 の上記 3 つの部分は、完全な newDex のこの領域を形成できます。
  2. これでstringData領域のパッチアルゴリズムが完成します。

    残りの 14 個のアルゴリズムの実行コードは同じ (親クラス) であり、実行される操作も同様であり、パッチ アルゴリズムのすべての部分が完了します。

    すべての領域が復元されると、残っているのはヘッダーとマップリストだけになるので、すべてのアルゴリズムの実行が完了した場所に戻ります。

    リーリー

    ヘッダー領域を見つけてヘッダー関連データを書き込み、マップ リスト領域を見つけてマップ リスト関連データを書き込みます。両方が完了したら、ヘッダーに 2 つの特別なフィールド (signature と checkSum) を記述する必要があります。これら 2 つのフィールドはマップ リストに依存するため、マップ リストの後に記述する必要があります。

    これで完全な dex リカバリが完了し、最後にメモリ内のすべてのデータがファイルに書き込まれます。

    5. 事件の簡単な分析
    (1) dexの準備

    Hello.dex ができたので、別のクラスを作成しましょう:

    リーリー

    次に、このクラスをコンパイルして dx ファイルに入力します。

    リーリー

    このようにして、Hello.dex と World.dex という 2 つの dex を準備しました。

    (2) 差分

    010 エディターを使用して 2 つの dex をそれぞれ開きます。主に string_id_item に焦点を当てます;

    Android 热修复 Tinker 源码分析

    両側に 13 個の文字列があります。上で紹介した diff アルゴリズムによれば、次の操作が得られます:

    両側の文字列のトラバースと比較を開始します:

  • >0 の場合、newItem は新しく追加されたとみなされ、PatchOperation.OP_ADD として記録され、newItem のインデックスと値が PatchOperation オブジェクトに記録され、patchOperationList に追加されます。
  • =0 の場合、PatchOperation は生成されません。
リーリー

その後、インデックスに従って並べ替えますが、変更はありません;

次に、すべての操作を繰り返し、一貫したインデックスと隣接する DEL と ADD を使用して操作を置換します。

リーリー

最後に、書き込み時にトラバーサルが実行され、操作が DEL、ADD、REPLACE に従って分類され、表示される項目が newItemList に配置されます。

リーリー

newItemList は次のようになります:

リーリー

次に書き込みます。書き込み順序は次のようになります:

リーリー

ここでは、DexPatchGenerator の writeResultToStream の関連位置に直接ログインします。 リーリー

出力は次のようになります:

リーリー

上記の分析結果と一致しています ~~

その後、他の領域も同様の方法で検証でき、パッチも同様であるため、詳細は説明しません。

以上がAndroid ホットフィックス Tinker ソースコード分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はlinuxprobe.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。