[go: nahoru, domu]

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.content.pm.PackageManager.NameNotFoundException;
24import android.os.Build;
25import android.util.Log;
26
27import dalvik.system.DexFile;
28
29import java.io.File;
30import java.io.IOException;
31import java.lang.reflect.Array;
32import java.lang.reflect.Field;
33import java.lang.reflect.InvocationTargetException;
34import java.lang.reflect.Method;
35import java.util.ArrayList;
36import java.util.Arrays;
37import java.util.HashSet;
38import java.util.List;
39import java.util.ListIterator;
40import java.util.Set;
41import java.util.regex.Matcher;
42import java.util.regex.Pattern;
43import java.util.zip.ZipFile;
44
45/**
46 * Monkey patches {@link Context#getClassLoader() the application context class
47 * loader} in order to load classes from more than one dex file. The primary
48 * {@code classes.dex} must contain the classes necessary for calling this
49 * class methods. Secondary dex files named classes2.dex, classes3.dex... found
50 * in the application apk will be added to the classloader after first call to
51 * {@link #install(Context)}.
52 *
53 * <p/>
54 * This library provides compatibility for platforms with API level 4 through 20. This library does
55 * nothing on newer versions of the platform which provide built-in support for secondary dex files.
56 */
57public final class MultiDex {
58
59    static final String TAG = "MultiDex";
60
61    private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes";
62
63    private static final String CODE_CACHE_NAME = "code_cache";
64
65    private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "secondary-dexes";
66
67    private static final int MAX_SUPPORTED_SDK_VERSION = 20;
68
69    private static final int MIN_SDK_VERSION = 4;
70
71    private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
72
73    private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
74
75    private static final Set<String> installedApk = new HashSet<String>();
76
77    private static final boolean IS_VM_MULTIDEX_CAPABLE =
78            isVMMultidexCapable(System.getProperty("java.vm.version"));
79
80    private MultiDex() {}
81
82    /**
83     * Patches the application context class loader by appending extra dex files
84     * loaded from the application apk. This method should be called in the
85     * attachBaseContext of your {@link Application}, see
86     * {@link MultiDexApplication} for more explanation and an example.
87     *
88     * @param context application context.
89     * @throws RuntimeException if an error occurred preventing the classloader
90     *         extension.
91     */
92    public static void install(Context context) {
93        Log.i(TAG, "install");
94        if (IS_VM_MULTIDEX_CAPABLE) {
95            Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
96            return;
97        }
98
99        if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
100            throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
101                    + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
102        }
103
104        try {
105            ApplicationInfo applicationInfo = getApplicationInfo(context);
106            if (applicationInfo == null) {
107                // Looks like running on a test Context, so just return without patching.
108                return;
109            }
110
111            synchronized (installedApk) {
112                String apkPath = applicationInfo.sourceDir;
113                if (installedApk.contains(apkPath)) {
114                    return;
115                }
116                installedApk.add(apkPath);
117
118                if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
119                    Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
120                            + Build.VERSION.SDK_INT + ": SDK version higher than "
121                            + MAX_SUPPORTED_SDK_VERSION + " should be backed by "
122                            + "runtime with built-in multidex capabilty but it's not the "
123                            + "case here: java.vm.version=\""
124                            + System.getProperty("java.vm.version") + "\"");
125                }
126
127                /* The patched class loader is expected to be a descendant of
128                 * dalvik.system.BaseDexClassLoader. We modify its
129                 * dalvik.system.DexPathList pathList field to append additional DEX
130                 * file entries.
131                 */
132                ClassLoader loader;
133                try {
134                    loader = context.getClassLoader();
135                } catch (RuntimeException e) {
136                    /* Ignore those exceptions so that we don't break tests relying on Context like
137                     * a android.test.mock.MockContext or a android.content.ContextWrapper with a
138                     * null base Context.
139                     */
140                    Log.w(TAG, "Failure while trying to obtain Context class loader. " +
141                            "Must be running in test mode. Skip patching.", e);
142                    return;
143                }
144                if (loader == null) {
145                    // Note, the context class loader is null when running Robolectric tests.
146                    Log.e(TAG,
147                            "Context class loader is null. Must be running in test mode. "
148                            + "Skip patching.");
149                    return;
150                }
151
152                try {
153                  clearOldDexDir(context);
154                } catch (Throwable t) {
155                  Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
156                      + "continuing without cleaning.", t);
157                }
158
159                File dexDir = getDexDir(context, applicationInfo);
160                List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
161                if (checkValidZipFiles(files)) {
162                    installSecondaryDexes(loader, dexDir, files);
163                } else {
164                    Log.w(TAG, "Files were not valid zip files.  Forcing a reload.");
165                    // Try again, but this time force a reload of the zip file.
166                    files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
167
168                    if (checkValidZipFiles(files)) {
169                        installSecondaryDexes(loader, dexDir, files);
170                    } else {
171                        // Second time didn't work, give up
172                        throw new RuntimeException("Zip files were not valid.");
173                    }
174                }
175            }
176
177        } catch (Exception e) {
178            Log.e(TAG, "Multidex installation failure", e);
179            throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
180        }
181        Log.i(TAG, "install done");
182    }
183
184    private static ApplicationInfo getApplicationInfo(Context context)
185            throws NameNotFoundException {
186        PackageManager pm;
187        String packageName;
188        try {
189            pm = context.getPackageManager();
190            packageName = context.getPackageName();
191        } catch (RuntimeException e) {
192            /* Ignore those exceptions so that we don't break tests relying on Context like
193             * a android.test.mock.MockContext or a android.content.ContextWrapper with a null
194             * base Context.
195             */
196            Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " +
197                    "Must be running in test mode. Skip patching.", e);
198            return null;
199        }
200        if (pm == null || packageName == null) {
201            // This is most likely a mock context, so just return without patching.
202            return null;
203        }
204        ApplicationInfo applicationInfo =
205                pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
206        return applicationInfo;
207    }
208
209    /**
210     * Identifies if the current VM has a native support for multidex, meaning there is no need for
211     * additional installation by this library.
212     * @return true if the VM handles multidex
213     */
214    /* package visible for test */
215    static boolean isVMMultidexCapable(String versionString) {
216        boolean isMultidexCapable = false;
217        if (versionString != null) {
218            Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
219            if (matcher.matches()) {
220                try {
221                    int major = Integer.parseInt(matcher.group(1));
222                    int minor = Integer.parseInt(matcher.group(2));
223                    isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
224                            || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
225                                    && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
226                } catch (NumberFormatException e) {
227                    // let isMultidexCapable be false
228                }
229            }
230        }
231        Log.i(TAG, "VM with version " + versionString +
232                (isMultidexCapable ?
233                        " has multidex support" :
234                        " does not have multidex support"));
235        return isMultidexCapable;
236    }
237
238    private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
239            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
240            InvocationTargetException, NoSuchMethodException, IOException {
241        if (!files.isEmpty()) {
242            if (Build.VERSION.SDK_INT >= 19) {
243                V19.install(loader, files, dexDir);
244            } else if (Build.VERSION.SDK_INT >= 14) {
245                V14.install(loader, files, dexDir);
246            } else {
247                V4.install(loader, files);
248            }
249        }
250    }
251
252    /**
253     * Returns whether all files in the list are valid zip files.  If {@code files} is empty, then
254     * returns true.
255     */
256    private static boolean checkValidZipFiles(List<File> files) {
257        for (File file : files) {
258            if (!MultiDexExtractor.verifyZipFile(file)) {
259                return false;
260            }
261        }
262        return true;
263    }
264
265    /**
266     * Locates a given field anywhere in the class inheritance hierarchy.
267     *
268     * @param instance an object to search the field into.
269     * @param name field name
270     * @return a field object
271     * @throws NoSuchFieldException if the field cannot be located
272     */
273    private static Field findField(Object instance, String name) throws NoSuchFieldException {
274        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
275            try {
276                Field field = clazz.getDeclaredField(name);
277
278
279                if (!field.isAccessible()) {
280                    field.setAccessible(true);
281                }
282
283                return field;
284            } catch (NoSuchFieldException e) {
285                // ignore and search next
286            }
287        }
288
289        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
290    }
291
292    /**
293     * Locates a given method anywhere in the class inheritance hierarchy.
294     *
295     * @param instance an object to search the method into.
296     * @param name method name
297     * @param parameterTypes method parameter types
298     * @return a method object
299     * @throws NoSuchMethodException if the method cannot be located
300     */
301    private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
302            throws NoSuchMethodException {
303        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
304            try {
305                Method method = clazz.getDeclaredMethod(name, parameterTypes);
306
307
308                if (!method.isAccessible()) {
309                    method.setAccessible(true);
310                }
311
312                return method;
313            } catch (NoSuchMethodException e) {
314                // ignore and search next
315            }
316        }
317
318        throw new NoSuchMethodException("Method " + name + " with parameters " +
319                Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
320    }
321
322    /**
323     * Replace the value of a field containing a non null array, by a new array containing the
324     * elements of the original array plus the elements of extraElements.
325     * @param instance the instance whose field is to be modified.
326     * @param fieldName the field to modify.
327     * @param extraElements elements to append at the end of the array.
328     */
329    private static void expandFieldArray(Object instance, String fieldName,
330            Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
331            IllegalAccessException {
332        Field jlrField = findField(instance, fieldName);
333        Object[] original = (Object[]) jlrField.get(instance);
334        Object[] combined = (Object[]) Array.newInstance(
335                original.getClass().getComponentType(), original.length + extraElements.length);
336        System.arraycopy(original, 0, combined, 0, original.length);
337        System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
338        jlrField.set(instance, combined);
339    }
340
341    private static void clearOldDexDir(Context context) throws Exception {
342        File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME);
343        if (dexDir.isDirectory()) {
344            Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
345            File[] files = dexDir.listFiles();
346            if (files == null) {
347                Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
348                return;
349            }
350            for (File oldFile : files) {
351                Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size "
352                        + oldFile.length());
353                if (!oldFile.delete()) {
354                    Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
355                } else {
356                    Log.i(TAG, "Deleted old file " + oldFile.getPath());
357                }
358            }
359            if (!dexDir.delete()) {
360                Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath());
361            } else {
362                Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath());
363            }
364        }
365    }
366
367    private static File getDexDir(Context context, ApplicationInfo applicationInfo)
368            throws IOException {
369        File cache = new File(applicationInfo.dataDir, CODE_CACHE_NAME);
370        try {
371            mkdirChecked(cache);
372        } catch (IOException e) {
373            /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless
374             * files on disk if the device ever updates to android 5+. But since this seems to
375             * happen only on some devices running android 2, this should cause no pollution.
376             */
377            cache = new File(context.getFilesDir(), CODE_CACHE_NAME);
378            mkdirChecked(cache);
379        }
380        File dexDir = new File(cache, CODE_CACHE_SECONDARY_FOLDER_NAME);
381        mkdirChecked(dexDir);
382        return dexDir;
383    }
384
385    private static void mkdirChecked(File dir) throws IOException {
386        dir.mkdir();
387        if (!dir.isDirectory()) {
388            File parent = dir.getParentFile();
389            if (parent == null) {
390                Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null.");
391            } else {
392                Log.e(TAG, "Failed to create dir " + dir.getPath() +
393                        ". parent file is a dir " + parent.isDirectory() +
394                        ", a file " + parent.isFile() +
395                        ", exists " + parent.exists() +
396                        ", readable " + parent.canRead() +
397                        ", writable " + parent.canWrite());
398            }
399            throw new IOException("Failed to create directory " + dir.getPath());
400        }
401    }
402
403    /**
404     * Installer for platform versions 19.
405     */
406    private static final class V19 {
407
408        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
409                File optimizedDirectory)
410                        throws IllegalArgumentException, IllegalAccessException,
411                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
412            /* The patched class loader is expected to be a descendant of
413             * dalvik.system.BaseDexClassLoader. We modify its
414             * dalvik.system.DexPathList pathList field to append additional DEX
415             * file entries.
416             */
417            Field pathListField = findField(loader, "pathList");
418            Object dexPathList = pathListField.get(loader);
419            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
420            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
421                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
422                    suppressedExceptions));
423            if (suppressedExceptions.size() > 0) {
424                for (IOException e : suppressedExceptions) {
425                    Log.w(TAG, "Exception in makeDexElement", e);
426                }
427                Field suppressedExceptionsField =
428                        findField(dexPathList, "dexElementsSuppressedExceptions");
429                IOException[] dexElementsSuppressedExceptions =
430                        (IOException[]) suppressedExceptionsField.get(dexPathList);
431
432                if (dexElementsSuppressedExceptions == null) {
433                    dexElementsSuppressedExceptions =
434                            suppressedExceptions.toArray(
435                                    new IOException[suppressedExceptions.size()]);
436                } else {
437                    IOException[] combined =
438                            new IOException[suppressedExceptions.size() +
439                                            dexElementsSuppressedExceptions.length];
440                    suppressedExceptions.toArray(combined);
441                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
442                            suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
443                    dexElementsSuppressedExceptions = combined;
444                }
445
446                suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
447            }
448        }
449
450        /**
451         * A wrapper around
452         * {@code private static final dalvik.system.DexPathList#makeDexElements}.
453         */
454        private static Object[] makeDexElements(
455                Object dexPathList, ArrayList<File> files, File optimizedDirectory,
456                ArrayList<IOException> suppressedExceptions)
457                        throws IllegalAccessException, InvocationTargetException,
458                        NoSuchMethodException {
459            Method makeDexElements =
460                    findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
461                            ArrayList.class);
462
463            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
464                    suppressedExceptions);
465        }
466    }
467
468    /**
469     * Installer for platform versions 14, 15, 16, 17 and 18.
470     */
471    private static final class V14 {
472
473        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
474                File optimizedDirectory)
475                        throws IllegalArgumentException, IllegalAccessException,
476                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
477            /* The patched class loader is expected to be a descendant of
478             * dalvik.system.BaseDexClassLoader. We modify its
479             * dalvik.system.DexPathList pathList field to append additional DEX
480             * file entries.
481             */
482            Field pathListField = findField(loader, "pathList");
483            Object dexPathList = pathListField.get(loader);
484            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
485                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
486        }
487
488        /**
489         * A wrapper around
490         * {@code private static final dalvik.system.DexPathList#makeDexElements}.
491         */
492        private static Object[] makeDexElements(
493                Object dexPathList, ArrayList<File> files, File optimizedDirectory)
494                        throws IllegalAccessException, InvocationTargetException,
495                        NoSuchMethodException {
496            Method makeDexElements =
497                    findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
498
499            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
500        }
501    }
502
503    /**
504     * Installer for platform versions 4 to 13.
505     */
506    private static final class V4 {
507        private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
508                        throws IllegalArgumentException, IllegalAccessException,
509                        NoSuchFieldException, IOException {
510            /* The patched class loader is expected to be a descendant of
511             * dalvik.system.DexClassLoader. We modify its
512             * fields mPaths, mFiles, mZips and mDexs to append additional DEX
513             * file entries.
514             */
515            int extraSize = additionalClassPathEntries.size();
516
517            Field pathField = findField(loader, "path");
518
519            StringBuilder path = new StringBuilder((String) pathField.get(loader));
520            String[] extraPaths = new String[extraSize];
521            File[] extraFiles = new File[extraSize];
522            ZipFile[] extraZips = new ZipFile[extraSize];
523            DexFile[] extraDexs = new DexFile[extraSize];
524            for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
525                    iterator.hasNext();) {
526                File additionalEntry = iterator.next();
527                String entryPath = additionalEntry.getAbsolutePath();
528                path.append(':').append(entryPath);
529                int index = iterator.previousIndex();
530                extraPaths[index] = entryPath;
531                extraFiles[index] = additionalEntry;
532                extraZips[index] = new ZipFile(additionalEntry);
533                extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
534            }
535
536            pathField.set(loader, path.toString());
537            expandFieldArray(loader, "mPaths", extraPaths);
538            expandFieldArray(loader, "mFiles", extraFiles);
539            expandFieldArray(loader, "mZips", extraZips);
540            expandFieldArray(loader, "mDexs", extraDexs);
541        }
542    }
543
544}
545