Android Dexファイルについて

11148 ワード

概要
なぜDexファイルを理解するのか
Dexファイルを理解してから、日常的な開発でいくつかの問題が発生したことをもっと理解することができます.例えば、APKのダイエット、熱修復、プラグイン化、応用強化、Android逆工程、64 K方法数制限.
Dexファイルとは
Dexファイルとは何かを理解する前に、JVM、Dalvik、ARTについて理解しておきます.JVMはJAVA仮想マシンで、JAVAバイトコードプログラムを実行します.DalvikはGoogleが設計したAndroidプラットフォーム用のランタイム環境で、モバイル環境でメモリとプロセッサの速度が限られているシステムに適しています.ARTであるAndroid Runtimeは、GoogleがDalvikが設計した新しいAndroidランタイム環境に置き換えるため、Android 4.4で発売した.ARTはDalvikより性能が良い.AndroidプログラムはJava言語で開発されるのが一般的ですが、Dalvik仮想マシンではJAVAバイトコードの直接実行はサポートされていませんので、コンパイルによって生成された.classファイルを翻訳、再構築、解釈、圧縮などの処理を行います.この処理過程はdxで処理され、処理が完了した後に生成された生成物は.dexで終わり、Dexファイルと呼ばれます.DexファイルフォーマットはDalvik専用に設計された圧縮フォーマットです.したがって、Dexファイルは多くの.classファイル処理後の生成物であり、最終的にAndroidランタイム環境で実行できると簡単に理解できます.
Dexファイルはどのように生成されますか?
JAvaコードがdexファイルに変換されるプロセスは図に示すように、もちろん本当に処理プロセスはそんなに簡単ではありません.ここではイメージの表示にすぎません.
注意:画像はネットワークから
次に、javaコードからdexファイルへの変換を簡単な例で実現します.
.javaから.classへ
まずHello.javaファイルを作成し、分析を容易にするために簡単なコードを書きます.コードは次のとおりです.
public class Hello {
    private String helloString = "hello! youzan";

    public static void main(String[] args) {
        Hello hello = new Hello();
        hello.fun(hello.helloString);
    }

    public void fun(String a) {
        System.out.println(a);
    }
}

このファイルの兄弟ディレクトリの下でJDKのjavacを使用してこのjavaファイルをコンパイルします.
javac Hello

JAvacコマンドが実行されると、現在のディレクトリでHello.classファイルが生成されます.Hello.classファイルはJVM仮想マシン上で直接実行できます.ここではコマンドを使用してファイルを実行します.
java Hello

実行するとコンソールに「hello!youzan」と印刷されるはずです.
ここではHello.classファイルに対してjavapコマンドを実行し、逆アセンブリを行うこともできます.
javap -c Hello

実行結果は次のとおりです.
public class Hello {
  public Hello();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: aload_0
       5: ldc           #2                  // String hello! youzan
       7: putfield      #3                  // Field helloString:Ljava/lang/String;
      10: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #4                  // class Hello
       3: dup
       4: invokespecial #5                  // Method "":()V
       7: astore_1
       8: aload_1
       9: aload_1
      10: getfield      #3                  // Field helloString:Ljava/lang/String;
      13: invokevirtual #6                  // Method fun:(Ljava/lang/String;)V
      16: return

  public void fun(java.lang.String);
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_1
       4: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       7: return
}

ここで、コードの後には、JVM仮想マシンが実行するための具体的な命令が表示されます.命令の具体的な意味はJAVA公式文書を参照してください.
.classから.dexへ
上記で生成した.classファイルは既にJVM環境で実行可能であるが、Androidランタイム環境で実行するには特別な処理が必要である場合、それはdx処理であり、.classファイルの翻訳、再構築、解釈、圧縮などの操作を行う.
dx処理はツールdx.jarに使用されます.このファイルはSDKにあります.具体的なディレクトリは、SDKルートディレクトリ/build-tools/任意のバージョンにあります.上で生成したHello.classファイルをdxツールで処理し、Hello.classのディレクトリの下で次のコマンドを使用します.
dx --dex --output=Hello.dex Hello.class

実行が完了すると、現在のディレクトリの下にHello.dexファイルが生成されます.この.dexファイルはAndroidランタイム環境で直接実行でき、一般的にPathClassLoaderでdexファイルをロードすることができます.現在のディレクトリの下でdexdumpネーミングを実行して逆コンパイルします.
dexdump -d Hello.dex

実行結果は次のとおりです(一部の領域の意味は以下に説明します).
Processing 'Hello.dex'...
Opened 'Hello.dex', DEX version '035'

------        Hello.java       ------
Class #0            -
  Class descriptor  : 'LHello;'
  Access flags      : 0x0001 (PUBLIC)
  Superclass        : 'Ljava/lang/Object;'
  Interfaces        -
  Static fields     -
  Instance fields   -
    #0              : (in LHello;)
      name          : 'helloString'
      type          : 'Ljava/lang/String;'
      access        : 0x0002 (PRIVATE)

------                。7010 0400 0100 1a00 0b00                    。Dalvik     16     ,      4      ,     16  。invoke-direct              ,             。               https://source.android.com/devices/tech/dalvik/instruction-formats    ------
  Direct methods    -
    #0              : (in LHello;) 
      name          : '' ---     :            ---
      type          : '()V' ---     ,()      ,()       ,V  void---
      access        : 0x10001 (PUBLIC CONSTRUCTOR) ---        ---
      code          -
      registers     : 2  ---            ---
      ins           : 1  ---     ,             ,              ---
      outs          : 1 
      insns size    : 8 16-bit code units  ---      ---
000148:                                        |[000148] Hello.:()V
000158: 7010 0400 0100                         |0000: invoke-direct {v1}, Ljava/lang/Object;.:()V // method@0004
00015e: 1a00 0b00                              |0003: const-string v0, "hello! youzan" // string@000b
000162: 5b10 0000                              |0005: iput-object v0, v1, LHello;.helloString:Ljava/lang/String; // field@0000
000166: 0e00                                   |0007: return-void
      catches       : (none)
      positions     :
        0x0000 line=1
        0x0003 line=2
      locals        :
        0x0000 - 0x0008 reg=1 this LHello;

    #1              : (in LHello;)
      name          : 'main'
      type          : '([Ljava/lang/String;)V'
      access        : 0x0009 (PUBLIC STATIC)
      code          -
      registers     : 3
      ins           : 1
      outs          : 2
      insns size    : 11 16-bit code units
000168:                                        |[000168] Hello.main:([Ljava/lang/String;)V
000178: 2200 0000                              |0000: new-instance v0, LHello; // type@0000
00017c: 7010 0000 0000                         |0002: invoke-direct {v0}, LHello;.:()V // method@0000
000182: 5401 0000                              |0005: iget-object v1, v0, LHello;.helloString:Ljava/lang/String; // field@0000
000186: 6e20 0100 1000                         |0007: invoke-virtual {v0, v1}, LHello;.fun:(Ljava/lang/String;)V // method@0001
00018c: 0e00                                   |000a: return-void
      catches       : (none)
      positions     :
        0x0000 line=5
        0x0005 line=6
        0x000a line=7
      locals        :

  Virtual methods   -
    #0              : (in LHello;)
      name          : 'fun'
      type          : '(Ljava/lang/String;)V'
      access        : 0x0001 (PUBLIC)
      code          -
      registers     : 3
      ins           : 2
      outs          : 2
      insns size    : 6 16-bit code units
000190:                                        |[000190] Hello.fun:(Ljava/lang/String;)V
0001a0: 6200 0100                              |0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@0001
0001a4: 6e20 0300 2000                         |0002: invoke-virtual {v0, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V // method@0003
0001aa: 0e00                                   |0005: return-void
      catches       : (none)
      positions     :
        0x0000 line=10
        0x0005 line=11
      locals        :
        0x0000 - 0x0006 reg=1 this LHello;

  source_file_idx   : 1 (Hello.java)

ここまでJavaコードをDalvik実行可能ファイルであるdexに変換することは完了している.
Dexファイルの具体的なフォーマット
今、Dexファイルの具体的なフォーマットを分析してみましょう.MP 3、MP 4、JPG、PNGファイルのように、Dexファイルにも独自のフォーマットがあり、これらのフォーマットを守ってこそ、Android実行時の環境に正しく認識されます.
Dexファイル全体のレイアウトを下図に示します.これらの領域のデータは相互に関連し、相互に参照されます.紙面の都合で、ここには一部のエリアの関連が表示されているだけなので、完全に公式サイトに行って自分で関連データを見て整理してください.下図の各フィールドは、後述する各領域の詳細について説明します.
ファイルヘッダ、インデックス領域、クラス定義領域について簡単に説明します.他のエリアはAndroidの公式サイトで知ることができます.
ファイルヘッダ
ファイルヘッダ領域は、このファイルをどのように読み込むかを決定します.具体的なフォーマットは次の表(ファイルに並べられた順序は次の表の順序)です.
id領域
id領域には文字列,type,prototype,field,methodリソースの真のデータのファイル内のオフセット量が格納されており,id領域のオフセット量に基づいてそのidに対応する真のデータを見つけることができる.
文字列id領域
このブロックはオフセット量リストであり、各オフセット量は真の文字列リソースに対応し、各オフセット量は32ビットを占める.オフセット量によって対応する実際の文字列データを見つけることができます.具体的なフォーマットは、最終的にこのオフセットの位置がデータ領域に落ちるべきです.このオフセット量の場所を見つけたら、次のフォーマットに従って、この文字列リソースの具体的なデータを読み出すことができます.
タイプid領域
このブロックはインデックスリストであり、インデックスの値は文字列id領域オフセット量リストのいずれかに対応する.データフォーマットは、あるタイプの値を見つけるには、まずタイプidリストのインデックス値に基づいて文字列idリストに対応する項目を見つける必要があります.この格納されたオフセット量に対応する文字列リソースは、このタイプの文字列記述です.
メソッドプロトタイプid領域
このブロックはメソッドプロトタイプidリストであり、データフォーマットは:
メンバーid領域
このブロックには、プロトタイプidリストが格納され、データフォーマットは次のとおりです.
メソッドid領域
このブロックにはメソッドidリストが格納され、データフォーマットは:このブロックにはプロトタイプidリストが格納され、データフォーマットは:
クラス定義領域
この領域にはクラス定義のリストが格納されています.具体的なデータ構造は次のとおりです.
dexファイルを解析するツール
ここではdexファイルを解析できるツール010 Editorをお勧めします.プリセットテンプレートを使用すると、dexファイルのフォーマットをより明確に理解できます.
Android Tinkerの熱修復におけるDexファイルの応用
現在主流のAndroidホットリペア案では、Tinkerは無料、オープンソース、ユーザー数が多いなどの利点があるため、Tinkerに基づいてAndroidホットリペアサービスを構築している.Tinkerホットリペアの主な原理は、古いAPKのdexファイルと新しいAPKのdexファイルを比較することによってパッチパッケージを生成し、APPでパッチパッケージを通じて古いAPKのdexファイルと新しいdexファイルを合成することである.次の図に流れを示します.
注:画像はTinkerの公式サイトから
パッチの生成
Tinkerの公式使用は、DexDiffの合成案です.Dexファイル形式の特性に基づいており、パッチパッケージが小さく、メモリ消費が小さいなどの利点があります.DexDiffアルゴリズムでは、Dexファイルのフォーマットに基づいて、Dexファイルを異なるブロッククラスに分類します.以下の図です.これらのブロックには統一されたデータ構造があり、主なデータにはブロックに対応する実際のデータ型とファイル内のオフセット量があります.ブロックデータの実データ型とオフセット量があり、実データ型に対応するデータ構造に基づいて、このブロックに含まれる実データをファイルから読み出すことができる.ここでヘッダ領域を例にとると、読み出しコードは以下のようになります(一部の無関係コードが削除され、コードは上記のDexファイル形式のファイルヘッダの紹介を参照できます).
private void readHeader(Dex.Section headerIn) throws UnsupportedEncodingException {
 byte[] magic = headerIn.readByteArray(8); 
 int apiTarget = DexFormat.magicToApi(magic);
 checksum = headerIn.readInt(); 
 signature = headerIn.readByteArray(20);
 fileSize = headerIn.readInt();
 int headerSize = headerIn.readInt();
 int endianTag = headerIn.readInt();
 linkSize = headerIn.readInt();
 linkOff = headerIn.readInt();
 mapList.off = headerIn.readInt();
 stringIds.size = headerIn.readInt();
 stringIds.off = headerIn.readInt();
 typeIds.size = headerIn.readInt();
 typeIds.off = headerIn.readInt();
 protoIds.size = headerIn.readInt();
 protoIds.off = headerIn.readInt();
 fieldIds.size = headerIn.readInt();
 fieldIds.off = headerIn.readInt();
 methodIds.size = headerIn.readInt();
 methodIds.off = headerIn.readInt();
 classDefs.size = headerIn.readInt();
 classDefs.off = headerIn.readInt();
 dataSize = headerIn.readInt();
 dataOff = headerIn.readInt();
}

ファイルから新旧Dexファイルの各ブロックの具体的なデータを読み込んで、比較してパッチを生成することができます.各ブロックのデータ構造が一致しないため、各ブロックは対応するdiffアルゴリズムを持って各ブロックパッチの生成と合成を処理する.アルゴリズムのリストは図のようです:これらのアルゴリズムは新旧のDexファイルがデータ構造に変換された後のデータの違いを比較して、それから関連する操作命令を生成して、パッチファイルに保存して、クライアントに送ります.
パッチの合成
クライアントはパッチファイルを受け取った後、同じ読み取り方式で旧Dexファイルを関連データ構造に変換し、パッチパッケージの操作命令を使用して旧Dexデータを修正し、新Dexデータを生成し、最後にデータをファイルに書き込み、新Dexファイルを生成し、パッチの合成を完了する.
最後に書く
本文は特に深いことを書いていないし、dexのファイルフォーマットについても完全に説明していない.主にdexファイルの概略構造を共有し、実際の応用もあります.後で関連する問題に遭遇したとき、dexファイルを理解し、問題を解決する方向があります.最後に、何かアドバイスや意見があれば、フィードバックを歓迎します.
リファレンスリソース
  • Android公式資料
  • Tinker紹介
  • DalvikとJavaバイトコードの対比