@@ -58,11 +58,17 @@ final class MultiDexExtractor {
58
58
private static final String EXTRACTED_SUFFIX = ".zip" ;
59
59
private static final int MAX_EXTRACT_ATTEMPTS = 3 ;
60
60
61
- private static final int BUFFER_SIZE = 0x4000 ;
62
-
63
61
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 ;
66
72
67
73
/**
68
74
* Extracts application secondary dexes into files in the application data
@@ -75,8 +81,87 @@ final class MultiDexExtractor {
75
81
*/
76
82
static List <File > load (Context context , ApplicationInfo applicationInfo , File dexDir ,
77
83
boolean forceReload ) throws IOException {
78
- Log .i (TAG , "load(" + applicationInfo .sourceDir + ", forceReload= " + forceReload + ")" );
84
+ Log .i (TAG , "MultiDexExtractor. load(" + applicationInfo .sourceDir + ", " + forceReload + ")" );
79
85
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
+
80
165
final String extractedFilePrefix = sourceApk .getName () + EXTRACTED_NAME_EXT ;
81
166
82
167
// 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
85
170
// while another had created it.
86
171
prepareDexDir (dexDir , extractedFilePrefix );
87
172
88
- final List <File > files = new ArrayList <File >();
89
- final ZipFile apk = new ZipFile (applicationInfo .sourceDir );
173
+ List <File > files = new ArrayList <File >();
90
174
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 );
97
176
try {
98
177
99
178
int secondaryNumber = 2 ;
@@ -104,41 +183,36 @@ static List<File> load(Context context, ApplicationInfo applicationInfo, File de
104
183
File extractedFile = new File (dexDir , fileName );
105
184
files .add (extractedFile );
106
185
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 () + "'" );
129
209
}
130
210
}
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 + ")" );
142
216
}
143
217
secondaryNumber ++;
144
218
dexFile = apk .getEntry (DEX_PREFIX + secondaryNumber + DEX_SUFFIX );
@@ -154,69 +228,17 @@ static List<File> load(Context context, ApplicationInfo applicationInfo, File de
154
228
return files ;
155
229
}
156
230
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 ) {
214
233
SharedPreferences prefs = getMultiDexPreferences (context );
215
234
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 );
220
242
apply (edit );
221
243
}
222
244
@@ -227,10 +249,6 @@ private static SharedPreferences getMultiDexPreferences(Context context) {
227
249
: Context .MODE_PRIVATE | Context .MODE_MULTI_PROCESS );
228
250
}
229
251
230
- private static String makeDexCrcKey (int i ) {
231
- return KEY_PREFIX_DEX_CRC + Integer .toString (i );
232
- }
233
-
234
252
/**
235
253
* This removes any files that do not have the correct prefix.
236
254
*/
@@ -338,7 +356,7 @@ private static void closeQuietly(Closeable closeable) {
338
356
private static Method sApplyMethod ; // final
339
357
static {
340
358
try {
341
- Class cls = SharedPreferences .Editor .class ;
359
+ Class <?> cls = SharedPreferences .Editor .class ;
342
360
sApplyMethod = cls .getMethod ("apply" );
343
361
} catch (NoSuchMethodException unused ) {
344
362
sApplyMethod = null ;
0 commit comments