summaryrefslogtreecommitdiff
path: root/library/src
diff options
context:
space:
mode:
authorYohann Roussel <yroussel@google.com>2014-03-28 17:35:02 +0100
committerYohann Roussel <yroussel@google.com>2014-04-09 14:56:20 +0200
commit602c6ca8cae4718ba8ff9f65e53305d002479359 (patch)
treeea27d7ec0e1b77ea6471b4955c57ec71b38d5492 /library/src
parent5516c9ffbddd16c90f17c45caea3f4b59ab360d8 (diff)
downloadmultidex-602c6ca8cae4718ba8ff9f65e53305d002479359.tar.gz
Change update detection to reduce load time.
Reduces load time if extraction was already made. It appeared that new ZipFile was really slow because it's preparing much things as soon as it's instanciated. The new criteria consist of the last modified time of the apk plus the crc of the apk's central directory, last modified time should be enough for nearly all modifications and the crc is here to try to handle an OTA mixing with dates. The transition from old criteria to new should be good: since there will be no stored values they would be detected as a new installation. Change-Id: Id390b77b03d794b8b7feb91eb0daae1126c6d691
Diffstat (limited to 'library/src')
-rw-r--r--library/src/android/support/multidex/MultiDex.java7
-rw-r--r--library/src/android/support/multidex/MultiDexExtractor.java242
-rw-r--r--library/src/android/support/multidex/ZipUtil.java125
3 files changed, 260 insertions, 114 deletions
diff --git a/library/src/android/support/multidex/MultiDex.java b/library/src/android/support/multidex/MultiDex.java
index 6bfbf6f..6e650d0 100644
--- a/library/src/android/support/multidex/MultiDex.java
+++ b/library/src/android/support/multidex/MultiDex.java
@@ -16,14 +16,14 @@
package android.support.multidex;
-import dalvik.system.DexFile;
-
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
+import dalvik.system.DexFile;
+
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
@@ -78,6 +78,7 @@ public final class MultiDex {
* extension.
*/
public static void install(Context context) {
+ Log.i(TAG, "install");
if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
@@ -158,6 +159,7 @@ public final class MultiDex {
Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
// Try again, but this time force a reload of the zip file.
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
+
if (checkValidZipFiles(files)) {
installSecondaryDexes(loader, dexDir, files);
} else {
@@ -171,6 +173,7 @@ public final class MultiDex {
Log.e(TAG, "Multidex installation failure", e);
throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
}
+ Log.i(TAG, "install done");
}
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
diff --git a/library/src/android/support/multidex/MultiDexExtractor.java b/library/src/android/support/multidex/MultiDexExtractor.java
index 010999d..99c5269 100644
--- a/library/src/android/support/multidex/MultiDexExtractor.java
+++ b/library/src/android/support/multidex/MultiDexExtractor.java
@@ -58,11 +58,17 @@ final class MultiDexExtractor {
private static final String EXTRACTED_SUFFIX = ".zip";
private static final int MAX_EXTRACT_ATTEMPTS = 3;
- private static final int BUFFER_SIZE = 0x4000;
-
private static final String PREFS_FILE = "multidex.version";
- private static final String KEY_NUM_DEX_FILES = "num_dex";
- private static final String KEY_PREFIX_DEX_CRC = "crc";
+ private static final String KEY_TIME_STAMP = "timestamp";
+ private static final String KEY_CRC = "crc";
+ private static final String KEY_DEX_NUMBER = "dex.number";
+
+ /**
+ * Size of reading buffers.
+ */
+ private static final int BUFFER_SIZE = 0x4000;
+ /* Keep value away from 0 because it is a too probable time stamp value */
+ private static final long NO_VALUE = -1L;
/**
* Extracts application secondary dexes into files in the application data
@@ -75,8 +81,87 @@ final class MultiDexExtractor {
*/
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
boolean forceReload) throws IOException {
- Log.i(TAG, "load(" + applicationInfo.sourceDir + ", forceReload=" + forceReload + ")");
+ Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
final File sourceApk = new File(applicationInfo.sourceDir);
+
+ File archive = new File(applicationInfo.sourceDir);
+ long currentCrc = getZipCrc(archive);
+
+ List<File> files;
+ if (!forceReload && !isModified(context, archive, currentCrc)) {
+ try {
+ files = loadExistingExtractions(context, sourceApk, dexDir);
+ } catch (IOException ioe) {
+ Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
+ + " falling back to fresh extraction", ioe);
+ files = performExtractions(sourceApk, dexDir);
+ putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
+
+ }
+ } else {
+ Log.i(TAG, "Detected that extraction must be performed.");
+ files = performExtractions(sourceApk, dexDir);
+ putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
+ }
+
+ Log.i(TAG, "load found " + files.size() + " secondary dex files");
+ return files;
+ }
+
+ private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
+ throws IOException {
+ Log.i(TAG, "loading existing secondary dex files");
+
+ final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
+ int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
+ final List<File> files = new ArrayList<File>(totalDexNumber);
+
+ for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
+ String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
+ File extractedFile = new File(dexDir, fileName);
+ if (extractedFile.isFile()) {
+ files.add(extractedFile);
+ if (!verifyZipFile(extractedFile)) {
+ Log.i(TAG, "Invalid zip file: " + extractedFile);
+ throw new IOException("Invalid ZIP file.");
+ }
+ } else {
+ throw new IOException("Missing extracted secondary dex file '" +
+ extractedFile.getPath() + "'");
+ }
+ }
+
+ return files;
+ }
+
+ private static boolean isModified(Context context, File archive, long currentCrc) {
+ SharedPreferences prefs = getMultiDexPreferences(context);
+ return (prefs.getLong(KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
+ || (prefs.getLong(KEY_CRC, NO_VALUE) != currentCrc);
+ }
+
+ private static long getTimeStamp(File archive) {
+ long timeStamp = archive.lastModified();
+ if (timeStamp == NO_VALUE) {
+ // never return NO_VALUE
+ timeStamp--;
+ }
+ return timeStamp;
+ }
+
+
+ private static long getZipCrc(File archive) throws IOException {
+ long computedValue = ZipUtil.getZipCrc(archive);
+ if (computedValue == NO_VALUE) {
+ // never return NO_VALUE
+ computedValue--;
+ }
+ return computedValue;
+ }
+
+ private static List<File> performExtractions(File sourceApk, File dexDir)
+ throws IOException {
+
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
// Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
@@ -85,15 +170,9 @@ final class MultiDexExtractor {
// while another had created it.
prepareDexDir(dexDir, extractedFilePrefix);
- final List<File> files = new ArrayList<File>();
- final ZipFile apk = new ZipFile(applicationInfo.sourceDir);
+ List<File> files = new ArrayList<File>();
- // If the CRC of any of the dex files is different than what we have stored or the number of
- // dex files are different, then force reload everything.
- ArrayList<Long> dexCrcs = getAllDexCrcs(apk);
- if (isAnyDexCrcDifferent(context, dexCrcs)) {
- forceReload = true;
- }
+ final ZipFile apk = new ZipFile(sourceApk);
try {
int secondaryNumber = 2;
@@ -104,41 +183,36 @@ final class MultiDexExtractor {
File extractedFile = new File(dexDir, fileName);
files.add(extractedFile);
- Log.i(TAG, "Need extracted file " + extractedFile);
- if (forceReload || !extractedFile.isFile()) {
- Log.i(TAG, "Extraction is needed for file " + extractedFile);
- int numAttempts = 0;
- boolean isExtractionSuccessful = false;
- while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
- numAttempts++;
-
- // Create a zip file (extractedFile) containing only the secondary dex file
- // (dexFile) from the apk.
- extract(apk, dexFile, extractedFile, extractedFilePrefix);
-
- // Verify that the extracted file is indeed a zip file.
- isExtractionSuccessful = verifyZipFile(extractedFile);
-
- // Log the sha1 of the extracted zip file
- Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
- " - length " + extractedFile.getAbsolutePath() + ": " +
- extractedFile.length());
- if (!isExtractionSuccessful) {
- // Delete the extracted file
- extractedFile.delete();
+ Log.i(TAG, "Extraction is needed for file " + extractedFile);
+ int numAttempts = 0;
+ boolean isExtractionSuccessful = false;
+ while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
+ numAttempts++;
+
+ // Create a zip file (extractedFile) containing only the secondary dex file
+ // (dexFile) from the apk.
+ extract(apk, dexFile, extractedFile, extractedFilePrefix);
+
+ // Verify that the extracted file is indeed a zip file.
+ isExtractionSuccessful = verifyZipFile(extractedFile);
+
+ // Log the sha1 of the extracted zip file
+ Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
+ " - length " + extractedFile.getAbsolutePath() + ": " +
+ extractedFile.length());
+ if (!isExtractionSuccessful) {
+ // Delete the extracted file
+ extractedFile.delete();
+ if (extractedFile.exists()) {
+ Log.w(TAG, "Failed to delete corrupted secondary dex '" +
+ extractedFile.getPath() + "'");
}
}
- if (isExtractionSuccessful) {
- // Write the dex crc's into the shared preferences
- putStoredDexCrcs(context, dexCrcs);
- } else {
- throw new IOException("Could not create zip file " +
- extractedFile.getAbsolutePath() + " for secondary dex (" +
- secondaryNumber + ")");
- }
- } else {
- Log.i(TAG, "No extraction needed for " + extractedFile + " of size " +
- extractedFile.length());
+ }
+ if (!isExtractionSuccessful) {
+ throw new IOException("Could not create zip file " +
+ extractedFile.getAbsolutePath() + " for secondary dex (" +
+ secondaryNumber + ")");
}
secondaryNumber++;
dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
@@ -154,69 +228,17 @@ final class MultiDexExtractor {
return files;
}
- /**
- * Iterate through the expected dex files, classes.dex, classes2.dex, classes3.dex, etc. and
- * return the CRC of each zip entry in a list.
- */
- private static ArrayList<Long> getAllDexCrcs(ZipFile apk) {
- ArrayList<Long> dexCrcs = new ArrayList<Long>();
-
- // Add the first one
- dexCrcs.add(apk.getEntry(DEX_PREFIX + DEX_SUFFIX).getCrc());
-
- // Get the number of dex files in the apk.
- int secondaryNumber = 2;
- while (true) {
- ZipEntry dexEntry = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
- if (dexEntry == null) {
- break;
- }
-
- dexCrcs.add(dexEntry.getCrc());
- secondaryNumber++;
- }
- return dexCrcs;
- }
-
- /**
- * Returns true if the number of dex files is different than what is stored in the shared
- * preferences file or if any dex CRC value is different.
- */
- private static boolean isAnyDexCrcDifferent(Context context, ArrayList<Long> dexCrcs) {
- final ArrayList<Long> storedDexCrcs = getStoredDexCrcs(context);
-
- if (dexCrcs.size() != storedDexCrcs.size()) {
- return true;
- }
-
- // We know the length of storedDexCrcs and dexCrcs are the same.
- for (int i = 0; i < storedDexCrcs.size(); i++) {
- if (storedDexCrcs.get(i).longValue() != dexCrcs.get(i).longValue()) {
- return true;
- }
- }
-
- // All the same
- return false;
- }
-
- private static ArrayList<Long> getStoredDexCrcs(Context context) {
- SharedPreferences prefs = getMultiDexPreferences(context);
- int numDexFiles = prefs.getInt(KEY_NUM_DEX_FILES, 0);
- ArrayList<Long> dexCrcs = new ArrayList<Long>(numDexFiles);
- for (int i = 0; i < numDexFiles; i++) {
- dexCrcs.add(prefs.getLong(makeDexCrcKey(i), 0));
- }
- return dexCrcs;
- }
-
- private static void putStoredDexCrcs(Context context, ArrayList<Long> dexCrcs) {
+ private static void putStoredApkInfo(Context context, long timeStamp, long crc,
+ int totalDexNumber) {
SharedPreferences prefs = getMultiDexPreferences(context);
SharedPreferences.Editor edit = prefs.edit();
- edit.putInt(KEY_NUM_DEX_FILES, dexCrcs.size());
- for (int i = 0; i < dexCrcs.size(); i++) {
- edit.putLong(makeDexCrcKey(i), dexCrcs.get(i));
- }
+ edit.putLong(KEY_TIME_STAMP, timeStamp);
+ edit.putLong(KEY_CRC, crc);
+ /* SharedPreferences.Editor doc says that apply() and commit() "atomically performs the
+ * requested modifications" it should be OK to rely on saving the dex files number (getting
+ * old number value would go along with old crc and time stamp).
+ */
+ edit.putInt(KEY_DEX_NUMBER, totalDexNumber);
apply(edit);
}
@@ -227,10 +249,6 @@ final class MultiDexExtractor {
: Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
}
- private static String makeDexCrcKey(int i) {
- return KEY_PREFIX_DEX_CRC + Integer.toString(i);
- }
-
/**
* This removes any files that do not have the correct prefix.
*/
@@ -338,7 +356,7 @@ final class MultiDexExtractor {
private static Method sApplyMethod; // final
static {
try {
- Class cls = SharedPreferences.Editor.class;
+ Class<?> cls = SharedPreferences.Editor.class;
sApplyMethod = cls.getMethod("apply");
} catch (NoSuchMethodException unused) {
sApplyMethod = null;
diff --git a/library/src/android/support/multidex/ZipUtil.java b/library/src/android/support/multidex/ZipUtil.java
new file mode 100644
index 0000000..cd518cc
--- /dev/null
+++ b/library/src/android/support/multidex/ZipUtil.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* Apache Harmony HEADER because the code in this class comes mostly from ZipFile, ZipEntry and
+ * ZipConstants from android libcore.
+ */
+
+package android.support.multidex;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.zip.CRC32;
+import java.util.zip.ZipException;
+
+/**
+ * Tools to build a quick partial crc of zip files.
+ */
+final class ZipUtil {
+ static class CentralDirectory {
+ long offset;
+ long size;
+ }
+
+ /* redefine those constant here because of bug 13721174 preventing to compile using the
+ * constants defined in ZipFile */
+ private static final int ENDHDR = 22;
+ private static final int ENDSIG = 0x6054b50;
+
+ /**
+ * Size of reading buffers.
+ */
+ private static final int BUFFER_SIZE = 0x4000;
+
+ /**
+ * Compute crc32 of the central directory of an apk. The central directory contains
+ * the crc32 of each entries in the zip so the computed result is considered valid for the whole
+ * zip file. Does not support zip64 nor multidisk but it should be OK for now since ZipFile does
+ * not either.
+ */
+ static long getZipCrc(File apk) throws IOException {
+ RandomAccessFile raf = new RandomAccessFile(apk, "r");
+ try {
+ CentralDirectory dir = findCentralDirectory(raf);
+
+ return computeCrcOfCentralDir(raf, dir);
+ } finally {
+ raf.close();
+ }
+ }
+
+ /* Package visible for testing */
+ static CentralDirectory findCentralDirectory(RandomAccessFile raf) throws IOException,
+ ZipException {
+ long scanOffset = raf.length() - ENDHDR;
+ if (scanOffset < 0) {
+ throw new ZipException("File too short to be a zip file: " + raf.length());
+ }
+
+ long stopOffset = scanOffset - 0x10000 /* ".ZIP file comment"'s max length */;
+ if (stopOffset < 0) {
+ stopOffset = 0;
+ }
+
+ int endSig = Integer.reverseBytes(ENDSIG);
+ while (true) {
+ raf.seek(scanOffset);
+ if (raf.readInt() == endSig) {
+ break;
+ }
+
+ scanOffset--;
+ if (scanOffset < stopOffset) {
+ throw new ZipException("End Of Central Directory signature not found");
+ }
+ }
+ // Read the End Of Central Directory. ENDHDR includes the signature
+ // bytes,
+ // which we've already read.
+
+ // Pull out the information we need.
+ raf.skipBytes(2); // diskNumber
+ raf.skipBytes(2); // diskWithCentralDir
+ raf.skipBytes(2); // numEntries
+ raf.skipBytes(2); // totalNumEntries
+ CentralDirectory dir = new CentralDirectory();
+ dir.size = Integer.reverseBytes(raf.readInt()) & 0xFFFFFFFFL;
+ dir.offset = Integer.reverseBytes(raf.readInt()) & 0xFFFFFFFFL;
+ return dir;
+ }
+
+ /* Package visible for testing */
+ static long computeCrcOfCentralDir(RandomAccessFile raf, CentralDirectory dir)
+ throws IOException {
+ CRC32 crc = new CRC32();
+ long stillToRead = dir.size;
+ raf.seek(dir.offset);
+ int length = (int) Math.min(BUFFER_SIZE, stillToRead);
+ byte[] buffer = new byte[BUFFER_SIZE];
+ length = raf.read(buffer, 0, length);
+ while (length != -1) {
+ crc.update(buffer, 0, length);
+ stillToRead -= length;
+ if (stillToRead == 0) {
+ break;
+ }
+ length = (int) Math.min(BUFFER_SIZE, stillToRead);
+ length = raf.read(buffer, 0, length);
+ }
+ return crc.getValue();
+ }
+}