[go: nahoru, domu]

MultiDex.java revision dd3cc22f2fbc8ea4dd5fa88978dc8d1ae5034bd9
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.zip.ZipFile;
41
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
45 * {@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
48 * {@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
71     * 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.
74     *
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) {
80        Log.i(TAG, "install");
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);
154                List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
155                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.
160                    files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
161
162                    if (checkValidZipFiles(files)) {
163                        installSecondaryDexes(loader, dexDir, files);
164                    } else {
165                        // Second time didn't work, give up
166                        throw new RuntimeException("Zip files were not valid.");
167                    }
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        }
175        Log.i(TAG, "install done");
176    }
177
178    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
205    /**
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) {
302                for (IOException e : suppressedExceptions) {
303                    Log.w(TAG, "Exception in makeDexElement", e);
304                }
305                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 {
385        private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
386                        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
390             * fields mPaths, mFiles, mZips and mDexs to append additional DEX
391             * 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];
400            ZipFile[] extraZips = new ZipFile[extraSize];
401            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;
410                extraZips[index] = new ZipFile(additionalEntry);
411                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);
417            expandFieldArray(loader, "mZips", extraZips);
418            expandFieldArray(loader, "mDexs", extraDexs);
419        }
420    }
421
422}
423