ホットリペア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つのことをしました.次に、ソースコードで分析します.
まず、アプリケーションInfo.sourceDirの2つのファイルパスをApplicationで取得します.
【/data/app/com.hotfix.sample-JEOYk3NmgdLD31DnXH25EQ==/base.apk】
applicationInfo.dataDir
【/data/user/0/com.hotfix.sample】
次にメソッドdoInstallation()を呼び出し、doInstallation()メソッドを参照します.
MultiDex.install(Context)の過程で、肝心なステップはMultiDexExtractor#load方法とMultiDex#installSecondaryDexes方法で、MultiDexExtractor#load方法が何をしたのかを見続けます.
このプロセスは主にインストール可能なdexファイルのリストを取得し、前回解凍したキャッシュファイルであってもよいし、Apkパッケージから再抽出してもよい.performExtractions()メソッドでDexを解凍すると、ここでは明らかな時間がかかり、解凍したdexファイルは.zip圧縮パッケージに圧縮され、圧縮のプロセスにも明らかな時間がかかる(ここでdexファイルを圧縮するのは省スペースを聞いたのかもしれません).
dexファイルが再解凍された場合、解凍されたapkファイルのcrc値、変更時間、dexファイルの数など、dexファイルの情報が保存され、次回の起動時に解凍されたdexキャッシュファイルを毎回再解凍するのではなく、直接使用することができます.
では、今までのMultiDex#install方法に戻って、一連の処理を通じて、私たちは処理されたdex[.zip]を手に入れました.次のことは、このdexをClassLoaderの中に入れました.installSecondaryDexes(loader,dexDir,files);
この方法はandroidのバージョン判断をして、>=19の分岐を選んで見てみましょう
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
公式文書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私が直面した問題
0 x 05おすすめ読書
あなたが知らないMultiDexには穴がたくさんあります.https://www.jianshu.com/p/a5353748159f