diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java index b00b374fa1b19..e53a08a8f3d4c 100644 --- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java +++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java @@ -28,12 +28,12 @@ import android.os.UserHandle; import android.os.WorkSource; import android.util.Log; import android.util.Slog; -import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.IndentingPrintWriter; import com.android.server.pm.Installer.InstallerException; import com.android.server.pm.dex.DexoptOptions; +import com.android.server.pm.dex.DexoptUtils; import java.io.File; import java.io.IOException; @@ -157,12 +157,14 @@ public class PackageDexOptimizer { final boolean profileUpdated = options.isCheckForProfileUpdates() && isProfileUpdated(pkg, sharedGid, compilerFilter); - final String sharedLibrariesPath = getSharedLibrariesPath(sharedLibraries); // Get the dexopt flags after getRealCompilerFilter to make sure we get the correct flags. final int dexoptFlags = getDexFlags(pkg, compilerFilter, options.isBootComplete()); - // Get the dependencies of each split in the package. For each code path in the package, - // this array contains the relative paths of each split it depends on, separated by colons. - String[] splitDependencies = getSplitDependencies(pkg); + + // Get the class loader context dependencies. + // For each code path in the package, this array contains the class loader context that + // needs to be passed to dexopt in order to ensure correct optimizations. + String[] classLoaderContexts = DexoptUtils.getClassLoaderContexts( + pkg.applicationInfo, sharedLibraries); int result = DEX_OPT_SKIPPED; for (int i = 0; i < paths.size(); i++) { @@ -181,17 +183,10 @@ public class PackageDexOptimizer { } } - String sharedLibrariesPathWithSplits; - if (sharedLibrariesPath != null && splitDependencies[i] != null) { - sharedLibrariesPathWithSplits = sharedLibrariesPath + ":" + splitDependencies[i]; - } else { - sharedLibrariesPathWithSplits = - splitDependencies[i] != null ? splitDependencies[i] : sharedLibrariesPath; - } for (String dexCodeIsa : dexCodeInstructionSets) { - int newResult = dexOptPath(pkg, path, dexCodeIsa, compilerFilter, profileUpdated, - sharedLibrariesPathWithSplits, dexoptFlags, sharedGid, packageStats, - options.isDowngrade()); + int newResult = dexOptPath(pkg, path, dexCodeIsa, compilerFilter, + profileUpdated, classLoaderContexts[i], dexoptFlags, sharedGid, + packageStats, options.isDowngrade()); // The end result is: // - FAILED if any path failed, // - PERFORMED if at least one path needed compilation, @@ -459,86 +454,6 @@ public class PackageDexOptimizer { return adjustDexoptNeeded(dexoptNeeded); } - /** - * Computes the shared libraries path that should be passed to dexopt. - */ - private String getSharedLibrariesPath(String[] sharedLibraries) { - if (sharedLibraries == null || sharedLibraries.length == 0) { - return null; - } - StringBuilder sb = new StringBuilder(); - for (String lib : sharedLibraries) { - if (sb.length() != 0) { - sb.append(":"); - } - sb.append(lib); - } - return sb.toString(); - } - - /** - * Walks dependency tree and gathers the dependencies for each split in a split apk. - * The split paths are stored as relative paths, separated by colons. - */ - private String[] getSplitDependencies(PackageParser.Package pkg) { - // Convert all the code paths to relative paths. - String baseCodePath = new File(pkg.baseCodePath).getParent(); - List paths = pkg.getAllCodePaths(); - String[] splitDependencies = new String[paths.size()]; - for (int i = 0; i < paths.size(); i++) { - File pathFile = new File(paths.get(i)); - String fileName = pathFile.getName(); - paths.set(i, fileName); - - // Sanity check that the base paths of the splits are all the same. - String basePath = pathFile.getParent(); - if (!basePath.equals(baseCodePath)) { - Slog.wtf(TAG, "Split paths have different base paths: " + basePath + " and " + - baseCodePath); - } - } - - // If there are no other dependencies, fill in the implicit dependency on the base apk. - SparseArray dependencies = pkg.applicationInfo.splitDependencies; - if (dependencies == null) { - for (int i = 1; i < paths.size(); i++) { - splitDependencies[i] = paths.get(0); - } - return splitDependencies; - } - - // Fill in the dependencies, skipping the base apk which has no dependencies. - for (int i = 1; i < dependencies.size(); i++) { - getParentDependencies(dependencies.keyAt(i), paths, dependencies, splitDependencies); - } - - return splitDependencies; - } - - /** - * Recursive method to generate dependencies for a particular split. - * The index is a key from the package's splitDependencies. - */ - private String getParentDependencies(int index, List paths, - SparseArray dependencies, String[] splitDependencies) { - // The base apk is always first, and has no dependencies. - if (index == 0) { - return null; - } - // Return the result if we've computed the dependencies for this index already. - if (splitDependencies[index] != null) { - return splitDependencies[index]; - } - // Get the dependencies for the parent of this index and append its path to it. - int parent = dependencies.get(index)[0]; - String parentDependencies = - getParentDependencies(parent, paths, dependencies, splitDependencies); - String path = parentDependencies == null ? paths.get(parent) : - parentDependencies + ":" + paths.get(parent); - splitDependencies[index] = path; - return path; - } - /** * Checks if there is an update on the profile information of the {@code pkg}. * If the compiler filter is not profile guided the method returns false. diff --git a/services/core/java/com/android/server/pm/dex/DexoptUtils.java b/services/core/java/com/android/server/pm/dex/DexoptUtils.java new file mode 100644 index 0000000000000..c750f6526c7dc --- /dev/null +++ b/services/core/java/com/android/server/pm/dex/DexoptUtils.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.server.pm.dex; + + +import android.content.pm.ApplicationInfo; +import android.util.Slog; +import android.util.SparseArray; + +import java.io.File; + +public final class DexoptUtils { + private static final String TAG = "DexoptUtils"; + + private DexoptUtils() {} + + /** + * Creates the class loader context dependencies for each of the application code paths. + * The returned array contains the class loader contexts that needs to be passed to dexopt in + * order to ensure correct optimizations. + * + * A class loader context describes how the class loader chain should be built by dex2oat + * in order to ensure that classes are resolved during compilation as they would be resolved + * at runtime. The context will be encoded in the compiled code. If at runtime the dex file is + * loaded in a different context (with a different set of class loaders or a different + * classpath), the compiled code will be rejected. + * + * Note that the class loader context only includes dependencies and not the code path itself. + * The contexts are created based on the application split dependency list and + * the provided shared libraries. + * + * All the code paths encoded in the context will be relative to the base directory. This + * enables stage compilation where compiler artifacts may be moved around. + * + * The result is indexed as follows: + * - index 0 contains the context for the base apk + * - index 1 to n contain the context for the splits in the order determined by + * {@code info.getSplitCodePaths()} + */ + public static String[] getClassLoaderContexts(ApplicationInfo info, String[] sharedLibraries) { + // The base class loader context contains only the shared library. + String sharedLibrariesClassPath = encodeClasspath(sharedLibraries); + String baseApkContextClassLoader = encodeClassLoader( + sharedLibrariesClassPath, "dalvik.system.PathClassLoader"); + + String[] splitCodePaths = info.getSplitCodePaths(); + + if (splitCodePaths == null) { + // The application has no splits. + return new String[] {baseApkContextClassLoader}; + } + + // The application has splits. Compute their class loader contexts. + + // The splits have an implicit dependency on the base apk. + // This means that we have to add the base apk file in addition to the shared libraries. + String baseApkName = new File(info.getBaseCodePath()).getName(); + String splitDependencyOnBase = encodeClassLoader( + encodeClasspath(sharedLibrariesClassPath, baseApkName), + "dalvik.system.PathClassLoader"); + + // The result is stored in classLoaderContexts. + // Index 0 is the class loaded context for the base apk. + // Index `i` is the class loader context encoding for split `i`. + String[] classLoaderContexts = new String[/*base apk*/ 1 + splitCodePaths.length]; + classLoaderContexts[0] = baseApkContextClassLoader; + + SparseArray splitDependencies = info.splitDependencies; + + if (splitDependencies == null) { + // If there are no inter-split dependencies, populate the result with the implicit + // dependency on the base apk. + for (int i = 1; i < classLoaderContexts.length; i++) { + classLoaderContexts[i] = splitDependencyOnBase; + } + } else { + // In case of inter-split dependencies, we need to walk the dependency chain of each + // split. We do this recursively and store intermediate results in classLoaderContexts. + + // First, look at the split class loaders and cache their individual contexts (i.e. + // the class loader + the name of the split). This is an optimization to avoid + // re-computing them during the recursive call. + // The cache is stored in splitClassLoaderEncodingCache. The difference between this and + // classLoaderContexts is that the later contains the full chain of class loaders for + // a given split while splitClassLoaderEncodingCache only contains a single class loader + // encoding. + String baseCodePath = new File(info.getBaseCodePath()).getParent(); + String[] splitClassLoaderEncodingCache = new String[splitCodePaths.length]; + for (int i = 0; i < splitCodePaths.length; i++) { + File pathFile = new File(splitCodePaths[i]); + String fileName = pathFile.getName(); + splitClassLoaderEncodingCache[i] = encodeClassLoader(fileName, + "dalvik.system.PathClassLoader"); + // Sanity check that the base paths of the splits are all the same. + String basePath = pathFile.getParent(); + if (!basePath.equals(baseCodePath)) { + Slog.wtf(TAG, "Split paths have different base paths: " + basePath + " and " + + baseCodePath); + } + } + for (int i = 1; i < splitDependencies.size(); i++) { + getParentDependencies(splitDependencies.keyAt(i), splitClassLoaderEncodingCache, + splitDependencies, classLoaderContexts, splitDependencyOnBase); + } + } + + // At this point classLoaderContexts contains only the parent dependencies. + // We also need to add the class loader of the current split which should + // come first in the context. + for (int i = 1; i < classLoaderContexts.length; i++) { + String splitClassLoader = encodeClassLoader("", "dalvik.system.PathClassLoader"); + classLoaderContexts[i] = encodeClassLoaderChain( + splitClassLoader, classLoaderContexts[i]); + } + + return classLoaderContexts; + } + + /** + * Recursive method to generate the class loader context dependencies for the split with the + * given index. {@param classLoaderContexts} acts as an accumulator. Upton return + * {@code classLoaderContexts[index]} will contain the split dependency. + * During computation, the method may resolve the dependencies of other splits as it traverses + * the entire parent chain. The result will also be stored in {@param classLoaderContexts}. + * + * Note that {@code index 0} denotes the base apk and it is special handled. When the + * recursive call hits {@code index 0} the method returns {@code splitDependencyOnBase}. + * {@code classLoaderContexts[0]} is not modified in this method. + * + * @param index the index of the split (Note that index 0 denotes the base apk) + * @param splitClassLoaderEncodingCache the class loader encoding for the individual splits. + * It contains only the split class loader and not the the base. The split + * with {@code index} has its context at {@code splitClassLoaderEncodingCache[index - 1]}. + * @param splitDependencies the dependencies for all splits. Note that in this array index 0 + * is the base and splits start from index 1. + * @param classLoaderContexts the result accumulator. index 0 is the base and never set. Splits + * start at index 1. + * @param splitDependencyOnBase the encoding of the implicit split dependency on base. + */ + private static String getParentDependencies(int index, String[] splitClassLoaderEncodingCache, + SparseArray splitDependencies, String[] classLoaderContexts, + String splitDependencyOnBase) { + // If we hit the base apk return its custom dependency list which is + // sharedLibraries + base.apk + if (index == 0) { + return splitDependencyOnBase; + } + // Return the result if we've computed the splitDependencies for this index already. + if (classLoaderContexts[index] != null) { + return classLoaderContexts[index]; + } + // Get the splitDependencies for the parent of this index and append its path to it. + int parent = splitDependencies.get(index)[0]; + String parentDependencies = getParentDependencies(parent, splitClassLoaderEncodingCache, + splitDependencies, classLoaderContexts, splitDependencyOnBase); + + // The split context is: `parent context + parent dependencies context`. + String splitContext = (parent == 0) ? + parentDependencies : + encodeClassLoaderChain(splitClassLoaderEncodingCache[parent - 1], parentDependencies); + classLoaderContexts[index] = splitContext; + return splitContext; + } + + /** + * Encodes the shared libraries classpathElements in a format accepted by dexopt. + * NOTE: Keep this in sync with the dexopt expectations! Right now that is + * a list separated by ':'. + */ + private static String encodeClasspath(String[] classpathElements) { + if (classpathElements == null || classpathElements.length == 0) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (String element : classpathElements) { + if (sb.length() != 0) { + sb.append(":"); + } + sb.append(element); + } + return sb.toString(); + } + + /** + * Adds an element to the encoding of an existing classpath. + * {@see PackageDexOptimizer.encodeClasspath(String[])} + */ + private static String encodeClasspath(String classpath, String newElement) { + return classpath.isEmpty() ? newElement : (classpath + ":" + newElement); + } + + /** + * Encodes a single class loader dependency starting from {@param path} and + * {@param classLoaderName}. + * NOTE: Keep this in sync with the dexopt expectations! Right now that is either "PCL[path]" + * for a PathClassLoader or "DLC[path]" for a DelegateLastClassLoader. + */ + private static String encodeClassLoader(String classpath, String classLoaderName) { + String classLoaderDexoptEncoding = classLoaderName; + if ("dalvik.system.PathClassLoader".equals(classLoaderName)) { + classLoaderDexoptEncoding = "PCL"; + } else { + Slog.wtf(TAG, "Unsupported classLoaderName: " + classLoaderName); + } + return classLoaderDexoptEncoding + "[" + classpath + "]"; + } + + /** + * Links to dependencies together in a format accepted by dexopt. + * NOTE: Keep this in sync with the dexopt expectations! Right now that is a list of split + * dependencies {@see encodeClassLoader} separated by ';'. + */ + private static String encodeClassLoaderChain(String cl1, String cl2) { + return cl1.isEmpty() ? cl2 : (cl1 + ";" + cl2); + } +} diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java new file mode 100644 index 0000000000000..f1f176789148c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.server.pm.dex; + +import static org.junit.Assert.assertEquals; + +import android.content.pm.ApplicationInfo; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.SparseArray; + +import dalvik.system.DelegateLastClassLoader; +import dalvik.system.PathClassLoader; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DexoptUtilsTest { + private static final String PATH_CLASS_LOADER_NAME = PathClassLoader.class.getName(); + private static final String DELEGATE_LAST_CLASS_LOADER_NAME = + DelegateLastClassLoader.class.getName(); + + private ApplicationInfo createMockApplicationInfo(String baseClassLoader, boolean addSplits, + boolean addSplitDependencies) { + ApplicationInfo ai = new ApplicationInfo(); + String codeDir = "/data/app/mock.android.com"; + ai.setBaseCodePath(codeDir + "/base.dex"); + + if (addSplits) { + ai.setSplitCodePaths(new String[]{ + codeDir + "/base-1.dex", + codeDir + "/base-2.dex", + codeDir + "/base-3.dex", + codeDir + "/base-4.dex", + codeDir + "/base-5.dex", + codeDir + "/base-6.dex"}); + + if (addSplitDependencies) { + ai.splitDependencies = new SparseArray<>(6 + 1); + ai.splitDependencies.put(0, new int[] {-1}); // base has no dependency + ai.splitDependencies.put(1, new int[] {2}); // split 1 depends on 2 + ai.splitDependencies.put(2, new int[] {4}); // split 2 depends on 4 + ai.splitDependencies.put(3, new int[] {4}); // split 3 depends on 4 + ai.splitDependencies.put(4, new int[] {0}); // split 4 depends on base + ai.splitDependencies.put(5, new int[] {0}); // split 5 depends on base + ai.splitDependencies.put(6, new int[] {5}); // split 6 depends on 5 + } + } + return ai; + } + + @Test + public void testSplitChain() { + ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, true, true); + String[] sharedLibrary = new String[] {"a.dex", "b.dex"}; + String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary); + + assertEquals(7, contexts.length); + assertEquals("PCL[a.dex:b.dex]", contexts[0]); + assertEquals("PCL[];PCL[base-2.dex];PCL[base-4.dex];PCL[a.dex:b.dex:base.dex]", contexts[1]); + assertEquals("PCL[];PCL[base-4.dex];PCL[a.dex:b.dex:base.dex]", contexts[2]); + assertEquals("PCL[];PCL[base-4.dex];PCL[a.dex:b.dex:base.dex]", contexts[3]); + assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[4]); + assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[5]); + assertEquals("PCL[];PCL[base-5.dex];PCL[a.dex:b.dex:base.dex]", contexts[6]); + } + + @Test + public void testSplitChainNoSplitDependencies() { + ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, true, false); + String[] sharedLibrary = new String[] {"a.dex", "b.dex"}; + String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary); + + assertEquals(7, contexts.length); + assertEquals("PCL[a.dex:b.dex]", contexts[0]); + assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[1]); + assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[2]); + assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[3]); + assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[4]); + assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[5]); + assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[6]); + } + + @Test + public void testSplitChainNoSharedLibraries() { + ApplicationInfo ai = createMockApplicationInfo( + DELEGATE_LAST_CLASS_LOADER_NAME, true, true); + String[] contexts = DexoptUtils.getClassLoaderContexts(ai, null); + + assertEquals(7, contexts.length); + assertEquals("PCL[]", contexts[0]); + assertEquals("PCL[];PCL[base-2.dex];PCL[base-4.dex];PCL[base.dex]", contexts[1]); + assertEquals("PCL[];PCL[base-4.dex];PCL[base.dex]", contexts[2]); + assertEquals("PCL[];PCL[base-4.dex];PCL[base.dex]", contexts[3]); + assertEquals("PCL[];PCL[base.dex]", contexts[4]); + assertEquals("PCL[];PCL[base.dex]", contexts[5]); + assertEquals("PCL[];PCL[base-5.dex];PCL[base.dex]", contexts[6]); + } + + @Test + public void testSplitChainWithNullPrimaryClassLoader() { + // A null classLoaderName should mean PathClassLoader. + ApplicationInfo ai = createMockApplicationInfo(null, true, true); + String[] sharedLibrary = new String[] {"a.dex", "b.dex"}; + String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary); + + assertEquals(7, contexts.length); + assertEquals("PCL[a.dex:b.dex]", contexts[0]); + assertEquals("PCL[];PCL[base-2.dex];PCL[base-4.dex];PCL[a.dex:b.dex:base.dex]", contexts[1]); + assertEquals("PCL[];PCL[base-4.dex];PCL[a.dex:b.dex:base.dex]", contexts[2]); + assertEquals("PCL[];PCL[base-4.dex];PCL[a.dex:b.dex:base.dex]", contexts[3]); + assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[4]); + assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[5]); + assertEquals("PCL[];PCL[base-5.dex];PCL[a.dex:b.dex:base.dex]", contexts[6]); + } + + @Test + public void tesNoSplits() { + ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, false, false); + String[] sharedLibrary = new String[] {"a.dex", "b.dex"}; + String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary); + + assertEquals(1, contexts.length); + assertEquals("PCL[a.dex:b.dex]", contexts[0]); + } + + @Test + public void tesNoSplitsNullClassLoaderName() { + ApplicationInfo ai = createMockApplicationInfo(null, false, false); + String[] sharedLibrary = new String[] {"a.dex", "b.dex"}; + String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary); + + assertEquals(1, contexts.length); + assertEquals("PCL[a.dex:b.dex]", contexts[0]); + } + + @Test + public void tesNoSplitDelegateLast() { + ApplicationInfo ai = createMockApplicationInfo( + DELEGATE_LAST_CLASS_LOADER_NAME, false, false); + String[] sharedLibrary = new String[] {"a.dex", "b.dex"}; + String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary); + + assertEquals(1, contexts.length); + assertEquals("PCL[a.dex:b.dex]", contexts[0]); + } + + @Test + public void tesNoSplitsNoSharedLibraries() { + ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, false, false); + String[] contexts = DexoptUtils.getClassLoaderContexts(ai, null); + + assertEquals(1, contexts.length); + assertEquals("PCL[]", contexts[0]); + } + + @Test + public void tesNoSplitDelegateLastNoSharedLibraries() { + ApplicationInfo ai = createMockApplicationInfo( + DELEGATE_LAST_CLASS_LOADER_NAME, false, false); + String[] contexts = DexoptUtils.getClassLoaderContexts(ai, null); + + assertEquals(1, contexts.length); + assertEquals("PCL[]", contexts[0]); + } +}