From 0452670374ebb2a22d2dc44c030f7b43173800c0 Mon Sep 17 00:00:00 2001 From: "Torne (Richard Coles)" Date: Fri, 13 Jan 2017 14:19:39 +0000 Subject: [PATCH] Support loading a stub WebView using a donor package. Support loading a WebView package which specifies the name of a "donor" that provides missing files. This allows a preinstalled stub WebView to function by loading its code and assets from the preinstalled Monochrome implementation, as long as the versions are close enough that the manifest contents are compatible, which should be fine since preinstalled versions will match. To do this, we replace the stub's code paths in AppplicationInfo with the donor's, so that all Java and native code and resources are loaded from the donor APK at runtime instead of from the (mostly empty) stub. To get the ClassLoader with the modified path cached as if it was the regular path, we introduce a new "cacheKey" parameter in ApplicationLoaders. Bug: 21643067 Test: build "new" stub WebView upstream in chromium and test loading Change-Id: I08cc9122b1c9def3e1206974f3e0e8973cca3419 --- core/java/android/app/ApplicationLoaders.java | 33 ++++++++---- core/java/android/os/ZygoteProcess.java | 9 ++-- core/java/android/webkit/WebViewFactory.java | 52 +++++++++++++++++-- core/java/android/webkit/WebViewZygote.java | 13 ++++- .../internal/os/WebViewZygoteInit.java | 11 ++-- .../android/internal/os/ZygoteConnection.java | 6 ++- 6 files changed, 101 insertions(+), 23 deletions(-) diff --git a/core/java/android/app/ApplicationLoaders.java b/core/java/android/app/ApplicationLoaders.java index ef2db4a0d7958..2062930929a27 100644 --- a/core/java/android/app/ApplicationLoaders.java +++ b/core/java/android/app/ApplicationLoaders.java @@ -18,6 +18,7 @@ package android.app; import android.os.Build; import android.os.Trace; +import android.text.TextUtils; import android.util.ArrayMap; import com.android.internal.os.PathClassLoaderFactory; import dalvik.system.PathClassLoader; @@ -29,8 +30,16 @@ public class ApplicationLoaders { } ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled, - String librarySearchPath, String libraryPermittedPath, - ClassLoader parent) { + String librarySearchPath, String libraryPermittedPath, + ClassLoader parent) { + // For normal usage the cache key used is the same as the zip path. + return getClassLoader(zip, targetSdkVersion, isBundled, librarySearchPath, + libraryPermittedPath, parent, zip); + } + + private ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled, + String librarySearchPath, String libraryPermittedPath, + ClassLoader parent, String cacheKey) { /* * This is the parent we use if they pass "null" in. In theory * this should be the "system" class loader; in practice we @@ -50,7 +59,7 @@ public class ApplicationLoaders { * new ClassLoader for the zip archive. */ if (parent == baseParent) { - ClassLoader loader = mLoaders.get(zip); + ClassLoader loader = mLoaders.get(cacheKey); if (loader != null) { return loader; } @@ -71,7 +80,7 @@ public class ApplicationLoaders { setupVulkanLayerPath(pathClassloader, librarySearchPath); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - mLoaders.put(zip, pathClassloader); + mLoaders.put(cacheKey, pathClassloader); return pathClassloader; } @@ -87,12 +96,16 @@ public class ApplicationLoaders { * by this class. This is used in the WebView zygote, where its presence in the cache speeds up * startup and enables memory sharing. */ - public ClassLoader createAndCacheWebViewClassLoader(String packagePath, String libsPath) { - // The correct paths are calculated by WebViewZygote in the system server and passed to - // us here. We hardcode the other parameters: WebView always targets the current SDK, - // does not need to use non-public system libraries, and uses the base classloader as its - // parent to permit usage of the cache. - return getClassLoader(packagePath, Build.VERSION.SDK_INT, false, libsPath, null, null); + public ClassLoader createAndCacheWebViewClassLoader(String packagePath, String libsPath, + String cacheKey) { + // The correct paths are calculated by WebViewZygote in the system server and passed to + // us here. We hardcode the other parameters: WebView always targets the current SDK, + // does not need to use non-public system libraries, and uses the base classloader as its + // parent to permit usage of the cache. + // The cache key is passed separately to enable the stub WebView to be cached under the + // stub's APK path, when the actual package path is the donor APK. + return getClassLoader(packagePath, Build.VERSION.SDK_INT, false, libsPath, null, null, + cacheKey); } private static native void setupVulkanLayerPath(ClassLoader classLoader, String librarySearchPath); diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index b3366d883fc66..8208438dc62a8 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -487,11 +487,11 @@ public class ZygoteProcess { * Instructs the zygote to pre-load the classes and native libraries at the given paths * for the specified abi. Not all zygotes support this function. */ - public void preloadPackageForAbi(String packagePath, String libsPath, String abi) - throws ZygoteStartFailedEx, IOException { + public void preloadPackageForAbi(String packagePath, String libsPath, String cacheKey, + String abi) throws ZygoteStartFailedEx, IOException { synchronized(mLock) { ZygoteState state = openZygoteSocketIfNeeded(abi); - state.writer.write("3"); + state.writer.write("4"); state.writer.newLine(); state.writer.write("--preload-package"); @@ -503,6 +503,9 @@ public class ZygoteProcess { state.writer.write(libsPath); state.writer.newLine(); + state.writer.write(cacheKey); + state.writer.newLine(); + state.writer.flush(); } } diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index 81c2f5d5ef4c3..71db6b141ca9a 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -280,6 +280,44 @@ public final class WebViewFactory { } } + /** + * If the ApplicationInfo provided is for a stub WebView, fix up the object to include the + * required values from the donor package. If the ApplicationInfo is for a full WebView, + * leave it alone. Throws MissingWebViewPackageException if the donor is missing. + */ + private static void fixupStubApplicationInfo(ApplicationInfo ai, PackageManager pm) { + String donorPackageName = null; + if (ai.metaData != null) { + donorPackageName = ai.metaData.getString("com.android.webview.WebViewDonorPackage"); + } + if (donorPackageName != null) { + PackageInfo donorPackage; + try { + donorPackage = pm.getPackageInfo( + donorPackageName, + PackageManager.GET_SHARED_LIBRARY_FILES + | PackageManager.MATCH_DEBUG_TRIAGED_MISSING + | PackageManager.MATCH_UNINSTALLED_PACKAGES + | PackageManager.MATCH_FACTORY_ONLY); + } catch (PackageManager.NameNotFoundException e) { + throw new MissingWebViewPackageException("Failed to find donor package: " + + donorPackageName); + } + ApplicationInfo donorInfo = donorPackage.applicationInfo; + + // Replace the stub's code locations with the donor's. + ai.sourceDir = donorInfo.sourceDir; + ai.splitSourceDirs = donorInfo.splitSourceDirs; + ai.nativeLibraryDir = donorInfo.nativeLibraryDir; + ai.secondaryNativeLibraryDir = donorInfo.secondaryNativeLibraryDir; + + // Copy the donor's primary and secondary ABIs, since the stub doesn't have native code + // and so they are unset. + ai.primaryCpuAbi = donorInfo.primaryCpuAbi; + ai.secondaryCpuAbi = donorInfo.secondaryCpuAbi; + } + } + private static Context getWebViewContextAndSetProvider() { Application initialApplication = AppGlobals.getInitialApplication(); try { @@ -307,9 +345,10 @@ public final class WebViewFactory { } // Fetch package info and verify it against the chosen package PackageInfo newPackageInfo = null; + PackageManager pm = initialApplication.getPackageManager(); Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "PackageManager.getPackageInfo()"); try { - newPackageInfo = initialApplication.getPackageManager().getPackageInfo( + newPackageInfo = pm.getPackageInfo( response.packageInfo.packageName, PackageManager.GET_SHARED_LIBRARY_FILES | PackageManager.MATCH_DEBUG_TRIAGED_MISSING @@ -328,12 +367,15 @@ public final class WebViewFactory { // failure verifyPackageInfo(response.packageInfo, newPackageInfo); + ApplicationInfo ai = newPackageInfo.applicationInfo; + fixupStubApplicationInfo(ai, pm); + Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "initialApplication.createApplicationContext"); try { // Construct an app context to load the Java code into the current app. Context webViewContext = initialApplication.createApplicationContext( - newPackageInfo.applicationInfo, + ai, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); sPackageInfo = newPackageInfo; return webViewContext; @@ -449,7 +491,11 @@ public final class WebViewFactory { */ public static int onWebViewProviderChanged(PackageInfo packageInfo) { String[] nativeLibs = null; + String originalSourceDir = packageInfo.applicationInfo.sourceDir; try { + fixupStubApplicationInfo(packageInfo.applicationInfo, + AppGlobals.getInitialApplication().getPackageManager()); + nativeLibs = WebViewFactory.getWebViewNativeLibraryPaths(packageInfo); if (nativeLibs != null) { long newVmSize = 0L; @@ -498,7 +544,7 @@ public final class WebViewFactory { Log.e(LOGTAG, "error preparing webview native library", t); } - WebViewZygote.onWebViewProviderChanged(packageInfo); + WebViewZygote.onWebViewProviderChanged(packageInfo, originalSourceDir); return prepareWebViewInSystemServer(nativeLibs); } diff --git a/core/java/android/webkit/WebViewZygote.java b/core/java/android/webkit/WebViewZygote.java index f78d622500296..2123debfeb671 100644 --- a/core/java/android/webkit/WebViewZygote.java +++ b/core/java/android/webkit/WebViewZygote.java @@ -66,6 +66,13 @@ public class WebViewZygote { @GuardedBy("sLock") private static PackageInfo sPackage; + /** + * Cache key for the selected WebView package's classloader. This is set from + * #onWebViewProviderChanged(). + */ + @GuardedBy("sLock") + private static String sPackageCacheKey; + /** * Flag for whether multi-process WebView is enabled. If this is false, the zygote * will not be started. @@ -118,9 +125,10 @@ public class WebViewZygote { } } - public static void onWebViewProviderChanged(PackageInfo packageInfo) { + public static void onWebViewProviderChanged(PackageInfo packageInfo, String cacheKey) { synchronized (sLock) { sPackage = packageInfo; + sPackageCacheKey = cacheKey; // If multi-process is not enabled, then do not start the zygote service. if (!sMultiprocessEnabled) { @@ -210,7 +218,8 @@ public class WebViewZygote { TextUtils.join(File.pathSeparator, zipPaths); Log.d(LOGTAG, "Preloading package " + zip + " " + librarySearchPath); - sZygote.preloadPackageForAbi(zip, librarySearchPath, Build.SUPPORTED_ABIS[0]); + sZygote.preloadPackageForAbi(zip, librarySearchPath, sPackageCacheKey, + Build.SUPPORTED_ABIS[0]); } catch (Exception e) { Log.e(LOGTAG, "Error connecting to " + serviceName, e); sZygote = null; diff --git a/core/java/com/android/internal/os/WebViewZygoteInit.java b/core/java/com/android/internal/os/WebViewZygoteInit.java index f27c0d4df4cef..cc3f58cb0c3db 100644 --- a/core/java/com/android/internal/os/WebViewZygoteInit.java +++ b/core/java/com/android/internal/os/WebViewZygoteInit.java @@ -26,6 +26,7 @@ import android.util.Log; import android.webkit.WebViewFactory; import android.webkit.WebViewFactoryProvider; +import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -67,16 +68,20 @@ class WebViewZygoteInit { } @Override - protected boolean handlePreloadPackage(String packagePath, String libsPath) { + protected boolean handlePreloadPackage(String packagePath, String libsPath, + String cacheKey) { // Ask ApplicationLoaders to create and cache a classloader for the WebView APK so that // our children will reuse the same classloader instead of creating their own. // This enables us to preload Java and native code in the webview zygote process and // have the preloaded versions actually be used post-fork. ClassLoader loader = ApplicationLoaders.getDefault().createAndCacheWebViewClassLoader( - packagePath, libsPath); + packagePath, libsPath, cacheKey); // Add the APK to the Zygote's list of allowed files for children. - Zygote.nativeAllowFileAcrossFork(packagePath); + String[] packageList = TextUtils.split(packagePath, File.pathSeparator); + for (String packageEntry : packageList) { + Zygote.nativeAllowFileAcrossFork(packageEntry); + } // Once we have the classloader, look up the WebViewFactoryProvider implementation and // call preloadInZygote() on it to give it the opportunity to preload the native library diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index e2485e90e5372..a9bec4123fd2a 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -177,7 +177,7 @@ class ZygoteConnection { if (parsedArgs.preloadPackage != null) { return handlePreloadPackage(parsedArgs.preloadPackage, - parsedArgs.preloadPackageLibs); + parsedArgs.preloadPackageLibs, parsedArgs.preloadPackageCacheKey); } if (parsedArgs.permittedCapabilities != 0 || parsedArgs.effectiveCapabilities != 0) { @@ -314,7 +314,7 @@ class ZygoteConnection { return ZygoteInit.isPreloadComplete(); } - protected boolean handlePreloadPackage(String packagePath, String libsPath) { + protected boolean handlePreloadPackage(String packagePath, String libsPath, String cacheKey) { throw new RuntimeException("Zyogte does not support package preloading"); } @@ -428,6 +428,7 @@ class ZygoteConnection { */ String preloadPackage; String preloadPackageLibs; + String preloadPackageCacheKey; /** * Whether this is a request to start preloading the default resources and classes. @@ -599,6 +600,7 @@ class ZygoteConnection { } else if (arg.equals("--preload-package")) { preloadPackage = args[++curArg]; preloadPackageLibs = args[++curArg]; + preloadPackageCacheKey = args[++curArg]; } else if (arg.equals("--preload-default")) { preloadDefault = true; } else {