MultiDex.java revision 7b86f7e21c70ac06129c05ed137e585a308c6fd1
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
17package android.support.multidex;
18
19import android.app.Application;
20import android.content.Context;
21import android.content.pm.ApplicationInfo;
22import android.content.pm.PackageManager;
23import android.os.Build;
24import android.util.Log;
25
26import dalvik.system.DexFile;
27
28import 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;
40import java.util.regex.Matcher;
41import java.util.regex.Pattern;
42import java.util.zip.ZipFile;
43
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
47 * {@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
50 * {@link #install(Context)}.
51 *
52 * <p/>
53 * 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.
55 */
56public final class MultiDex {
57
58    static final String TAG = "MultiDex";
59
60    private static final String SECONDARY_FOLDER_NAME = "secondary-dexes";
61
62    private static final int MAX_SUPPORTED_SDK_VERSION = 20;
63
64    private static final int MIN_SDK_VERSION = 4;
65
66    private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
67
68    private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
69
70    private static final Set<String> installedApk = new HashSet<String>();
71
72    private static final boolean IS_VM_MULTIDEX_CAPABLE =
73            isVMMultidexCapable(System.getProperty("java.vm.version"));
74
75    private MultiDex() {}
76
77    /**
78     * Patches the application context class loader by appending extra dex files
79     * 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.
82     *
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) {
88        Log.i(TAG, "install");
89        if (IS_VM_MULTIDEX_CAPABLE) {
90            Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
91            return;
92        }
93
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
99        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
132                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") + "\"");
139                }
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);
167                List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
168                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.
173                    files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
174
175                    if (checkValidZipFiles(files)) {
176                        installSecondaryDexes(loader, dexDir, files);
177                    } else {
178                        // Second time didn't work, give up
179                        throw new RuntimeException("Zip files were not valid.");
180                    }
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        }
188        Log.i(TAG, "install done");
189    }
190
191    /**
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
220    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
247    /**
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) {
344                for (IOException e : suppressedExceptions) {
345                    Log.w(TAG, "Exception in makeDexElement", e);
346                }
347                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 {
427        private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
428                        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
432             * fields mPaths, mFiles, mZips and mDexs to append additional DEX
433             * 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];
442            ZipFile[] extraZips = new ZipFile[extraSize];
443            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;
452                extraZips[index] = new ZipFile(additionalEntry);
453                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);
459            expandFieldArray(loader, "mZips", extraZips);
460            expandFieldArray(loader, "mDexs", extraDexs);
461        }
462    }
463
464}
465