blob: 22a7478d6cf675053f61109434a000ba5051b2a9 [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;
Maurice Chu66f379f2013-11-14 19:08:32 -080040import java.util.zip.ZipFile;
Maurice Chu667f9a82013-10-16 13:12:22 -070041
42/**
43 * Monkey patches {@link Context#getClassLoader() the application context class
44 * loader} in order to load classes from more than one dex file. The primary
Yohann Rousseldd3cc222014-04-22 10:47:53 +020045 * {@code classes.dex} must contain the classes necessary for calling this
46 * class methods. Secondary dex files named classes2.dex, classes3.dex... found
47 * in the application apk will be added to the classloader after first call to
Maurice Chu667f9a82013-10-16 13:12:22 -070048 * {@link #install(Context)}.
49 *
50 * <p/>
51 * <strong>IMPORTANT:</strong>This library provides compatibility for platforms
52 * with API level 4 through 19. This library does nothing on newer versions of
53 * the platform which provide built-in support for secondary dex files.
54 */
55public final class MultiDex {
56
57 static final String TAG = "MultiDex";
58
59 private static final String SECONDARY_FOLDER_NAME = "secondary-dexes";
60
61 private static final int SUPPORTED_MULTIDEX_SDK_VERSION = 20;
62
63 private static final int MIN_SDK_VERSION = 4;
64
65 private static final Set<String> installedApk = new HashSet<String>();
66
67 private MultiDex() {}
68
69 /**
70 * Patches the application context class loader by appending extra dex files
Yohann Rousseldd3cc222014-04-22 10:47:53 +020071 * loaded from the application apk. This method should be called in the
72 * attachBaseContext of your {@link Application}, see
73 * {@link MultiDexApplication} for more explanation and an example.
Maurice Chu667f9a82013-10-16 13:12:22 -070074 *
75 * @param context application context.
76 * @throws RuntimeException if an error occurred preventing the classloader
77 * extension.
78 */
79 public static void install(Context context) {
Yohann Roussel602c6ca2014-03-28 17:35:02 +010080 Log.i(TAG, "install");
Maurice Chu667f9a82013-10-16 13:12:22 -070081
82 if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
83 throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
84 + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
85 }
86
87
88 try {
89 PackageManager pm;
90 String packageName;
91 try {
92 pm = context.getPackageManager();
93 packageName = context.getPackageName();
94 } catch (RuntimeException e) {
95 /* Ignore those exceptions so that we don't break tests relying on Context like
96 * a android.test.mock.MockContext or a android.content.ContextWrapper with a null
97 * base Context.
98 */
99 Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " +
100 "Must be running in test mode. Skip patching.", e);
101 return;
102 }
103 if (pm == null || packageName == null) {
104 // This is most likely a mock context, so just return without patching.
105 return;
106 }
107 ApplicationInfo applicationInfo =
108 pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
109 if (applicationInfo == null) {
110 // This is from a mock context, so just return without patching.
111 return;
112 }
113
114 synchronized (installedApk) {
115 String apkPath = applicationInfo.sourceDir;
116 if (installedApk.contains(apkPath)) {
117 return;
118 }
119 installedApk.add(apkPath);
120
121 if (Build.VERSION.SDK_INT >= SUPPORTED_MULTIDEX_SDK_VERSION) {
122 // STOPSHIP: Any app that uses this class needs approval before being released
123 // as well as figuring out what the right behavior should be here.
124 throw new RuntimeException("Platform support of multidex for SDK " +
125 Build.VERSION.SDK_INT + " has not been confirmed yet.");
126 }
127
128 /* The patched class loader is expected to be a descendant of
129 * dalvik.system.BaseDexClassLoader. We modify its
130 * dalvik.system.DexPathList pathList field to append additional DEX
131 * file entries.
132 */
133 ClassLoader loader;
134 try {
135 loader = context.getClassLoader();
136 } catch (RuntimeException e) {
137 /* Ignore those exceptions so that we don't break tests relying on Context like
138 * a android.test.mock.MockContext or a android.content.ContextWrapper with a
139 * null base Context.
140 */
141 Log.w(TAG, "Failure while trying to obtain Context class loader. " +
142 "Must be running in test mode. Skip patching.", e);
143 return;
144 }
145 if (loader == null) {
146 // Note, the context class loader is null when running Robolectric tests.
147 Log.e(TAG,
148 "Context class loader is null. Must be running in test mode. "
149 + "Skip patching.");
150 return;
151 }
152
153 File dexDir = new File(context.getFilesDir(), SECONDARY_FOLDER_NAME);
Maurice Chu7e267a32014-01-15 19:02:18 -0800154 List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
Maurice Chucc63eda2013-12-02 15:39:59 -0800155 if (checkValidZipFiles(files)) {
156 installSecondaryDexes(loader, dexDir, files);
157 } else {
158 Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
159 // Try again, but this time force a reload of the zip file.
Maurice Chu7e267a32014-01-15 19:02:18 -0800160 files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100161
Maurice Chucc63eda2013-12-02 15:39:59 -0800162 if (checkValidZipFiles(files)) {
163 installSecondaryDexes(loader, dexDir, files);
Maurice Chu667f9a82013-10-16 13:12:22 -0700164 } else {
Maurice Chucc63eda2013-12-02 15:39:59 -0800165 // Second time didn't work, give up
166 throw new RuntimeException("Zip files were not valid.");
Maurice Chu667f9a82013-10-16 13:12:22 -0700167 }
168 }
169 }
170
171 } catch (Exception e) {
172 Log.e(TAG, "Multidex installation failure", e);
173 throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
174 }
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100175 Log.i(TAG, "install done");
Maurice Chu667f9a82013-10-16 13:12:22 -0700176 }
177
Maurice Chucc63eda2013-12-02 15:39:59 -0800178 private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
179 throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
180 InvocationTargetException, NoSuchMethodException, IOException {
181 if (!files.isEmpty()) {
182 if (Build.VERSION.SDK_INT >= 19) {
183 V19.install(loader, files, dexDir);
184 } else if (Build.VERSION.SDK_INT >= 14) {
185 V14.install(loader, files, dexDir);
186 } else {
187 V4.install(loader, files);
188 }
189 }
190 }
191
192 /**
193 * Returns whether all files in the list are valid zip files. If {@code files} is empty, then
194 * returns true.
195 */
196 private static boolean checkValidZipFiles(List<File> files) {
197 for (File file : files) {
198 if (!MultiDexExtractor.verifyZipFile(file)) {
199 return false;
200 }
201 }
202 return true;
203 }
204
Maurice Chu667f9a82013-10-16 13:12:22 -0700205 /**
206 * Locates a given field anywhere in the class inheritance hierarchy.
207 *
208 * @param instance an object to search the field into.
209 * @param name field name
210 * @return a field object
211 * @throws NoSuchFieldException if the field cannot be located
212 */
213 private static Field findField(Object instance, String name) throws NoSuchFieldException {
214 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
215 try {
216 Field field = clazz.getDeclaredField(name);
217
218
219 if (!field.isAccessible()) {
220 field.setAccessible(true);
221 }
222
223 return field;
224 } catch (NoSuchFieldException e) {
225 // ignore and search next
226 }
227 }
228
229 throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
230 }
231
232 /**
233 * Locates a given method anywhere in the class inheritance hierarchy.
234 *
235 * @param instance an object to search the method into.
236 * @param name method name
237 * @param parameterTypes method parameter types
238 * @return a method object
239 * @throws NoSuchMethodException if the method cannot be located
240 */
241 private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
242 throws NoSuchMethodException {
243 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
244 try {
245 Method method = clazz.getDeclaredMethod(name, parameterTypes);
246
247
248 if (!method.isAccessible()) {
249 method.setAccessible(true);
250 }
251
252 return method;
253 } catch (NoSuchMethodException e) {
254 // ignore and search next
255 }
256 }
257
258 throw new NoSuchMethodException("Method " + name + " with parameters " +
259 Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
260 }
261
262 /**
263 * Replace the value of a field containing a non null array, by a new array containing the
264 * elements of the original array plus the elements of extraElements.
265 * @param instance the instance whose field is to be modified.
266 * @param fieldName the field to modify.
267 * @param extraElements elements to append at the end of the array.
268 */
269 private static void expandFieldArray(Object instance, String fieldName,
270 Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
271 IllegalAccessException {
272 Field jlrField = findField(instance, fieldName);
273 Object[] original = (Object[]) jlrField.get(instance);
274 Object[] combined = (Object[]) Array.newInstance(
275 original.getClass().getComponentType(), original.length + extraElements.length);
276 System.arraycopy(original, 0, combined, 0, original.length);
277 System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
278 jlrField.set(instance, combined);
279 }
280
281 /**
282 * Installer for platform versions 19.
283 */
284 private static final class V19 {
285
286 private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
287 File optimizedDirectory)
288 throws IllegalArgumentException, IllegalAccessException,
289 NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
290 /* The patched class loader is expected to be a descendant of
291 * dalvik.system.BaseDexClassLoader. We modify its
292 * dalvik.system.DexPathList pathList field to append additional DEX
293 * file entries.
294 */
295 Field pathListField = findField(loader, "pathList");
296 Object dexPathList = pathListField.get(loader);
297 ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
298 expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
299 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
300 suppressedExceptions));
301 if (suppressedExceptions.size() > 0) {
Yohann Roussel88117c32013-11-28 23:22:11 +0100302 for (IOException e : suppressedExceptions) {
303 Log.w(TAG, "Exception in makeDexElement", e);
304 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700305 Field suppressedExceptionsField =
306 findField(loader, "dexElementsSuppressedExceptions");
307 IOException[] dexElementsSuppressedExceptions =
308 (IOException[]) suppressedExceptionsField.get(loader);
309
310 if (dexElementsSuppressedExceptions == null) {
311 dexElementsSuppressedExceptions =
312 suppressedExceptions.toArray(
313 new IOException[suppressedExceptions.size()]);
314 } else {
315 IOException[] combined =
316 new IOException[suppressedExceptions.size() +
317 dexElementsSuppressedExceptions.length];
318 suppressedExceptions.toArray(combined);
319 System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
320 suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
321 dexElementsSuppressedExceptions = combined;
322 }
323
324 suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions);
325 }
326 }
327
328 /**
329 * A wrapper around
330 * {@code private static final dalvik.system.DexPathList#makeDexElements}.
331 */
332 private static Object[] makeDexElements(
333 Object dexPathList, ArrayList<File> files, File optimizedDirectory,
334 ArrayList<IOException> suppressedExceptions)
335 throws IllegalAccessException, InvocationTargetException,
336 NoSuchMethodException {
337 Method makeDexElements =
338 findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
339 ArrayList.class);
340
341 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
342 suppressedExceptions);
343 }
344 }
345
346 /**
347 * Installer for platform versions 14, 15, 16, 17 and 18.
348 */
349 private static final class V14 {
350
351 private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
352 File optimizedDirectory)
353 throws IllegalArgumentException, IllegalAccessException,
354 NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
355 /* The patched class loader is expected to be a descendant of
356 * dalvik.system.BaseDexClassLoader. We modify its
357 * dalvik.system.DexPathList pathList field to append additional DEX
358 * file entries.
359 */
360 Field pathListField = findField(loader, "pathList");
361 Object dexPathList = pathListField.get(loader);
362 expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
363 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
364 }
365
366 /**
367 * A wrapper around
368 * {@code private static final dalvik.system.DexPathList#makeDexElements}.
369 */
370 private static Object[] makeDexElements(
371 Object dexPathList, ArrayList<File> files, File optimizedDirectory)
372 throws IllegalAccessException, InvocationTargetException,
373 NoSuchMethodException {
374 Method makeDexElements =
375 findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
376
377 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
378 }
379 }
380
381 /**
382 * Installer for platform versions 4 to 13.
383 */
384 private static final class V4 {
Yohann Roussel52eafa02013-11-21 11:46:53 +0100385 private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
Maurice Chu667f9a82013-10-16 13:12:22 -0700386 throws IllegalArgumentException, IllegalAccessException,
387 NoSuchFieldException, IOException {
388 /* The patched class loader is expected to be a descendant of
389 * dalvik.system.DexClassLoader. We modify its
Yohann Roussel52eafa02013-11-21 11:46:53 +0100390 * fields mPaths, mFiles, mZips and mDexs to append additional DEX
Maurice Chu667f9a82013-10-16 13:12:22 -0700391 * file entries.
392 */
393 int extraSize = additionalClassPathEntries.size();
394
395 Field pathField = findField(loader, "path");
396
397 StringBuilder path = new StringBuilder((String) pathField.get(loader));
398 String[] extraPaths = new String[extraSize];
399 File[] extraFiles = new File[extraSize];
Maurice Chu66f379f2013-11-14 19:08:32 -0800400 ZipFile[] extraZips = new ZipFile[extraSize];
Maurice Chu667f9a82013-10-16 13:12:22 -0700401 DexFile[] extraDexs = new DexFile[extraSize];
402 for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
403 iterator.hasNext();) {
404 File additionalEntry = iterator.next();
405 String entryPath = additionalEntry.getAbsolutePath();
406 path.append(':').append(entryPath);
407 int index = iterator.previousIndex();
408 extraPaths[index] = entryPath;
409 extraFiles[index] = additionalEntry;
Maurice Chu66f379f2013-11-14 19:08:32 -0800410 extraZips[index] = new ZipFile(additionalEntry);
Maurice Chu667f9a82013-10-16 13:12:22 -0700411 extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
412 }
413
414 pathField.set(loader, path.toString());
415 expandFieldArray(loader, "mPaths", extraPaths);
416 expandFieldArray(loader, "mFiles", extraFiles);
Maurice Chu66f379f2013-11-14 19:08:32 -0800417 expandFieldArray(loader, "mZips", extraZips);
Maurice Chu667f9a82013-10-16 13:12:22 -0700418 expandFieldArray(loader, "mDexs", extraDexs);
419 }
420 }
421
422}