MultiDexExtractor.java revision 7e267a38525afac2a571da186e770a2b86a01846
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.util.Log; 23 24import java.io.BufferedOutputStream; 25import java.io.Closeable; 26import java.io.File; 27import java.io.FileFilter; 28import java.io.FileNotFoundException; 29import java.io.FileOutputStream; 30import java.io.IOException; 31import java.io.InputStream; 32import java.lang.reflect.InvocationTargetException; 33import java.lang.reflect.Method; 34import java.util.ArrayList; 35import java.util.List; 36import java.util.zip.ZipEntry; 37import java.util.zip.ZipException; 38import java.util.zip.ZipFile; 39import java.util.zip.ZipOutputStream; 40 41/** 42 * Exposes application secondary dex files as files in the application data 43 * directory. 44 */ 45final class MultiDexExtractor { 46 47 private static final String TAG = MultiDex.TAG; 48 49 /** 50 * We look for additional dex files named {@code classes2.dex}, 51 * {@code classes3.dex}, etc. 52 */ 53 private static final String DEX_PREFIX = "classes"; 54 private static final String DEX_SUFFIX = ".dex"; 55 56 private static final String EXTRACTED_NAME_EXT = ".classes"; 57 private static final String EXTRACTED_SUFFIX = ".zip"; 58 private static final int MAX_EXTRACT_ATTEMPTS = 3; 59 60 private static final int BUFFER_SIZE = 0x4000; 61 62 private static final String PREFS_FILE = "multidex.version"; 63 private static final String KEY_APK_SIZE = "apk_size"; 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 long currentApkSize = sourceApk.length(); 81 final boolean isNewApk = getStoredApkSize(context) != currentApkSize; 82 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; 83 84 prepareDexDir(dexDir, extractedFilePrefix, isNewApk); 85 86 final List<File> files = new ArrayList<File>(); 87 final ZipFile apk = new ZipFile(applicationInfo.sourceDir); 88 89 // If the CRC of any of the dex files is different than what we have stored or the number of 90 // dex files are different, then force reload everything. 91 ArrayList<Long> dexCrcs = getAllDexCrcs(apk); 92 if (isAnyDexCrcDifferent(context, dexCrcs)) { 93 forceReload = true; 94 } 95 try { 96 97 int secondaryNumber = 2; 98 99 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); 100 while (dexFile != null) { 101 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; 102 File extractedFile = new File(dexDir, fileName); 103 files.add(extractedFile); 104 105 Log.i(TAG, "Need extracted file " + extractedFile); 106 if (forceReload || !extractedFile.isFile()) { 107 Log.i(TAG, "Extraction is needed for file " + extractedFile); 108 int numAttempts = 0; 109 boolean isExtractionSuccessful = false; 110 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) { 111 numAttempts++; 112 113 // Create a zip file (extractedFile) containing only the secondary dex file 114 // (dexFile) from the apk. 115 extract(apk, dexFile, extractedFile, extractedFilePrefix); 116 117 // Verify that the extracted file is indeed a zip file. 118 isExtractionSuccessful = verifyZipFile(extractedFile); 119 120 // Log the sha1 of the extracted zip file 121 Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") + 122 " - length " + extractedFile.getAbsolutePath() + ": " + 123 extractedFile.length()); 124 if (!isExtractionSuccessful) { 125 // Delete the extracted file 126 extractedFile.delete(); 127 } 128 } 129 if (isExtractionSuccessful) { 130 // Write the current apk size and the dex crc's into the shared preferences 131 putStoredApkSize(context, currentApkSize); 132 putStoredDexCrcs(context, dexCrcs); 133 } else { 134 throw new IOException("Could not create zip file " + 135 extractedFile.getAbsolutePath() + " for secondary dex (" + 136 secondaryNumber + ")"); 137 } 138 } else { 139 Log.i(TAG, "No extraction needed for " + extractedFile + " of size " + 140 extractedFile.length()); 141 } 142 secondaryNumber++; 143 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); 144 } 145 } finally { 146 try { 147 apk.close(); 148 } catch (IOException e) { 149 Log.w(TAG, "Failed to close resource", e); 150 } 151 } 152 153 return files; 154 } 155 156 /** 157 * Iterate through the expected dex files, classes.dex, classes2.dex, classes3.dex, etc. and 158 * return the CRC of each zip entry in a list. 159 */ 160 private static ArrayList<Long> getAllDexCrcs(ZipFile apk) { 161 ArrayList<Long> dexCrcs = new ArrayList<Long>(); 162 163 // Add the first one 164 dexCrcs.add(apk.getEntry(DEX_PREFIX + DEX_SUFFIX).getCrc()); 165 166 // Get the number of dex files in the apk. 167 int secondaryNumber = 2; 168 while (true) { 169 ZipEntry dexEntry = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); 170 if (dexEntry == null) { 171 break; 172 } 173 174 dexCrcs.add(dexEntry.getCrc()); 175 secondaryNumber++; 176 } 177 return dexCrcs; 178 } 179 180 /** 181 * Returns true if the number of dex files is different than what is stored in the shared 182 * preferences file or if any dex CRC value is different. 183 */ 184 private static boolean isAnyDexCrcDifferent(Context context, ArrayList<Long> dexCrcs) { 185 final ArrayList<Long> storedDexCrcs = getStoredDexCrcs(context); 186 187 if (dexCrcs.size() != storedDexCrcs.size()) { 188 return true; 189 } 190 191 // We know the length of storedDexCrcs and dexCrcs are the same. 192 for (int i = 0; i < storedDexCrcs.size(); i++) { 193 if (storedDexCrcs.get(i) != dexCrcs.get(i)) { 194 return true; 195 } 196 } 197 198 // All the same 199 return false; 200 } 201 202 private static long getStoredApkSize(Context context) { 203 SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0); 204 return prefs.getLong(KEY_APK_SIZE, -1L); 205 } 206 207 private static void putStoredApkSize(Context context, long apkSize) { 208 SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0); 209 SharedPreferences.Editor edit = prefs.edit(); 210 edit.putLong(KEY_APK_SIZE, apkSize); 211 apply(edit); 212 } 213 214 private static ArrayList<Long> getStoredDexCrcs(Context context) { 215 SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0); 216 int numDexFiles = prefs.getInt(KEY_NUM_DEX_FILES, 0); 217 ArrayList<Long> dexCrcs = new ArrayList<Long>(numDexFiles); 218 for (int i = 0; i < numDexFiles; i++) { 219 dexCrcs.add(prefs.getLong(makeDexCrcKey(i), 0)); 220 } 221 return dexCrcs; 222 } 223 224 private static void putStoredDexCrcs(Context context, ArrayList<Long> dexCrcs) { 225 SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0); 226 SharedPreferences.Editor edit = prefs.edit(); 227 edit.putInt(KEY_NUM_DEX_FILES, dexCrcs.size()); 228 for (int i = 0; i < dexCrcs.size(); i++) { 229 edit.putLong(makeDexCrcKey(i), dexCrcs.get(i)); 230 } 231 apply(edit); 232 } 233 234 private static String makeDexCrcKey(int i) { 235 return KEY_PREFIX_DEX_CRC + Integer.toString(i); 236 } 237 238 /** 239 * This always removes extraneous files. If {@code removeAll} is true, then any existing 240 * zip and dex files are removed from the secondary-dexes directory. 241 */ 242 private static void prepareDexDir(File dexDir, final String extractedFilePrefix, 243 final boolean removeAll) throws IOException { 244 dexDir.mkdir(); 245 if (!dexDir.isDirectory()) { 246 throw new IOException("Failed to create dex directory " + dexDir.getPath()); 247 } 248 249 // Clean possible old files 250 FileFilter filter = new FileFilter() { 251 252 @Override 253 public boolean accept(File pathname) { 254 return removeAll || !pathname.getName().startsWith(extractedFilePrefix); 255 } 256 }; 257 File[] files = dexDir.listFiles(filter); 258 if (files == null) { 259 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); 260 return; 261 } 262 for (File oldFile : files) { 263 Log.w(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + 264 oldFile.length()); 265 if (!oldFile.delete()) { 266 Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); 267 } else { 268 Log.w(TAG, "Deleted old file " + oldFile.getPath()); 269 } 270 } 271 } 272 273 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, 274 String extractedFilePrefix) throws IOException, FileNotFoundException { 275 276 InputStream in = apk.getInputStream(dexFile); 277 ZipOutputStream out = null; 278 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX, 279 extractTo.getParentFile()); 280 Log.i(TAG, "Extracting " + tmp.getPath()); 281 try { 282 out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); 283 try { 284 ZipEntry classesDex = new ZipEntry("classes.dex"); 285 // keep zip entry time since it is the criteria used by Dalvik 286 classesDex.setTime(dexFile.getTime()); 287 out.putNextEntry(classesDex); 288 289 byte[] buffer = new byte[BUFFER_SIZE]; 290 int length = in.read(buffer); 291 while (length != -1) { 292 out.write(buffer, 0, length); 293 length = in.read(buffer); 294 } 295 out.closeEntry(); 296 } finally { 297 out.close(); 298 } 299 Log.i(TAG, "Renaming to " + extractTo.getPath()); 300 if (!tmp.renameTo(extractTo)) { 301 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + 302 "\" to \"" + extractTo.getAbsolutePath() + "\""); 303 } 304 } finally { 305 closeQuietly(in); 306 tmp.delete(); // return status ignored 307 } 308 } 309 310 /** 311 * Returns whether the file is a valid zip file. 312 */ 313 static boolean verifyZipFile(File file) { 314 try { 315 ZipFile zipFile = new ZipFile(file); 316 try { 317 zipFile.close(); 318 return true; 319 } catch (IOException e) { 320 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath()); 321 } 322 } catch (ZipException ex) { 323 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex); 324 } catch (IOException ex) { 325 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex); 326 } 327 return false; 328 } 329 330 /** 331 * Closes the given {@code Closeable}. Suppresses any IO exceptions. 332 */ 333 private static void closeQuietly(Closeable closeable) { 334 try { 335 closeable.close(); 336 } catch (IOException e) { 337 Log.w(TAG, "Failed to close resource", e); 338 } 339 } 340 341 // The following is taken from SharedPreferencesCompat to avoid having a dependency of the 342 // multidex support library on another support library. 343 private static Method sApplyMethod; // final 344 static { 345 try { 346 Class cls = SharedPreferences.Editor.class; 347 sApplyMethod = cls.getMethod("apply"); 348 } catch (NoSuchMethodException unused) { 349 sApplyMethod = null; 350 } 351 } 352 353 private static void apply(SharedPreferences.Editor editor) { 354 if (sApplyMethod != null) { 355 try { 356 sApplyMethod.invoke(editor); 357 return; 358 } catch (InvocationTargetException unused) { 359 // fall through 360 } catch (IllegalAccessException unused) { 361 // fall through 362 } 363 } 364 editor.commit(); 365 } 366} 367