blob: d9f164f43f2800b0876200b5957e94887348657c [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
Maurice Chu66f379f2013-11-14 19:08:32 -080019import dalvik.system.DexFile;
20
Maurice Chu667f9a82013-10-16 13:12:22 -070021import android.content.Context;
22import android.content.pm.ApplicationInfo;
23import android.content.pm.PackageManager;
24import android.os.Build;
25import android.util.Log;
26
Maurice Chu667f9a82013-10-16 13:12:22 -070027import java.io.File;
28import java.io.IOException;
29import java.lang.reflect.Array;
30import java.lang.reflect.Field;
31import java.lang.reflect.InvocationTargetException;
32import java.lang.reflect.Method;
33import java.util.ArrayList;
34import java.util.Arrays;
35import java.util.HashSet;
36import java.util.List;
37import java.util.ListIterator;
38import java.util.Set;
Maurice Chu66f379f2013-11-14 19:08:32 -080039import java.util.zip.ZipFile;
Maurice Chu667f9a82013-10-16 13:12:22 -070040
41/**
42 * Monkey patches {@link Context#getClassLoader() the application context class
43 * loader} in order to load classes from more than one dex file. The primary
44 * {@code classes.dex} file necessary for calling this class methods. secondary
45 * dex files named classes2.dex, classes".dex... found in the application apk
46 * will be added to the classloader after first call to
47 * {@link #install(Context)}.
48 *
49 * <p/>
50 * <strong>IMPORTANT:</strong>This library provides compatibility for platforms
51 * with API level 4 through 19. This library does nothing on newer versions of
52 * the platform which provide built-in support for secondary dex files.
53 */
54public final class MultiDex {
55
56 static final String TAG = "MultiDex";
57
58 private static final String SECONDARY_FOLDER_NAME = "secondary-dexes";
59
60 private static final int SUPPORTED_MULTIDEX_SDK_VERSION = 20;
61
62 private static final int MIN_SDK_VERSION = 4;
63
64 private static final Set<String> installedApk = new HashSet<String>();
65
66 private MultiDex() {}
67
68 /**
69 * Patches the application context class loader by appending extra dex files
70 * loaded from the application apk. Call this method first thing in your
71 * {@code Application#OnCreate}, {@code Instrumentation#OnCreate},
72 * {@code BackupAgent#OnCreate}, {@code Service#OnCreate},
73 * {@code BroadcastReceiver#onReceive}, {@code Activity#OnCreate} and
74 * {@code ContentProvider#OnCreate} .
75 *
76 * @param context application context.
77 * @throws RuntimeException if an error occurred preventing the classloader
78 * extension.
79 */
80 public static void install(Context context) {
81
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);
Yohann Roussel52eafa02013-11-21 11:46:53 +0100154 List<File> files = MultiDexExtractor.load(applicationInfo, dexDir);
Maurice Chu667f9a82013-10-16 13:12:22 -0700155 if (!files.isEmpty()) {
156 if (Build.VERSION.SDK_INT >= 19) {
157 V19.install(loader, files, dexDir);
158 } else if (Build.VERSION.SDK_INT >= 14) {
159 V14.install(loader, files, dexDir);
160 } else {
Yohann Roussel52eafa02013-11-21 11:46:53 +0100161 V4.install(loader, files);
Maurice Chu667f9a82013-10-16 13:12:22 -0700162 }
163 }
164 }
165
166 } catch (Exception e) {
167 Log.e(TAG, "Multidex installation failure", e);
168 throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
169 }
170 }
171
172 /**
173 * Locates a given field anywhere in the class inheritance hierarchy.
174 *
175 * @param instance an object to search the field into.
176 * @param name field name
177 * @return a field object
178 * @throws NoSuchFieldException if the field cannot be located
179 */
180 private static Field findField(Object instance, String name) throws NoSuchFieldException {
181 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
182 try {
183 Field field = clazz.getDeclaredField(name);
184
185
186 if (!field.isAccessible()) {
187 field.setAccessible(true);
188 }
189
190 return field;
191 } catch (NoSuchFieldException e) {
192 // ignore and search next
193 }
194 }
195
196 throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
197 }
198
199 /**
200 * Locates a given method anywhere in the class inheritance hierarchy.
201 *
202 * @param instance an object to search the method into.
203 * @param name method name
204 * @param parameterTypes method parameter types
205 * @return a method object
206 * @throws NoSuchMethodException if the method cannot be located
207 */
208 private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
209 throws NoSuchMethodException {
210 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
211 try {
212 Method method = clazz.getDeclaredMethod(name, parameterTypes);
213
214
215 if (!method.isAccessible()) {
216 method.setAccessible(true);
217 }
218
219 return method;
220 } catch (NoSuchMethodException e) {
221 // ignore and search next
222 }
223 }
224
225 throw new NoSuchMethodException("Method " + name + " with parameters " +
226 Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
227 }
228
229 /**
230 * Replace the value of a field containing a non null array, by a new array containing the
231 * elements of the original array plus the elements of extraElements.
232 * @param instance the instance whose field is to be modified.
233 * @param fieldName the field to modify.
234 * @param extraElements elements to append at the end of the array.
235 */
236 private static void expandFieldArray(Object instance, String fieldName,
237 Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
238 IllegalAccessException {
239 Field jlrField = findField(instance, fieldName);
240 Object[] original = (Object[]) jlrField.get(instance);
241 Object[] combined = (Object[]) Array.newInstance(
242 original.getClass().getComponentType(), original.length + extraElements.length);
243 System.arraycopy(original, 0, combined, 0, original.length);
244 System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
245 jlrField.set(instance, combined);
246 }
247
248 /**
249 * Installer for platform versions 19.
250 */
251 private static final class V19 {
252
253 private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
254 File optimizedDirectory)
255 throws IllegalArgumentException, IllegalAccessException,
256 NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
257 /* The patched class loader is expected to be a descendant of
258 * dalvik.system.BaseDexClassLoader. We modify its
259 * dalvik.system.DexPathList pathList field to append additional DEX
260 * file entries.
261 */
262 Field pathListField = findField(loader, "pathList");
263 Object dexPathList = pathListField.get(loader);
264 ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
265 expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
266 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
267 suppressedExceptions));
268 if (suppressedExceptions.size() > 0) {
269 Field suppressedExceptionsField =
270 findField(loader, "dexElementsSuppressedExceptions");
271 IOException[] dexElementsSuppressedExceptions =
272 (IOException[]) suppressedExceptionsField.get(loader);
273
274 if (dexElementsSuppressedExceptions == null) {
275 dexElementsSuppressedExceptions =
276 suppressedExceptions.toArray(
277 new IOException[suppressedExceptions.size()]);
278 } else {
279 IOException[] combined =
280 new IOException[suppressedExceptions.size() +
281 dexElementsSuppressedExceptions.length];
282 suppressedExceptions.toArray(combined);
283 System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
284 suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
285 dexElementsSuppressedExceptions = combined;
286 }
287
288 suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions);
289 }
290 }
291
292 /**
293 * A wrapper around
294 * {@code private static final dalvik.system.DexPathList#makeDexElements}.
295 */
296 private static Object[] makeDexElements(
297 Object dexPathList, ArrayList<File> files, File optimizedDirectory,
298 ArrayList<IOException> suppressedExceptions)
299 throws IllegalAccessException, InvocationTargetException,
300 NoSuchMethodException {
301 Method makeDexElements =
302 findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
303 ArrayList.class);
304
305 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
306 suppressedExceptions);
307 }
308 }
309
310 /**
311 * Installer for platform versions 14, 15, 16, 17 and 18.
312 */
313 private static final class V14 {
314
315 private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
316 File optimizedDirectory)
317 throws IllegalArgumentException, IllegalAccessException,
318 NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
319 /* The patched class loader is expected to be a descendant of
320 * dalvik.system.BaseDexClassLoader. We modify its
321 * dalvik.system.DexPathList pathList field to append additional DEX
322 * file entries.
323 */
324 Field pathListField = findField(loader, "pathList");
325 Object dexPathList = pathListField.get(loader);
326 expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
327 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
328 }
329
330 /**
331 * A wrapper around
332 * {@code private static final dalvik.system.DexPathList#makeDexElements}.
333 */
334 private static Object[] makeDexElements(
335 Object dexPathList, ArrayList<File> files, File optimizedDirectory)
336 throws IllegalAccessException, InvocationTargetException,
337 NoSuchMethodException {
338 Method makeDexElements =
339 findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
340
341 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
342 }
343 }
344
345 /**
346 * Installer for platform versions 4 to 13.
347 */
348 private static final class V4 {
Yohann Roussel52eafa02013-11-21 11:46:53 +0100349 private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
Maurice Chu667f9a82013-10-16 13:12:22 -0700350 throws IllegalArgumentException, IllegalAccessException,
351 NoSuchFieldException, IOException {
352 /* The patched class loader is expected to be a descendant of
353 * dalvik.system.DexClassLoader. We modify its
Yohann Roussel52eafa02013-11-21 11:46:53 +0100354 * fields mPaths, mFiles, mZips and mDexs to append additional DEX
Maurice Chu667f9a82013-10-16 13:12:22 -0700355 * file entries.
356 */
357 int extraSize = additionalClassPathEntries.size();
358
359 Field pathField = findField(loader, "path");
360
361 StringBuilder path = new StringBuilder((String) pathField.get(loader));
362 String[] extraPaths = new String[extraSize];
363 File[] extraFiles = new File[extraSize];
Maurice Chu66f379f2013-11-14 19:08:32 -0800364 ZipFile[] extraZips = new ZipFile[extraSize];
Maurice Chu667f9a82013-10-16 13:12:22 -0700365 DexFile[] extraDexs = new DexFile[extraSize];
366 for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
367 iterator.hasNext();) {
368 File additionalEntry = iterator.next();
369 String entryPath = additionalEntry.getAbsolutePath();
370 path.append(':').append(entryPath);
371 int index = iterator.previousIndex();
372 extraPaths[index] = entryPath;
373 extraFiles[index] = additionalEntry;
Maurice Chu66f379f2013-11-14 19:08:32 -0800374 extraZips[index] = new ZipFile(additionalEntry);
Maurice Chu667f9a82013-10-16 13:12:22 -0700375 extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
376 }
377
378 pathField.set(loader, path.toString());
379 expandFieldArray(loader, "mPaths", extraPaths);
380 expandFieldArray(loader, "mFiles", extraFiles);
Maurice Chu66f379f2013-11-14 19:08:32 -0800381 expandFieldArray(loader, "mZips", extraZips);
Maurice Chu667f9a82013-10-16 13:12:22 -0700382 expandFieldArray(loader, "mDexs", extraDexs);
383 }
384 }
385
386}