MultiDex.java revision 602c6ca8cae4718ba8ff9f65e53305d002479359
105e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel/*
205e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel * Copyright (C) 2013 The Android Open Source Project
305e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel *
405e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel * Licensed under the Apache License, Version 2.0 (the "License");
505e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel * you may not use this file except in compliance with the License.
605e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel * You may obtain a copy of the License at
705e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel *
805e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel *      http://www.apache.org/licenses/LICENSE-2.0
905e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel *
1005e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel * Unless required by applicable law or agreed to in writing, software
1105e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel * distributed under the License is distributed on an "AS IS" BASIS,
1205e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1305e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel * See the License for the specific language governing permissions and
1405e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel * limitations under the License.
1505e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel */
1605e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel
1705e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselpackage android.support.multidex;
1805e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel
1905e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport android.content.Context;
2005e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport android.content.pm.ApplicationInfo;
2105e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport android.content.pm.PackageManager;
2205e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport android.os.Build;
2305e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport android.util.Log;
2405e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel
2505e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport dalvik.system.DexFile;
2605e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Roussel
2705e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport java.io.File;
2805e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport java.io.IOException;
2905e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport java.lang.reflect.Array;
3005e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport java.lang.reflect.Field;
3105e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport java.lang.reflect.InvocationTargetException;
3205e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport java.lang.reflect.Method;
3305e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport java.util.ArrayList;
3405e2a94c8b510131f43a686f5188d4c0f2a5eebdYohann Rousselimport java.util.Arrays;
35import java.util.HashSet;
36import java.util.List;
37import java.util.ListIterator;
38import java.util.Set;
39import java.util.zip.ZipFile;
40
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        Log.i(TAG, "install");
82
83        if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
84            throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
85                    + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
86        }
87
88
89        try {
90            PackageManager pm;
91            String packageName;
92            try {
93                pm = context.getPackageManager();
94                packageName = context.getPackageName();
95            } catch (RuntimeException e) {
96                /* Ignore those exceptions so that we don't break tests relying on Context like
97                 * a android.test.mock.MockContext or a android.content.ContextWrapper with a null
98                 * base Context.
99                 */
100                Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " +
101                        "Must be running in test mode. Skip patching.", e);
102                return;
103            }
104            if (pm == null || packageName == null) {
105                // This is most likely a mock context, so just return without patching.
106                return;
107            }
108            ApplicationInfo applicationInfo =
109                    pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
110            if (applicationInfo == null) {
111                // This is from a mock context, so just return without patching.
112                return;
113            }
114
115            synchronized (installedApk) {
116                String apkPath = applicationInfo.sourceDir;
117                if (installedApk.contains(apkPath)) {
118                    return;
119                }
120                installedApk.add(apkPath);
121
122                if (Build.VERSION.SDK_INT >= SUPPORTED_MULTIDEX_SDK_VERSION) {
123                    // STOPSHIP: Any app that uses this class needs approval before being released
124                    // as well as figuring out what the right behavior should be here.
125                    throw new RuntimeException("Platform support of multidex for SDK " +
126                            Build.VERSION.SDK_INT + " has not been confirmed yet.");
127                }
128
129                /* The patched class loader is expected to be a descendant of
130                 * dalvik.system.BaseDexClassLoader. We modify its
131                 * dalvik.system.DexPathList pathList field to append additional DEX
132                 * file entries.
133                 */
134                ClassLoader loader;
135                try {
136                    loader = context.getClassLoader();
137                } catch (RuntimeException e) {
138                    /* Ignore those exceptions so that we don't break tests relying on Context like
139                     * a android.test.mock.MockContext or a android.content.ContextWrapper with a
140                     * null base Context.
141                     */
142                    Log.w(TAG, "Failure while trying to obtain Context class loader. " +
143                            "Must be running in test mode. Skip patching.", e);
144                    return;
145                }
146                if (loader == null) {
147                    // Note, the context class loader is null when running Robolectric tests.
148                    Log.e(TAG,
149                            "Context class loader is null. Must be running in test mode. "
150                            + "Skip patching.");
151                    return;
152                }
153
154                File dexDir = new File(context.getFilesDir(), SECONDARY_FOLDER_NAME);
155                List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
156                if (checkValidZipFiles(files)) {
157                    installSecondaryDexes(loader, dexDir, files);
158                } else {
159                    Log.w(TAG, "Files were not valid zip files.  Forcing a reload.");
160                    // Try again, but this time force a reload of the zip file.
161                    files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
162
163                    if (checkValidZipFiles(files)) {
164                        installSecondaryDexes(loader, dexDir, files);
165                    } else {
166                        // Second time didn't work, give up
167                        throw new RuntimeException("Zip files were not valid.");
168                    }
169                }
170            }
171
172        } catch (Exception e) {
173            Log.e(TAG, "Multidex installation failure", e);
174            throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
175        }
176        Log.i(TAG, "install done");
177    }
178
179    private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
180            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
181            InvocationTargetException, NoSuchMethodException, IOException {
182        if (!files.isEmpty()) {
183            if (Build.VERSION.SDK_INT >= 19) {
184                V19.install(loader, files, dexDir);
185            } else if (Build.VERSION.SDK_INT >= 14) {
186                V14.install(loader, files, dexDir);
187            } else {
188                V4.install(loader, files);
189            }
190        }
191    }
192
193    /**
194     * Returns whether all files in the list are valid zip files.  If {@code files} is empty, then
195     * returns true.
196     */
197    private static boolean checkValidZipFiles(List<File> files) {
198        for (File file : files) {
199            if (!MultiDexExtractor.verifyZipFile(file)) {
200                return false;
201            }
202        }
203        return true;
204    }
205
206    /**
207     * Locates a given field anywhere in the class inheritance hierarchy.
208     *
209     * @param instance an object to search the field into.
210     * @param name field name
211     * @return a field object
212     * @throws NoSuchFieldException if the field cannot be located
213     */
214    private static Field findField(Object instance, String name) throws NoSuchFieldException {
215        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
216            try {
217                Field field = clazz.getDeclaredField(name);
218
219
220                if (!field.isAccessible()) {
221                    field.setAccessible(true);
222                }
223
224                return field;
225            } catch (NoSuchFieldException e) {
226                // ignore and search next
227            }
228        }
229
230        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
231    }
232
233    /**
234     * Locates a given method anywhere in the class inheritance hierarchy.
235     *
236     * @param instance an object to search the method into.
237     * @param name method name
238     * @param parameterTypes method parameter types
239     * @return a method object
240     * @throws NoSuchMethodException if the method cannot be located
241     */
242    private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
243            throws NoSuchMethodException {
244        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
245            try {
246                Method method = clazz.getDeclaredMethod(name, parameterTypes);
247
248
249                if (!method.isAccessible()) {
250                    method.setAccessible(true);
251                }
252
253                return method;
254            } catch (NoSuchMethodException e) {
255                // ignore and search next
256            }
257        }
258
259        throw new NoSuchMethodException("Method " + name + " with parameters " +
260                Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
261    }
262
263    /**
264     * Replace the value of a field containing a non null array, by a new array containing the
265     * elements of the original array plus the elements of extraElements.
266     * @param instance the instance whose field is to be modified.
267     * @param fieldName the field to modify.
268     * @param extraElements elements to append at the end of the array.
269     */
270    private static void expandFieldArray(Object instance, String fieldName,
271            Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
272            IllegalAccessException {
273        Field jlrField = findField(instance, fieldName);
274        Object[] original = (Object[]) jlrField.get(instance);
275        Object[] combined = (Object[]) Array.newInstance(
276                original.getClass().getComponentType(), original.length + extraElements.length);
277        System.arraycopy(original, 0, combined, 0, original.length);
278        System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
279        jlrField.set(instance, combined);
280    }
281
282    /**
283     * Installer for platform versions 19.
284     */
285    private static final class V19 {
286
287        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
288                File optimizedDirectory)
289                        throws IllegalArgumentException, IllegalAccessException,
290                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
291            /* The patched class loader is expected to be a descendant of
292             * dalvik.system.BaseDexClassLoader. We modify its
293             * dalvik.system.DexPathList pathList field to append additional DEX
294             * file entries.
295             */
296            Field pathListField = findField(loader, "pathList");
297            Object dexPathList = pathListField.get(loader);
298            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
299            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
300                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
301                    suppressedExceptions));
302            if (suppressedExceptions.size() > 0) {
303                for (IOException e : suppressedExceptions) {
304                    Log.w(TAG, "Exception in makeDexElement", e);
305                }
306                Field suppressedExceptionsField =
307                        findField(loader, "dexElementsSuppressedExceptions");
308                IOException[] dexElementsSuppressedExceptions =
309                        (IOException[]) suppressedExceptionsField.get(loader);
310
311                if (dexElementsSuppressedExceptions == null) {
312                    dexElementsSuppressedExceptions =
313                            suppressedExceptions.toArray(
314                                    new IOException[suppressedExceptions.size()]);
315                } else {
316                    IOException[] combined =
317                            new IOException[suppressedExceptions.size() +
318                                            dexElementsSuppressedExceptions.length];
319                    suppressedExceptions.toArray(combined);
320                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
321                            suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
322                    dexElementsSuppressedExceptions = combined;
323                }
324
325                suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions);
326            }
327        }
328
329        /**
330         * A wrapper around
331         * {@code private static final dalvik.system.DexPathList#makeDexElements}.
332         */
333        private static Object[] makeDexElements(
334                Object dexPathList, ArrayList<File> files, File optimizedDirectory,
335                ArrayList<IOException> suppressedExceptions)
336                        throws IllegalAccessException, InvocationTargetException,
337                        NoSuchMethodException {
338            Method makeDexElements =
339                    findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
340                            ArrayList.class);
341
342            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
343                    suppressedExceptions);
344        }
345    }
346
347    /**
348     * Installer for platform versions 14, 15, 16, 17 and 18.
349     */
350    private static final class V14 {
351
352        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
353                File optimizedDirectory)
354                        throws IllegalArgumentException, IllegalAccessException,
355                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
356            /* The patched class loader is expected to be a descendant of
357             * dalvik.system.BaseDexClassLoader. We modify its
358             * dalvik.system.DexPathList pathList field to append additional DEX
359             * file entries.
360             */
361            Field pathListField = findField(loader, "pathList");
362            Object dexPathList = pathListField.get(loader);
363            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
364                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
365        }
366
367        /**
368         * A wrapper around
369         * {@code private static final dalvik.system.DexPathList#makeDexElements}.
370         */
371        private static Object[] makeDexElements(
372                Object dexPathList, ArrayList<File> files, File optimizedDirectory)
373                        throws IllegalAccessException, InvocationTargetException,
374                        NoSuchMethodException {
375            Method makeDexElements =
376                    findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
377
378            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
379        }
380    }
381
382    /**
383     * Installer for platform versions 4 to 13.
384     */
385    private static final class V4 {
386        private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
387                        throws IllegalArgumentException, IllegalAccessException,
388                        NoSuchFieldException, IOException {
389            /* The patched class loader is expected to be a descendant of
390             * dalvik.system.DexClassLoader. We modify its
391             * fields mPaths, mFiles, mZips and mDexs to append additional DEX
392             * file entries.
393             */
394            int extraSize = additionalClassPathEntries.size();
395
396            Field pathField = findField(loader, "path");
397
398            StringBuilder path = new StringBuilder((String) pathField.get(loader));
399            String[] extraPaths = new String[extraSize];
400            File[] extraFiles = new File[extraSize];
401            ZipFile[] extraZips = new ZipFile[extraSize];
402            DexFile[] extraDexs = new DexFile[extraSize];
403            for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
404                    iterator.hasNext();) {
405                File additionalEntry = iterator.next();
406                String entryPath = additionalEntry.getAbsolutePath();
407                path.append(':').append(entryPath);
408                int index = iterator.previousIndex();
409                extraPaths[index] = entryPath;
410                extraFiles[index] = additionalEntry;
411                extraZips[index] = new ZipFile(additionalEntry);
412                extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
413            }
414
415            pathField.set(loader, path.toString());
416            expandFieldArray(loader, "mPaths", extraPaths);
417            expandFieldArray(loader, "mFiles", extraFiles);
418            expandFieldArray(loader, "mZips", extraZips);
419            expandFieldArray(loader, "mDexs", extraDexs);
420        }
421    }
422
423}
424