MultiDexExtractor.java revision 994fa84b3af07700fd5dcd477c02a6bd824dde45
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.multidex; 18 19import android.content.Context; 20import android.content.SharedPreferences; 21import android.content.pm.ApplicationInfo; 22import android.os.Build; 23import android.util.Log; 24 25import java.io.BufferedOutputStream; 26import java.io.Closeable; 27import java.io.File; 28import java.io.FileFilter; 29import java.io.FileNotFoundException; 30import java.io.FileOutputStream; 31import java.io.IOException; 32import java.io.InputStream; 33import java.lang.reflect.InvocationTargetException; 34import java.lang.reflect.Method; 35import java.util.ArrayList; 36import java.util.List; 37import java.util.zip.ZipEntry; 38import java.util.zip.ZipException; 39import java.util.zip.ZipFile; 40import java.util.zip.ZipOutputStream; 41 42/** 43 * Exposes application secondary dex files as files in the application data 44 * directory. 45 */ 46final class MultiDexExtractor { 47 48 private static final String TAG = MultiDex.TAG; 49 50 /** 51 * We look for additional dex files named {@code classes2.dex}, 52 * {@code classes3.dex}, etc. 53 */ 54 private static final String DEX_PREFIX = "classes"; 55 private static final String DEX_SUFFIX = ".dex"; 56 57 private static final String EXTRACTED_NAME_EXT = ".classes"; 58 private static final String EXTRACTED_SUFFIX = ".zip"; 59 private static final int MAX_EXTRACT_ATTEMPTS = 3; 60 61 private static final int BUFFER_SIZE = 0x4000; 62 63 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"; 66 67 /** 68 * Extracts application secondary dexes into files in the application data 69 * directory. 70 * 71 * @return a list of files that were created. The list may be empty if there 72 * are no secondary dex files. 73 * @throws IOException if encounters a problem while reading or writing 74 * secondary dex files 75 */ 76 static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, 77 boolean forceReload) throws IOException { 78 Log.i(TAG, "load(" + applicationInfo.sourceDir + ", forceReload=" + forceReload + ")"); 79 final File sourceApk = new File(applicationInfo.sourceDir); 80 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; 81 82 // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that 83 // contains a secondary dex file in there is not consistent with the latest apk. Otherwise, 84 // multi-process race conditions can cause a crash loop where one process deletes the zip 85 // while another had created it. 86 prepareDexDir(dexDir, extractedFilePrefix); 87 88 final List<File> files = new ArrayList<File>(); 89 final ZipFile apk = new ZipFile(applicationInfo.sourceDir); 90 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 } 97 try { 98 99 int secondaryNumber = 2; 100 101 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); 102 while (dexFile != null) { 103 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; 104 File extractedFile = new File(dexDir, fileName); 105 files.add(extractedFile); 106 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(); 129 } 130 } 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()); 142 } 143 secondaryNumber++; 144 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); 145 } 146 } finally { 147 try { 148 apk.close(); 149 } catch (IOException e) { 150 Log.w(TAG, "Failed to close resource", e); 151 } 152 } 153 154 return files; 155 } 156 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) != dexCrcs.get(i)) { 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) { 214 SharedPreferences prefs = getMultiDexPreferences(context); 215 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 } 220 apply(edit); 221 } 222 223 private static SharedPreferences getMultiDexPreferences(Context context) { 224 return context.getSharedPreferences(PREFS_FILE, 225 Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB 226 ? Context.MODE_PRIVATE 227 : Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS); 228 } 229 230 private static String makeDexCrcKey(int i) { 231 return KEY_PREFIX_DEX_CRC + Integer.toString(i); 232 } 233 234 /** 235 * This removes any files that do not have the correct prefix. 236 */ 237 private static void prepareDexDir(File dexDir, final String extractedFilePrefix) 238 throws IOException { 239 dexDir.mkdir(); 240 if (!dexDir.isDirectory()) { 241 throw new IOException("Failed to create dex directory " + dexDir.getPath()); 242 } 243 244 // Clean possible old files 245 FileFilter filter = new FileFilter() { 246 247 @Override 248 public boolean accept(File pathname) { 249 return !pathname.getName().startsWith(extractedFilePrefix); 250 } 251 }; 252 File[] files = dexDir.listFiles(filter); 253 if (files == null) { 254 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); 255 return; 256 } 257 for (File oldFile : files) { 258 Log.w(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + 259 oldFile.length()); 260 if (!oldFile.delete()) { 261 Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); 262 } else { 263 Log.w(TAG, "Deleted old file " + oldFile.getPath()); 264 } 265 } 266 } 267 268 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, 269 String extractedFilePrefix) throws IOException, FileNotFoundException { 270 271 InputStream in = apk.getInputStream(dexFile); 272 ZipOutputStream out = null; 273 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX, 274 extractTo.getParentFile()); 275 Log.i(TAG, "Extracting " + tmp.getPath()); 276 try { 277 out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); 278 try { 279 ZipEntry classesDex = new ZipEntry("classes.dex"); 280 // keep zip entry time since it is the criteria used by Dalvik 281 classesDex.setTime(dexFile.getTime()); 282 out.putNextEntry(classesDex); 283 284 byte[] buffer = new byte[BUFFER_SIZE]; 285 int length = in.read(buffer); 286 while (length != -1) { 287 out.write(buffer, 0, length); 288 length = in.read(buffer); 289 } 290 out.closeEntry(); 291 } finally { 292 out.close(); 293 } 294 Log.i(TAG, "Renaming to " + extractTo.getPath()); 295 if (!tmp.renameTo(extractTo)) { 296 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + 297 "\" to \"" + extractTo.getAbsolutePath() + "\""); 298 } 299 } finally { 300 closeQuietly(in); 301 tmp.delete(); // return status ignored 302 } 303 } 304 305 /** 306 * Returns whether the file is a valid zip file. 307 */ 308 static boolean verifyZipFile(File file) { 309 try { 310 ZipFile zipFile = new ZipFile(file); 311 try { 312 zipFile.close(); 313 return true; 314 } catch (IOException e) { 315 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath()); 316 } 317 } catch (ZipException ex) { 318 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex); 319 } catch (IOException ex) { 320 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex); 321 } 322 return false; 323 } 324 325 /** 326 * Closes the given {@code Closeable}. Suppresses any IO exceptions. 327 */ 328 private static void closeQuietly(Closeable closeable) { 329 try { 330 closeable.close(); 331 } catch (IOException e) { 332 Log.w(TAG, "Failed to close resource", e); 333 } 334 } 335 336 // The following is taken from SharedPreferencesCompat to avoid having a dependency of the 337 // multidex support library on another support library. 338 private static Method sApplyMethod; // final 339 static { 340 try { 341 Class cls = SharedPreferences.Editor.class; 342 sApplyMethod = cls.getMethod("apply"); 343 } catch (NoSuchMethodException unused) { 344 sApplyMethod = null; 345 } 346 } 347 348 private static void apply(SharedPreferences.Editor editor) { 349 if (sApplyMethod != null) { 350 try { 351 sApplyMethod.invoke(editor); 352 return; 353 } catch (InvocationTargetException unused) { 354 // fall through 355 } catch (IllegalAccessException unused) { 356 // fall through 357 } 358 } 359 editor.commit(); 360 } 361} 362