ホットリペアHotfixシリーズ(2)—MultiDex:パパ!

17464 ワード

0 x 01開編
公式文書MultiDexの説明:
1.Dalvik Executable(DEX)ファイルの合計メソッド数は65536以内に制限されています.Android framwork method、lib method、そしてあなたのcode methodが含まれていますので、MultiDexを使用してください.2.5.0以降のバージョンではmultidex support libraryを使用します.3.5.0以上のバージョンでは、ARTモードが存在するため、appの最初のインストール後にプリコンパイル(pre-compilation)が行われ、classes(..N).dexファイルの存在が発見されると、最終的に.oatのファイルに合成されます.
公式文書にはmultiDex support libの使用限界も明記されています.
1.アプリケーションが携帯電話にインストールされている間にdexファイルのインストールが複雑になる(complex)2番目のdexファイルが大きすぎるためANRになる可能性があります.proguardでコードを最適化してください.2.mulitDexを使用したAppは、Dalvik linearAllocバグのため、4.0(api level 14)以前のマシンで起動できない可能性があります.proguardでコードを最適化すると、バグの確率が減少します.3.mulitDexを使用したAppはruntime期間中にDalvik linearAlloc limit Crashのために使用される可能性があります.このメモリ割り当て制限は4.0バージョンで大きくなりますが、5.0以下のマシンのAppsにはこの制限があります.4.マスターdexがdalvik仮想マシンによって実行される場合、どのクラスがマスターdexファイルに存在しなければならないかという問題は複雑です.build toolsはこの問題を解決することができます.しかし、コードに反射とnativeの呼び出しがある場合も、100%正しいことは保証されません.
ちょっと穴があいているように見えますが、放っておいて、買春してからお金をあげて、流れ全体を見てみましょう.
0 x 02好戏开始了
Android 5.0の前にapp全体に1つのdexがあり、このdexのロードはインストール時にロードされたが、このdexのメソッド数は65535に制限されている.
MultiDexを導入すると、1つのプライマリdexと複数の分割dex(classes 1.dex,classes 2.dex,classesN.dex)に分けられ、アプリケーションの最初のインストールでプライマリdexがロードされ、2番目以降のdexではApplicationのattachBaseContext()でMultiDex.install(this)が呼び出されたときに解凍とインストールが行われます【インストールのコンセプトは、元のapkファイルに含まれるdexパッケージを解凍して最適化し、最適化ディレクトリをアーカイブすることであり、アプリケーションが次回起動して特定のディレクトリの下でこのdexパッケージを見つけてClassLoaderの初期化を行うことができるようにする】
この方法では主に2つのことをしました.次に、ソースコードで分析します.
public static void install(Context context) {
        Log.i("MultiDex", "Installing application");
        if (IS_VM_MULTIDEX_CAPABLE) {
            Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
        } else if (VERSION.SDK_INT < 4) {
            throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
        } else {
            try {
                ApplicationInfo applicationInfo = getApplicationInfo(context);
                if (applicationInfo == null) {
                    Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
                    return;
                }

                doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "");
            } catch (Exception var2) {
                Log.e("MultiDex", "MultiDex installation failure", var2);
                throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
            }

            Log.i("MultiDex", "install done");
        }
    }

まず、アプリケーションInfo.sourceDirの2つのファイルパスをApplicationで取得します.
【/data/app/com.hotfix.sample-JEOYk3NmgdLD31DnXH25EQ==/base.apk】
applicationInfo.dataDir
【/data/user/0/com.hotfix.sample】
次にメソッドdoInstallation()を呼び出し、doInstallation()メソッドを参照します.
private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
        Set var5 = installedApk;
        synchronized(installedApk) {
            if (!installedApk.contains(sourceApk)) {
                installedApk.add(sourceApk);
                if (VERSION.SDK_INT > 20) {
                    Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + VERSION.SDK_INT + ": SDK version higher than " + 20 + " should be backed by " + "runtime with built-in multidex capabilty but it's not the " + "case here: java.vm.version=\"" + System.getProperty("java.vm.version") + "\"");
                }

                //     ClassLoader  ,       ,     dex     ,  DexFile       ClassLoader   
                ClassLoader loader;
                try {
                    loader = mainContext.getClassLoader();
                } catch (RuntimeException var11) {
                    Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var11);
                    return;
                }

                if (loader == null) {
                    Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
                } else {
                    try {
                        //  /data/user/0/pkgName/files/code_cache/secondary-dexes   
                        clearOldDexDir(mainContext);
                    } catch (Throwable var10) {
                        Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var10);
                    }

                    //   /data/user/0/pkgName/code_cache/secondary-dexes  
                    //      /data/user/0/pkgName/files/code_cache/secondary-dexes
                    //            clearOldDexDir   
                    File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
                    //  Dex    
                    List extends File> files = MultiDexExtractor.load(mainContext, sourceApk, dexDir, prefsKeyPrefix, false);
                    //    
                    installSecondaryDexes(loader, dexDir, files);
                }
            }
        }
    }

MultiDex.install(Context)の過程で、肝心なステップはMultiDexExtractor#load方法とMultiDex#installSecondaryDexes方法で、MultiDexExtractor#load方法が何をしたのかを見続けます.
    static List extends File> load(Context context, File sourceApk, File dexDir, String prefsKeyPrefix, boolean forceReload) throws IOException {
        Log.i("MultiDex", "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " + prefsKeyPrefix + ")");
        long currentCrc = getZipCrc(sourceApk);//    Apk   crc 
        File lockFile = new File(dexDir, "MultiDex.lock");
        RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
        FileChannel lockChannel = null;
        FileLock cacheLock = null;
        IOException releaseLockException = null;

        List files;
        try {
            lockChannel = lockRaf.getChannel();
            Log.i("MultiDex", "Blocking on lock " + lockFile.getPath());
            cacheLock = lockChannel.lock();//     ,       
            Log.i("MultiDex", lockFile.getPath() + " locked");
            //            ,               dex  ,             
            //   ,  crc       ,    Apk       (    ),          dex  
            if (!forceReload && !isModified(context, sourceApk, currentCrc, prefsKeyPrefix)) {
                try {
                    //      dex  
                    files = loadExistingExtractions(context, sourceApk, dexDir, prefsKeyPrefix);
                } catch (IOException var21) {
                    Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var21);
                    //          
                    files = performExtractions(sourceApk, dexDir);
                    //       dex     [SP  ]
                    putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc, files);
                }
            } else {
                Log.i("MultiDex", "Detected that extraction must be performed.");
                files = performExtractions(sourceApk, dexDir);
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc, files);
            }
        } finally {
            if (cacheLock != null) {
                try {
                    cacheLock.release();
                } catch (IOException var20) {
                    Log.e("MultiDex", "Failed to release lock on " + lockFile.getPath());
                    releaseLockException = var20;
                }
            }

            if (lockChannel != null) {
                closeQuietly(lockChannel);
            }

            closeQuietly(lockRaf);
        }

        if (releaseLockException != null) {
            throw releaseLockException;
        } else {
            Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
            return files;
        }
    }

このプロセスは主にインストール可能なdexファイルのリストを取得し、前回解凍したキャッシュファイルであってもよいし、Apkパッケージから再抽出してもよい.performExtractions()メソッドでDexを解凍すると、ここでは明らかな時間がかかり、解凍したdexファイルは.zip圧縮パッケージに圧縮され、圧縮のプロセスにも明らかな時間がかかる(ここでdexファイルを圧縮するのは省スペースを聞いたのかもしれません).
dexファイルが再解凍された場合、解凍されたapkファイルのcrc値、変更時間、dexファイルの数など、dexファイルの情報が保存され、次回の起動時に解凍されたdexキャッシュファイルを毎回再解凍するのではなく、直接使用することができます.
private static List performExtractions(File sourceApk, File dexDir) throws IOException {
        String extractedFilePrefix = sourceApk.getName() + ".classes";
        prepareDexDir(dexDir, extractedFilePrefix);
        List files = new ArrayList();
        ZipFile apk = new ZipFile(sourceApk);

        try {
            int secondaryNumber = 2;

            for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
                String fileName = extractedFilePrefix + secondaryNumber + ".zip";
                MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(dexDir, fileName);
                files.add(extractedFile);
                Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;

                while(numAttempts < 3 && !isExtractionSuccessful) {
                    ++numAttempts;
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);

                    try {
                        extractedFile.crc = getZipCrc(extractedFile);
                        isExtractionSuccessful = true;
                    } catch (IOException var19) {
                        isExtractionSuccessful = false;
                        Log.w("MultiDex", "Failed to read crc from " + extractedFile.getAbsolutePath(), var19);
                    }

                    Log.i("MultiDex", "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") + " - length " + extractedFile.getAbsolutePath() + ": " + extractedFile.length() + " - crc: " + extractedFile.crc);
                    if (!isExtractionSuccessful) {
                        extractedFile.delete();
                        if (extractedFile.exists()) {
                            Log.w("MultiDex", "Failed to delete corrupted secondary dex '" + extractedFile.getPath() + "'");
                        }
                    }
                }

                if (!isExtractionSuccessful) {
                    throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + secondaryNumber + ")");
                }

                ++secondaryNumber;
            }
        } finally {
            try {
                apk.close();
            } catch (IOException var18) {
                Log.w("MultiDex", "Failed to close resource", var18);
            }

        }

        return files;
    }

では、今までのMultiDex#install方法に戻って、一連の処理を通じて、私たちは処理されたdex[.zip]を手に入れました.次のことは、このdexをClassLoaderの中に入れました.installSecondaryDexes(loader,dexDir,files);
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List extends File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
        if (!files.isEmpty()) {
            if (VERSION.SDK_INT >= 19) {
                MultiDex.V19.install(loader, files, dexDir);
            } else if (VERSION.SDK_INT >= 14) {
                MultiDex.V14.install(loader, files, dexDir);
            } else {
                MultiDex.V4.install(loader, files);
            }
        }
    }

この方法はandroidのバージョン判断をして、>=19の分岐を選んで見てみましょう
    private static void install(ClassLoader loader, List extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
        Field pathListField = MultiDex.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ArrayList suppressedExceptions = new ArrayList();
        MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
        if (suppressedExceptions.size() > 0) {
            Iterator var6 = suppressedExceptions.iterator();

            while(var6.hasNext()) {
                IOException e = (IOException)var6.next();
                Log.w("MultiDex", "Exception in makeDexElement", e);
            }

            Field suppressedExceptionsField = MultiDex.findField(dexPathList, "dexElementsSuppressedExceptions");
            IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(dexPathList));
            if (dexElementsSuppressedExceptions == null) {
                dexElementsSuppressedExceptions = (IOException[])suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
            } else {
                IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length];
                suppressedExceptions.toArray(combined);
                System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
                dexElementsSuppressedExceptions = combined;
            }

            suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
        }

    }

DexPathList#makeDexElementsメソッドを呼び出すと、私たちが解凍したdexファイルをロードすることができます.コードからもわかりますが、DexPathList#makeDexElementsも実際にDexFile#loadDexを呼び出すことでdexファイルをロードし、DexFileオブジェクトを作成しています.
V 19では、DexPathList#makeDexElementsメソッドを反射呼び出して必要なdexファイルをロードし、ロードした配列をClassLoaderインスタンスの「pathList」に拡張するを選択し、dexファイルのインストールを完了します.DexPathList#makeDexElementsメソッドを実行中に例外が発生した場合は、後で反射を使用してこれらの例外をDexPathListのdexElementsSuppressedExceptionsフィールドに記録します.
DexFileオブジェクトを作成する場合は、DexFileのNativeメソッドopenDexFileでdexファイルを開く必要があります.このプロセスの主な目的は、現在のdexファイルにOptimize最適化処理を行い、同じファイル名のodexファイルを生成することです.Appが実際にクラスをロードする場合はodexファイルで行います.
0 x 03まとめて
MulitDex.installは主にこのような流れを完了する~1.data/app/pkgName-x/base.apkから他の分dexをロードし、ストレージディレクトリの優先順位は/data/user/0/pkgName/code_cache/secondary-dexesまたは代替ディレクトリ/data/user/0/pkgName/files/code_cache/secondary-dexesである.
プライマリdex以外のn個のdexパケットの解凍が最初にインストールされ、dexのファイルリストオブジェクトdex-file-listが生成されます.
2.MulitDex#installSecondaryDexes dexをインストールします.前のステップではdex-file-listをロードして取得しました.次にmakeDexElements【DexFileを使用してdexをロードしてさらにパッケージ化し、ClassLoaderに追加されたDexElments配列に準備します】,makeDexElementsが行ったことは,前の処理が完了したdexを構造体DexFileに包装しClassLoaderのDexElmentsに挿入することである.
DexFileオブジェクトを作成する際には、DexFileのNativeメソッドopenDexFileでdexファイルを開く必要があります.このプロセスの主な目的は、現在のdexファイルにOptimize最適化処理を行い、同じファイル名のodexファイルを生成することです.主dexの最適化はapkをインストールするときに完了しました.残りのdexはMultiDex#installSecondaryDexesで最適化されています.後者もMultiDexプロセス中であり,もう一つの時間のかかる操作である.
0 x 04私が直面した問題
  • dexパッケージについて勉強していたとき、apkのdexパッケージの処理の流れについてよく分かりませんでしたが、困ったことに、dexパッケージはapkのインストールからどのような処理を経験しましたか?これらのdexパッケージは最後にどこに行きましたか?今、上の質問に答えます:
  • 私たちはapkをzip圧縮パッケージと見なし、dalvikはapkをロードするたびにclass.dexファイルを解凍し、ロードプロセスはdexのclassesに必要な雑多な依存ライブラリのロードにも関与し、これは非常に時間のかかる操作である.そこでAndroidはこの問題を最適化することを決定し、appが携帯電話にインストールされた後、システムはdexoptプログラムを実行してdexを最適化し、dexの依存をライブラリファイルと一部の補助データはodexファイルにパッケージ化されています.cache/dalvik_cacheディレクトリの下に保存されます.保存形式はapkパス@apk名@classes.dexです.これにより、dexファイルの読み取り/ロードのプロセスがスペーススワップ時間で大幅に短縮されます.multidexスキームは、プライマリdex以外の他のdexを解凍最適化した後もこのディレクトリに保存されるはずです.MultiDexプロセスではステップはodexのキャッシュファイルを取得することです.このディレクトリの下からdexのファイルリストをロードし、ClassLoaderにロードする必要があります.
  • Tinkerに関する原理を学習する際にdexパッケージの合成に関わるのですが、その際に理解できない問題があるのは、tinkerがdexの合成を行い、その後どこに存在するのか、ロードタイミングはどうなっているのかということです.
  • tinkerの差分パケットはapkの元のパスの下に差分のdexを見つけます(例えば私が熱的に修理したクラスはclasses 2.dexにあります).ああ、/data/app/pkgname=/base.apkで元のdexパケットを見つけてマージします.この新しく生成されたdexをnew_dexと呼びます.このnew_dexはtinkerのカスタムディレクトリの下に置いてあります.loadを起動する過程で、classloaderがベースパケットのdexリストをロードした後にclassloaderを生成し、MultiDexのような方法でこのnew_dexを生成します.処理を行い、DexElementsの一番前に置いて、熱修復の目的を達成する.
    0 x 05おすすめ読書
    あなたが知らないMultiDexには穴がたくさんあります.https://www.jianshu.com/p/a5353748159f