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