/* * Copyright 2014-present Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.buck.android.support.exopackage; import java.io.File; import java.lang.reflect.Array; import java.util.List; import android.annotation.TargetApi; import android.os.Build; import dalvik.system.BaseDexClassLoader; import dalvik.system.DexClassLoader; import dalvik.system.PathClassLoader; /** * Uses reflection to modify the system class loader. There's no way to override or replace the * system class loader with our own class loader that knows how to load our pre-dexed jars. This * uses reflection to modify the system class loader. This was written based on careful inspection * of the source Android source for {@link DexClassLoader} and {@link PathClassLoader}. */ class SystemClassLoaderAdder { private SystemClassLoaderAdder() {} /** * Installs a list of .dex.jar files into the application class loader. * * @param appClassLoader The application ClassLoader, which can be retrieved by calling * {@code getClassLoader} on the application Context. * @param optimizedDirectory Directory for storing optimized dex files. * @param dexJars The list of .dex.jar files to load. */ static void installDexJars( ClassLoader appClassLoader, File optimizedDirectory, List<File> dexJars) { SystemClassLoaderAdder classLoaderAdder = new SystemClassLoaderAdder(); for (File dexJar : dexJars) { DexClassLoader newClassLoader = new DexClassLoader( dexJar.getAbsolutePath(), optimizedDirectory.getAbsolutePath(), null, appClassLoader); classLoaderAdder.addPathsOfClassLoaderToSystemClassLoader( newClassLoader, (PathClassLoader) appClassLoader); } } /** * Adds the paths in {@code newClassLoader} to the paths in {@code systemClassLoader} using * reflection since there's no way to do this with public APIs. * * @param newClassLoader the class loader with the new paths * @param systemClassLoader the system class loader */ private void addPathsOfClassLoaderToSystemClassLoader( DexClassLoader newClassLoader, PathClassLoader systemClassLoader) { try { if (existsBaseDexClassLoader()) { // In ICS, PathClassLoader and DexClassLoader are built on a common base class called // BaseDexClassLoader. addNewClassLoaderToSystemClassLoaderWithBaseDex(newClassLoader, systemClassLoader); } else { // In earlier versions, PathClassLoader and DexClassLoader have similar layout but // don't share code. addNewClassLoaderToSystemClassLoaderPreBaseDex(newClassLoader, systemClassLoader); } } catch (NoSuchFieldException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } private boolean existsBaseDexClassLoader() { try { Class.forName("dalvik.system.BaseDexClassLoader"); return true; } catch (ClassNotFoundException e) { return false; } } /** * Adds the paths in {@code newClassLoader} to the paths in {@code systemClassLoader}. This works * with versions of Android that have {@link BaseDexClassLoader}. * * @param newClassLoader the class loader with the new paths * @param systemClassLoader the system class loader */ private void addNewClassLoaderToSystemClassLoaderWithBaseDex( DexClassLoader newClassLoader, PathClassLoader systemClassLoader) throws NoSuchFieldException, IllegalAccessException { Object currentElementsArray = getDexElementsArray(getDexPathList(systemClassLoader)); Object newElementsArray = getDexElementsArray(getDexPathList(newClassLoader)); Object mergedElementsArray = mergeArrays(currentElementsArray, newElementsArray); setDexElementsArray(getDexPathList(systemClassLoader), mergedElementsArray); } /** * Adds the paths in {@code newClassLoader} to the paths in {@code systemClassLoader}. This works * with versions of Android that pre-date {@link BaseDexClassLoader}. * * @param newClassLoader the class loader with the new paths * @param systemClassLoader the system class loader */ @SuppressWarnings("PMD.EmptyCatchBlock") private void addNewClassLoaderToSystemClassLoaderPreBaseDex( DexClassLoader newClassLoader, PathClassLoader systemClassLoader) throws NoSuchFieldException, IllegalAccessException { try { // The class loader is lazily initialized. Loading any class forces it to be initialized. newClassLoader.loadClass("foo"); } catch (ClassNotFoundException e) { // Expected to fail. } // Copy value in scalar field newClassLoader.mRawDexPath and array field // systemClassLoader.mPaths to a new array that replaces systemClassLoader.mPaths. Reflect.setField( systemClassLoader, PathClassLoader.class, "mPaths", mergeArrayAndScalar( Reflect.getField(systemClassLoader, PathClassLoader.class, "mPaths"), Reflect.getField(newClassLoader, DexClassLoader.class, "mRawDexPath") ) ); // Copy values in array field newClassLoader.mFiles and array field systemClassLoader.mFiles // to a new array that replaces systemClassLoader.mFiles. Reflect.setField( systemClassLoader, PathClassLoader.class, "mFiles", mergeArrays( Reflect.getField(systemClassLoader, PathClassLoader.class, "mFiles"), Reflect.getField(newClassLoader, DexClassLoader.class, "mFiles") ) ); // Copy values in array field newClassLoader.mZips and array field systemClassLoader.mZips // to a new array that replaces systemClassLoader.mZips. Reflect.setField( systemClassLoader, PathClassLoader.class, "mZips", mergeArrays( Reflect.getField(systemClassLoader, PathClassLoader.class, "mZips"), Reflect.getField(newClassLoader, DexClassLoader.class, "mZips") ) ); // Copy values in array field newClassLoader.mDexs and array field systemClassLoader.mDexs // to a new array that replaces systemClassLoader.mDexs. Reflect.setField( systemClassLoader, PathClassLoader.class, "mDexs", mergeArrays( Reflect.getField(systemClassLoader, PathClassLoader.class, "mDexs"), Reflect.getField(newClassLoader, DexClassLoader.class, "mDexs") ) ); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private Object getDexPathList(BaseDexClassLoader classLoader) throws NoSuchFieldException, IllegalAccessException { return Reflect.getField(classLoader, BaseDexClassLoader.class, "pathList"); } private Object getDexElementsArray(Object dexPathList) throws NoSuchFieldException, IllegalAccessException { return Reflect.getField(dexPathList, dexPathList.getClass(), "dexElements"); } private void setDexElementsArray(Object dexPathList, Object newElementArray) throws NoSuchFieldException, IllegalAccessException { Reflect.setField(dexPathList, dexPathList.getClass(), "dexElements", newElementArray); } private Object mergeArrays(Object array1, Object array2) { Class<?> arrayClass = array1.getClass(); Class<?> itemClass = arrayClass.getComponentType(); int array1Size = Array.getLength(array1); int array2Size = Array.getLength(array2); int newSize = array1Size + array2Size; Object newArray = Array.newInstance(itemClass, newSize); for (int i = 0; i < newSize; i++) { if (i < array1Size) { Array.set(newArray, i, Array.get(array1, i)); } else { Array.set(newArray, i, Array.get(array2, i - array1Size)); } } return newArray; } private Object mergeArrayAndScalar(Object array, Object scalar) { Class<?> arrayClass = array.getClass(); Class<?> itemClass = arrayClass.getComponentType(); int array1Size = Array.getLength(array); int newSize = array1Size + 1; Object newArray = Array.newInstance(itemClass, newSize); for (int i = 0; i < newSize; i++) { if (i < array1Size) { Array.set(newArray, i, Array.get(array, i)); } else { Array.set(newArray, i, scalar); } } return newArray; } }