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 17 package android.support.multidex; 18 19 import android.app.Application; 20 import android.content.Context; 21 import android.content.pm.ApplicationInfo; 22 import android.content.pm.PackageManager; 23 import android.content.pm.PackageManager.NameNotFoundException; 24 import android.os.Build; 25 import android.util.Log; 26 27 import dalvik.system.DexFile; 28 29 import java.io.File; 30 import java.io.IOException; 31 import java.lang.reflect.Array; 32 import java.lang.reflect.Field; 33 import java.lang.reflect.InvocationTargetException; 34 import java.lang.reflect.Method; 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.HashSet; 38 import java.util.List; 39 import java.util.ListIterator; 40 import java.util.Set; 41 import java.util.regex.Matcher; 42 import java.util.regex.Pattern; 43 import java.util.zip.ZipFile; 44 45 /** 46 * Monkey patches {@link Context#getClassLoader() the application context class 47 * loader} in order to load classes from more than one dex file. The primary 48 * {@code classes.dex} must contain the classes necessary for calling this 49 * class methods. Secondary dex files named classes2.dex, classes3.dex... found 50 * in the application apk will be added to the classloader after first call to 51 * {@link #install(Context)}. 52 * 53 * <p/> 54 * This library provides compatibility for platforms with API level 4 through 20. This library does 55 * nothing on newer versions of the platform which provide built-in support for secondary dex files. 56 */ 57 public final class MultiDex { 58 59 static final String TAG = "MultiDex"; 60 61 private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes"; 62 63 private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator + 64 "secondary-dexes"; 65 66 private static final int MAX_SUPPORTED_SDK_VERSION = 20; 67 68 private static final int MIN_SDK_VERSION = 4; 69 70 private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2; 71 72 private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1; 73 74 private static final Set<String> installedApk = new HashSet<String>(); 75 76 private static final boolean IS_VM_MULTIDEX_CAPABLE = 77 isVMMultidexCapable(System.getProperty("java.vm.version")); 78 MultiDex()79 private MultiDex() {} 80 81 /** 82 * Patches the application context class loader by appending extra dex files 83 * loaded from the application apk. This method should be called in the 84 * attachBaseContext of your {@link Application}, see 85 * {@link MultiDexApplication} for more explanation and an example. 86 * 87 * @param context application context. 88 * @throws RuntimeException if an error occurred preventing the classloader 89 * extension. 90 */ install(Context context)91 public static void install(Context context) { 92 Log.i(TAG, "install"); 93 if (IS_VM_MULTIDEX_CAPABLE) { 94 Log.i(TAG, "VM has multidex support, MultiDex support library is disabled."); 95 return; 96 } 97 98 if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) { 99 throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT 100 + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + "."); 101 } 102 103 try { 104 ApplicationInfo applicationInfo = getApplicationInfo(context); 105 if (applicationInfo == null) { 106 // Looks like running on a test Context, so just return without patching. 107 return; 108 } 109 110 synchronized (installedApk) { 111 String apkPath = applicationInfo.sourceDir; 112 if (installedApk.contains(apkPath)) { 113 return; 114 } 115 installedApk.add(apkPath); 116 117 if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) { 118 Log.w(TAG, "MultiDex is not guaranteed to work in SDK version " 119 + Build.VERSION.SDK_INT + ": SDK version higher than " 120 + MAX_SUPPORTED_SDK_VERSION + " should be backed by " 121 + "runtime with built-in multidex capabilty but it's not the " 122 + "case here: java.vm.version=\"" 123 + System.getProperty("java.vm.version") + "\""); 124 } 125 126 /* The patched class loader is expected to be a descendant of 127 * dalvik.system.BaseDexClassLoader. We modify its 128 * dalvik.system.DexPathList pathList field to append additional DEX 129 * file entries. 130 */ 131 ClassLoader loader; 132 try { 133 loader = context.getClassLoader(); 134 } catch (RuntimeException e) { 135 /* Ignore those exceptions so that we don't break tests relying on Context like 136 * a android.test.mock.MockContext or a android.content.ContextWrapper with a 137 * null base Context. 138 */ 139 Log.w(TAG, "Failure while trying to obtain Context class loader. " + 140 "Must be running in test mode. Skip patching.", e); 141 return; 142 } 143 if (loader == null) { 144 // Note, the context class loader is null when running Robolectric tests. 145 Log.e(TAG, 146 "Context class loader is null. Must be running in test mode. " 147 + "Skip patching."); 148 return; 149 } 150 151 try { 152 clearOldDexDir(context); 153 } catch (Throwable t) { 154 Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, " 155 + "continuing without cleaning.", t); 156 } 157 158 File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME); 159 List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false); 160 if (checkValidZipFiles(files)) { 161 installSecondaryDexes(loader, dexDir, files); 162 } else { 163 Log.w(TAG, "Files were not valid zip files. Forcing a reload."); 164 // Try again, but this time force a reload of the zip file. 165 files = MultiDexExtractor.load(context, applicationInfo, dexDir, true); 166 167 if (checkValidZipFiles(files)) { 168 installSecondaryDexes(loader, dexDir, files); 169 } else { 170 // Second time didn't work, give up 171 throw new RuntimeException("Zip files were not valid."); 172 } 173 } 174 } 175 176 } catch (Exception e) { 177 Log.e(TAG, "Multidex installation failure", e); 178 throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ")."); 179 } 180 Log.i(TAG, "install done"); 181 } 182 getApplicationInfo(Context context)183 private static ApplicationInfo getApplicationInfo(Context context) 184 throws NameNotFoundException { 185 PackageManager pm; 186 String packageName; 187 try { 188 pm = context.getPackageManager(); 189 packageName = context.getPackageName(); 190 } catch (RuntimeException e) { 191 /* Ignore those exceptions so that we don't break tests relying on Context like 192 * a android.test.mock.MockContext or a android.content.ContextWrapper with a null 193 * base Context. 194 */ 195 Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " + 196 "Must be running in test mode. Skip patching.", e); 197 return null; 198 } 199 if (pm == null || packageName == null) { 200 // This is most likely a mock context, so just return without patching. 201 return null; 202 } 203 ApplicationInfo applicationInfo = 204 pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); 205 return applicationInfo; 206 } 207 208 /** 209 * Identifies if the current VM has a native support for multidex, meaning there is no need for 210 * additional installation by this library. 211 * @return true if the VM handles multidex 212 */ 213 /* package visible for test */ isVMMultidexCapable(String versionString)214 static boolean isVMMultidexCapable(String versionString) { 215 boolean isMultidexCapable = false; 216 if (versionString != null) { 217 Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString); 218 if (matcher.matches()) { 219 try { 220 int major = Integer.parseInt(matcher.group(1)); 221 int minor = Integer.parseInt(matcher.group(2)); 222 isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR) 223 || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR) 224 && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR)); 225 } catch (NumberFormatException e) { 226 // let isMultidexCapable be false 227 } 228 } 229 } 230 Log.i(TAG, "VM with version " + versionString + 231 (isMultidexCapable ? 232 " has multidex support" : 233 " does not have multidex support")); 234 return isMultidexCapable; 235 } 236 installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)237 private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files) 238 throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, 239 InvocationTargetException, NoSuchMethodException, IOException { 240 if (!files.isEmpty()) { 241 if (Build.VERSION.SDK_INT >= 19) { 242 V19.install(loader, files, dexDir); 243 } else if (Build.VERSION.SDK_INT >= 14) { 244 V14.install(loader, files, dexDir); 245 } else { 246 V4.install(loader, files); 247 } 248 } 249 } 250 251 /** 252 * Returns whether all files in the list are valid zip files. If {@code files} is empty, then 253 * returns true. 254 */ checkValidZipFiles(List<File> files)255 private static boolean checkValidZipFiles(List<File> files) { 256 for (File file : files) { 257 if (!MultiDexExtractor.verifyZipFile(file)) { 258 return false; 259 } 260 } 261 return true; 262 } 263 264 /** 265 * Locates a given field anywhere in the class inheritance hierarchy. 266 * 267 * @param instance an object to search the field into. 268 * @param name field name 269 * @return a field object 270 * @throws NoSuchFieldException if the field cannot be located 271 */ findField(Object instance, String name)272 private static Field findField(Object instance, String name) throws NoSuchFieldException { 273 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { 274 try { 275 Field field = clazz.getDeclaredField(name); 276 277 278 if (!field.isAccessible()) { 279 field.setAccessible(true); 280 } 281 282 return field; 283 } catch (NoSuchFieldException e) { 284 // ignore and search next 285 } 286 } 287 288 throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass()); 289 } 290 291 /** 292 * Locates a given method anywhere in the class inheritance hierarchy. 293 * 294 * @param instance an object to search the method into. 295 * @param name method name 296 * @param parameterTypes method parameter types 297 * @return a method object 298 * @throws NoSuchMethodException if the method cannot be located 299 */ findMethod(Object instance, String name, Class<?>... parameterTypes)300 private static Method findMethod(Object instance, String name, Class<?>... parameterTypes) 301 throws NoSuchMethodException { 302 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { 303 try { 304 Method method = clazz.getDeclaredMethod(name, parameterTypes); 305 306 307 if (!method.isAccessible()) { 308 method.setAccessible(true); 309 } 310 311 return method; 312 } catch (NoSuchMethodException e) { 313 // ignore and search next 314 } 315 } 316 317 throw new NoSuchMethodException("Method " + name + " with parameters " + 318 Arrays.asList(parameterTypes) + " not found in " + instance.getClass()); 319 } 320 321 /** 322 * Replace the value of a field containing a non null array, by a new array containing the 323 * elements of the original array plus the elements of extraElements. 324 * @param instance the instance whose field is to be modified. 325 * @param fieldName the field to modify. 326 * @param extraElements elements to append at the end of the array. 327 */ expandFieldArray(Object instance, String fieldName, Object[] extraElements)328 private static void expandFieldArray(Object instance, String fieldName, 329 Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, 330 IllegalAccessException { 331 Field jlrField = findField(instance, fieldName); 332 Object[] original = (Object[]) jlrField.get(instance); 333 Object[] combined = (Object[]) Array.newInstance( 334 original.getClass().getComponentType(), original.length + extraElements.length); 335 System.arraycopy(original, 0, combined, 0, original.length); 336 System.arraycopy(extraElements, 0, combined, original.length, extraElements.length); 337 jlrField.set(instance, combined); 338 } 339 clearOldDexDir(Context context)340 private static void clearOldDexDir(Context context) throws Exception { 341 File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME); 342 if (dexDir.isDirectory()) { 343 Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ")."); 344 File[] files = dexDir.listFiles(); 345 if (files == null) { 346 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); 347 return; 348 } 349 for (File oldFile : files) { 350 Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " 351 + oldFile.length()); 352 if (!oldFile.delete()) { 353 Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); 354 } else { 355 Log.i(TAG, "Deleted old file " + oldFile.getPath()); 356 } 357 } 358 if (!dexDir.delete()) { 359 Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath()); 360 } else { 361 Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath()); 362 } 363 } 364 } 365 366 /** 367 * Installer for platform versions 19. 368 */ 369 private static final class V19 { 370 install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory)371 private static void install(ClassLoader loader, List<File> additionalClassPathEntries, 372 File optimizedDirectory) 373 throws IllegalArgumentException, IllegalAccessException, 374 NoSuchFieldException, InvocationTargetException, NoSuchMethodException { 375 /* The patched class loader is expected to be a descendant of 376 * dalvik.system.BaseDexClassLoader. We modify its 377 * dalvik.system.DexPathList pathList field to append additional DEX 378 * file entries. 379 */ 380 Field pathListField = findField(loader, "pathList"); 381 Object dexPathList = pathListField.get(loader); 382 ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); 383 expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, 384 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory, 385 suppressedExceptions)); 386 if (suppressedExceptions.size() > 0) { 387 for (IOException e : suppressedExceptions) { 388 Log.w(TAG, "Exception in makeDexElement", e); 389 } 390 Field suppressedExceptionsField = 391 findField(loader, "dexElementsSuppressedExceptions"); 392 IOException[] dexElementsSuppressedExceptions = 393 (IOException[]) suppressedExceptionsField.get(loader); 394 395 if (dexElementsSuppressedExceptions == null) { 396 dexElementsSuppressedExceptions = 397 suppressedExceptions.toArray( 398 new IOException[suppressedExceptions.size()]); 399 } else { 400 IOException[] combined = 401 new IOException[suppressedExceptions.size() + 402 dexElementsSuppressedExceptions.length]; 403 suppressedExceptions.toArray(combined); 404 System.arraycopy(dexElementsSuppressedExceptions, 0, combined, 405 suppressedExceptions.size(), dexElementsSuppressedExceptions.length); 406 dexElementsSuppressedExceptions = combined; 407 } 408 409 suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions); 410 } 411 } 412 413 /** 414 * A wrapper around 415 * {@code private static final dalvik.system.DexPathList#makeDexElements}. 416 */ makeDexElements( Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions)417 private static Object[] makeDexElements( 418 Object dexPathList, ArrayList<File> files, File optimizedDirectory, 419 ArrayList<IOException> suppressedExceptions) 420 throws IllegalAccessException, InvocationTargetException, 421 NoSuchMethodException { 422 Method makeDexElements = 423 findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, 424 ArrayList.class); 425 426 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, 427 suppressedExceptions); 428 } 429 } 430 431 /** 432 * Installer for platform versions 14, 15, 16, 17 and 18. 433 */ 434 private static final class V14 { 435 install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory)436 private static void install(ClassLoader loader, List<File> additionalClassPathEntries, 437 File optimizedDirectory) 438 throws IllegalArgumentException, IllegalAccessException, 439 NoSuchFieldException, InvocationTargetException, NoSuchMethodException { 440 /* The patched class loader is expected to be a descendant of 441 * dalvik.system.BaseDexClassLoader. We modify its 442 * dalvik.system.DexPathList pathList field to append additional DEX 443 * file entries. 444 */ 445 Field pathListField = findField(loader, "pathList"); 446 Object dexPathList = pathListField.get(loader); 447 expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, 448 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory)); 449 } 450 451 /** 452 * A wrapper around 453 * {@code private static final dalvik.system.DexPathList#makeDexElements}. 454 */ makeDexElements( Object dexPathList, ArrayList<File> files, File optimizedDirectory)455 private static Object[] makeDexElements( 456 Object dexPathList, ArrayList<File> files, File optimizedDirectory) 457 throws IllegalAccessException, InvocationTargetException, 458 NoSuchMethodException { 459 Method makeDexElements = 460 findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class); 461 462 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory); 463 } 464 } 465 466 /** 467 * Installer for platform versions 4 to 13. 468 */ 469 private static final class V4 { install(ClassLoader loader, List<File> additionalClassPathEntries)470 private static void install(ClassLoader loader, List<File> additionalClassPathEntries) 471 throws IllegalArgumentException, IllegalAccessException, 472 NoSuchFieldException, IOException { 473 /* The patched class loader is expected to be a descendant of 474 * dalvik.system.DexClassLoader. We modify its 475 * fields mPaths, mFiles, mZips and mDexs to append additional DEX 476 * file entries. 477 */ 478 int extraSize = additionalClassPathEntries.size(); 479 480 Field pathField = findField(loader, "path"); 481 482 StringBuilder path = new StringBuilder((String) pathField.get(loader)); 483 String[] extraPaths = new String[extraSize]; 484 File[] extraFiles = new File[extraSize]; 485 ZipFile[] extraZips = new ZipFile[extraSize]; 486 DexFile[] extraDexs = new DexFile[extraSize]; 487 for (ListIterator<File> iterator = additionalClassPathEntries.listIterator(); 488 iterator.hasNext();) { 489 File additionalEntry = iterator.next(); 490 String entryPath = additionalEntry.getAbsolutePath(); 491 path.append(':').append(entryPath); 492 int index = iterator.previousIndex(); 493 extraPaths[index] = entryPath; 494 extraFiles[index] = additionalEntry; 495 extraZips[index] = new ZipFile(additionalEntry); 496 extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0); 497 } 498 499 pathField.set(loader, path.toString()); 500 expandFieldArray(loader, "mPaths", extraPaths); 501 expandFieldArray(loader, "mFiles", extraFiles); 502 expandFieldArray(loader, "mZips", extraZips); 503 expandFieldArray(loader, "mDexs", extraDexs); 504 } 505 } 506 507 } 508