Skip to content

Commit 602c6ca

Browse files
author
Yohann Roussel
committed
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
1 parent 5516c9f commit 602c6ca

File tree

11 files changed

+609
-116
lines changed

11 files changed

+609
-116
lines changed

MODULE_LICENSE_APACHE2

Whitespace-only changes.

NOTICE

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
=========================================================================
2+
== NOTICE file corresponding to the section 4 d of ==
3+
== the Apache License, Version 2.0, ==
4+
== in this case for the Android-specific code. ==
5+
=========================================================================
6+
7+
Android Code
8+
Copyright 2005-2014 The Android Open Source Project
9+
10+
This product includes software developed as part of
11+
The Android Open Source Project (http://source.android.com).
12+
13+
=========================================================================
14+
== NOTICE file corresponding to the section 4 d of ==
15+
== the Apache License, Version 2.0, ==
16+
== in this case for the Apache Harmony distribution. ==
17+
=========================================================================
18+
19+
Apache Harmony
20+
Copyright 2006 The Apache Software Foundation
21+
22+
This product includes software developed at
23+
The Apache Software Foundation (http://www.apache.org/).
24+
25+
Portions of Harmony were originally developed by
26+
Intel Corporation and are licensed to the Apache Software
27+
Foundation under the "Software Grant and Corporate Contribution
28+
License Agreement", informally known as the "Intel Harmony CLA".

instrumentation/.classpath

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<classpathentry kind="src" path="src"/>
44
<classpathentry kind="src" path="gen"/>
55
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
6-
<classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
6+
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
7+
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
78
<classpathentry kind="output" path="bin/classes"/>
89
</classpath>

library/.classpath

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<classpathentry kind="src" path="src"/>
44
<classpathentry kind="src" path="gen"/>
55
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
6-
<classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
6+
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
7+
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
78
<classpathentry kind="output" path="bin/classes"/>
89
</classpath>

library/src/android/support/multidex/MultiDex.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616

1717
package android.support.multidex;
1818

19-
import dalvik.system.DexFile;
20-
2119
import android.content.Context;
2220
import android.content.pm.ApplicationInfo;
2321
import android.content.pm.PackageManager;
2422
import android.os.Build;
2523
import android.util.Log;
2624

25+
import dalvik.system.DexFile;
26+
2727
import java.io.File;
2828
import java.io.IOException;
2929
import java.lang.reflect.Array;
@@ -78,6 +78,7 @@ private MultiDex() {}
7878
* extension.
7979
*/
8080
public static void install(Context context) {
81+
Log.i(TAG, "install");
8182

8283
if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
8384
throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
@@ -158,6 +159,7 @@ public static void install(Context context) {
158159
Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
159160
// Try again, but this time force a reload of the zip file.
160161
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
162+
161163
if (checkValidZipFiles(files)) {
162164
installSecondaryDexes(loader, dexDir, files);
163165
} else {
@@ -171,6 +173,7 @@ public static void install(Context context) {
171173
Log.e(TAG, "Multidex installation failure", e);
172174
throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
173175
}
176+
Log.i(TAG, "install done");
174177
}
175178

176179
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)

library/src/android/support/multidex/MultiDexExtractor.java

+130-112
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,17 @@ final class MultiDexExtractor {
5858
private static final String EXTRACTED_SUFFIX = ".zip";
5959
private static final int MAX_EXTRACT_ATTEMPTS = 3;
6060

61-
private static final int BUFFER_SIZE = 0x4000;
62-
6361
private static final String PREFS_FILE = "multidex.version";
64-
private static final String KEY_NUM_DEX_FILES = "num_dex";
65-
private static final String KEY_PREFIX_DEX_CRC = "crc";
62+
private static final String KEY_TIME_STAMP = "timestamp";
63+
private static final String KEY_CRC = "crc";
64+
private static final String KEY_DEX_NUMBER = "dex.number";
65+
66+
/**
67+
* Size of reading buffers.
68+
*/
69+
private static final int BUFFER_SIZE = 0x4000;
70+
/* Keep value away from 0 because it is a too probable time stamp value */
71+
private static final long NO_VALUE = -1L;
6672

6773
/**
6874
* Extracts application secondary dexes into files in the application data
@@ -75,8 +81,87 @@ final class MultiDexExtractor {
7581
*/
7682
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
7783
boolean forceReload) throws IOException {
78-
Log.i(TAG, "load(" + applicationInfo.sourceDir + ", forceReload=" + forceReload + ")");
84+
Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
7985
final File sourceApk = new File(applicationInfo.sourceDir);
86+
87+
File archive = new File(applicationInfo.sourceDir);
88+
long currentCrc = getZipCrc(archive);
89+
90+
List<File> files;
91+
if (!forceReload && !isModified(context, archive, currentCrc)) {
92+
try {
93+
files = loadExistingExtractions(context, sourceApk, dexDir);
94+
} catch (IOException ioe) {
95+
Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
96+
+ " falling back to fresh extraction", ioe);
97+
files = performExtractions(sourceApk, dexDir);
98+
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
99+
100+
}
101+
} else {
102+
Log.i(TAG, "Detected that extraction must be performed.");
103+
files = performExtractions(sourceApk, dexDir);
104+
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
105+
}
106+
107+
Log.i(TAG, "load found " + files.size() + " secondary dex files");
108+
return files;
109+
}
110+
111+
private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
112+
throws IOException {
113+
Log.i(TAG, "loading existing secondary dex files");
114+
115+
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
116+
int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
117+
final List<File> files = new ArrayList<File>(totalDexNumber);
118+
119+
for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
120+
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
121+
File extractedFile = new File(dexDir, fileName);
122+
if (extractedFile.isFile()) {
123+
files.add(extractedFile);
124+
if (!verifyZipFile(extractedFile)) {
125+
Log.i(TAG, "Invalid zip file: " + extractedFile);
126+
throw new IOException("Invalid ZIP file.");
127+
}
128+
} else {
129+
throw new IOException("Missing extracted secondary dex file '" +
130+
extractedFile.getPath() + "'");
131+
}
132+
}
133+
134+
return files;
135+
}
136+
137+
private static boolean isModified(Context context, File archive, long currentCrc) {
138+
SharedPreferences prefs = getMultiDexPreferences(context);
139+
return (prefs.getLong(KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
140+
|| (prefs.getLong(KEY_CRC, NO_VALUE) != currentCrc);
141+
}
142+
143+
private static long getTimeStamp(File archive) {
144+
long timeStamp = archive.lastModified();
145+
if (timeStamp == NO_VALUE) {
146+
// never return NO_VALUE
147+
timeStamp--;
148+
}
149+
return timeStamp;
150+
}
151+
152+
153+
private static long getZipCrc(File archive) throws IOException {
154+
long computedValue = ZipUtil.getZipCrc(archive);
155+
if (computedValue == NO_VALUE) {
156+
// never return NO_VALUE
157+
computedValue--;
158+
}
159+
return computedValue;
160+
}
161+
162+
private static List<File> performExtractions(File sourceApk, File dexDir)
163+
throws IOException {
164+
80165
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
81166

82167
// Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
@@ -85,15 +170,9 @@ static List<File> load(Context context, ApplicationInfo applicationInfo, File de
85170
// while another had created it.
86171
prepareDexDir(dexDir, extractedFilePrefix);
87172

88-
final List<File> files = new ArrayList<File>();
89-
final ZipFile apk = new ZipFile(applicationInfo.sourceDir);
173+
List<File> files = new ArrayList<File>();
90174

91-
// If the CRC of any of the dex files is different than what we have stored or the number of
92-
// dex files are different, then force reload everything.
93-
ArrayList<Long> dexCrcs = getAllDexCrcs(apk);
94-
if (isAnyDexCrcDifferent(context, dexCrcs)) {
95-
forceReload = true;
96-
}
175+
final ZipFile apk = new ZipFile(sourceApk);
97176
try {
98177

99178
int secondaryNumber = 2;
@@ -104,41 +183,36 @@ static List<File> load(Context context, ApplicationInfo applicationInfo, File de
104183
File extractedFile = new File(dexDir, fileName);
105184
files.add(extractedFile);
106185

107-
Log.i(TAG, "Need extracted file " + extractedFile);
108-
if (forceReload || !extractedFile.isFile()) {
109-
Log.i(TAG, "Extraction is needed for file " + extractedFile);
110-
int numAttempts = 0;
111-
boolean isExtractionSuccessful = false;
112-
while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
113-
numAttempts++;
114-
115-
// Create a zip file (extractedFile) containing only the secondary dex file
116-
// (dexFile) from the apk.
117-
extract(apk, dexFile, extractedFile, extractedFilePrefix);
118-
119-
// Verify that the extracted file is indeed a zip file.
120-
isExtractionSuccessful = verifyZipFile(extractedFile);
121-
122-
// Log the sha1 of the extracted zip file
123-
Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
124-
" - length " + extractedFile.getAbsolutePath() + ": " +
125-
extractedFile.length());
126-
if (!isExtractionSuccessful) {
127-
// Delete the extracted file
128-
extractedFile.delete();
186+
Log.i(TAG, "Extraction is needed for file " + extractedFile);
187+
int numAttempts = 0;
188+
boolean isExtractionSuccessful = false;
189+
while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
190+
numAttempts++;
191+
192+
// Create a zip file (extractedFile) containing only the secondary dex file
193+
// (dexFile) from the apk.
194+
extract(apk, dexFile, extractedFile, extractedFilePrefix);
195+
196+
// Verify that the extracted file is indeed a zip file.
197+
isExtractionSuccessful = verifyZipFile(extractedFile);
198+
199+
// Log the sha1 of the extracted zip file
200+
Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
201+
" - length " + extractedFile.getAbsolutePath() + ": " +
202+
extractedFile.length());
203+
if (!isExtractionSuccessful) {
204+
// Delete the extracted file
205+
extractedFile.delete();
206+
if (extractedFile.exists()) {
207+
Log.w(TAG, "Failed to delete corrupted secondary dex '" +
208+
extractedFile.getPath() + "'");
129209
}
130210
}
131-
if (isExtractionSuccessful) {
132-
// Write the dex crc's into the shared preferences
133-
putStoredDexCrcs(context, dexCrcs);
134-
} else {
135-
throw new IOException("Could not create zip file " +
136-
extractedFile.getAbsolutePath() + " for secondary dex (" +
137-
secondaryNumber + ")");
138-
}
139-
} else {
140-
Log.i(TAG, "No extraction needed for " + extractedFile + " of size " +
141-
extractedFile.length());
211+
}
212+
if (!isExtractionSuccessful) {
213+
throw new IOException("Could not create zip file " +
214+
extractedFile.getAbsolutePath() + " for secondary dex (" +
215+
secondaryNumber + ")");
142216
}
143217
secondaryNumber++;
144218
dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
@@ -154,69 +228,17 @@ static List<File> load(Context context, ApplicationInfo applicationInfo, File de
154228
return files;
155229
}
156230

157-
/**
158-
* Iterate through the expected dex files, classes.dex, classes2.dex, classes3.dex, etc. and
159-
* return the CRC of each zip entry in a list.
160-
*/
161-
private static ArrayList<Long> getAllDexCrcs(ZipFile apk) {
162-
ArrayList<Long> dexCrcs = new ArrayList<Long>();
163-
164-
// Add the first one
165-
dexCrcs.add(apk.getEntry(DEX_PREFIX + DEX_SUFFIX).getCrc());
166-
167-
// Get the number of dex files in the apk.
168-
int secondaryNumber = 2;
169-
while (true) {
170-
ZipEntry dexEntry = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
171-
if (dexEntry == null) {
172-
break;
173-
}
174-
175-
dexCrcs.add(dexEntry.getCrc());
176-
secondaryNumber++;
177-
}
178-
return dexCrcs;
179-
}
180-
181-
/**
182-
* Returns true if the number of dex files is different than what is stored in the shared
183-
* preferences file or if any dex CRC value is different.
184-
*/
185-
private static boolean isAnyDexCrcDifferent(Context context, ArrayList<Long> dexCrcs) {
186-
final ArrayList<Long> storedDexCrcs = getStoredDexCrcs(context);
187-
188-
if (dexCrcs.size() != storedDexCrcs.size()) {
189-
return true;
190-
}
191-
192-
// We know the length of storedDexCrcs and dexCrcs are the same.
193-
for (int i = 0; i < storedDexCrcs.size(); i++) {
194-
if (storedDexCrcs.get(i).longValue() != dexCrcs.get(i).longValue()) {
195-
return true;
196-
}
197-
}
198-
199-
// All the same
200-
return false;
201-
}
202-
203-
private static ArrayList<Long> getStoredDexCrcs(Context context) {
204-
SharedPreferences prefs = getMultiDexPreferences(context);
205-
int numDexFiles = prefs.getInt(KEY_NUM_DEX_FILES, 0);
206-
ArrayList<Long> dexCrcs = new ArrayList<Long>(numDexFiles);
207-
for (int i = 0; i < numDexFiles; i++) {
208-
dexCrcs.add(prefs.getLong(makeDexCrcKey(i), 0));
209-
}
210-
return dexCrcs;
211-
}
212-
213-
private static void putStoredDexCrcs(Context context, ArrayList<Long> dexCrcs) {
231+
private static void putStoredApkInfo(Context context, long timeStamp, long crc,
232+
int totalDexNumber) {
214233
SharedPreferences prefs = getMultiDexPreferences(context);
215234
SharedPreferences.Editor edit = prefs.edit();
216-
edit.putInt(KEY_NUM_DEX_FILES, dexCrcs.size());
217-
for (int i = 0; i < dexCrcs.size(); i++) {
218-
edit.putLong(makeDexCrcKey(i), dexCrcs.get(i));
219-
}
235+
edit.putLong(KEY_TIME_STAMP, timeStamp);
236+
edit.putLong(KEY_CRC, crc);
237+
/* SharedPreferences.Editor doc says that apply() and commit() "atomically performs the
238+
* requested modifications" it should be OK to rely on saving the dex files number (getting
239+
* old number value would go along with old crc and time stamp).
240+
*/
241+
edit.putInt(KEY_DEX_NUMBER, totalDexNumber);
220242
apply(edit);
221243
}
222244

@@ -227,10 +249,6 @@ private static SharedPreferences getMultiDexPreferences(Context context) {
227249
: Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
228250
}
229251

230-
private static String makeDexCrcKey(int i) {
231-
return KEY_PREFIX_DEX_CRC + Integer.toString(i);
232-
}
233-
234252
/**
235253
* This removes any files that do not have the correct prefix.
236254
*/
@@ -338,7 +356,7 @@ private static void closeQuietly(Closeable closeable) {
338356
private static Method sApplyMethod; // final
339357
static {
340358
try {
341-
Class cls = SharedPreferences.Editor.class;
359+
Class<?> cls = SharedPreferences.Editor.class;
342360
sApplyMethod = cls.getMethod("apply");
343361
} catch (NoSuchMethodException unused) {
344362
sApplyMethod = null;

0 commit comments

Comments
 (0)