What can you learn from reading the passage?

  • WebView loading process
  • The loading process of WebView resources
  • Resource The process of loading resources
  • How AssetManager is built

background

Zapee, the company’s app based on Admob, tapped into intersets on cold launch and found that only 20+% of the ads were displayed for 7 seconds. In order to further improve the display rate of open screen ads, Admob initialized and loaded open screen ads from serial to parallel, and the display rate of ads was increased to 30+%. In the later versions, some Mi phones began to open the APP card in the welcome interface.

What happened (error message analysis)

Extract the key parts of the ANR log by exporting the error log with adb bugreport {output} command

. . "main" prio=5 tid=1 Blocked | group="main" sCount=1 dsCount=0 flags=1 obj=0x7a02c410 self=0x7398414c00 | sysTid=18522 nice=-10 cgrp=default sched=0/0 handle=0x741deb8548 | state=S schedstat=( 208328084 23627658 543 ) utm=16 stm=4 core=7 HZ=100 | stack=0x7fd9c57000-0x7fd9c59000 stackSize=8MB | held mutexes= at com.google.android.gms.internal.ads.zzbgm.zza(:2) - waiting to lock <0x0901fb7f> (a java.lang.Class<com.google.android.gms.internal.ads.zzbgm>) held by thread 35 at com.google.android.gms.ads.internal.ClientApi.zzb(:8) at com.google.android.gms.internal.ads.zzwh.zza(:12) at com.google.android.gms.internal.ads.zzwn.zzpy(:25) at com.google.android.gms.internal.ads.zzwn.zzd(:66) at com.google.android.gms.internal.ads.zzze.zza(:39) at com.google.android.gms.ads.InterstitialAd.loadAd(:9) at o.ckh$a.run(:53) at o.ckm$a.run(:13) at android.os.Handler.handleCallback(Handler.java:873) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:201) at android.app.ActivityThread.main(ActivityThread.java:6806) at java.lang.reflect.Method.invoke(Native method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873) ... . "RxIoScheduler-3" daemon prio=5 tid=35 Waiting | group="main" sCount=1 dsCount=0 flags=1 obj=0x13d040a8 self=0x737bccc800 | sysTid=18593 nice=0 cgrp=default sched=0/0 handle=0x737a3224f0 | state=S schedstat=( 62125674 47960004 273 ) utm=4 stm=2 core=7 HZ=100 | stack=0x737a21f000-0x737a221000 stackSize=1041KB | held mutexes= at java.lang.Object.wait(Native method) - waiting on <0x0979f381> (a java.lang.Object) at el.b(chromium-SystemWebViewGoogle.aab-stable-428014103:19) at el.g(chromium-SystemWebViewGoogle.aab-stable-428014103:3) - locked <0x0979f381> (a java.lang.Object) at com.android.webview.chromium.WebViewChromiumFactoryProvider.getStatics(chromium-SystemWebViewGoogle.aab-stable-428014103 :4) - locked <0x0979f381> (a java.lang.Object) at android.webkit.WebSettings.getDefaultUserAgent(WebSettings.java:1266) at com.google.android.gms.ads.internal.util.zzbv.call(:15) at com.google.android.gms.ads.internal.util.zzbu.zza(:24) at com.google.android.gms.ads.internal.util.zzbu.zza(:1) at com.google.android.gms.ads.internal.util.zzv.getDefaultUserAgent(:13) at com.google.android.gms.ads.internal.util.zzm.zzq(:90) - locked <0x07e42c26> (a java.lang.Object) at com.google.android.gms.internal.ads.zzayg.zzd(:37) at com.google.android.gms.internal.ads.zzbgm.zza(:21) - locked <0x0901fb7f> (a java.lang.Class<com.google.android.gms.internal.ads.zzbgm>) at com.google.android.gms.internal.ads.zzbgm.zzf(:10) at com.google.android.gms.ads.internal.ClientApi.zza(:44) at com.google.android.gms.internal.ads.zzwj.zza(:12) at com.google.android.gms.internal.ads.zzwn.zzpy(:25) at com.google.android.gms.internal.ads.zzwn.zzd(:66) at com.google.android.gms.internal.ads.zzzd.zzg(:168) at com.google.android.gms.internal.ads.zzzd.zza(:29) - locked <0x0cfe9067> (a java.lang.Object) at Com. Google. Android. GMS. Ads. MobileAds. The initialize (: 10) at com. Pugc. Ads. The ads $a. ˊ (202) : at the pugc. Ads. The ads $a. ˊ (123) : the at Com. Pugc. Ads. The ads $a $a. ˊ (154) : at the pugc. Ads. The ads $a $a.c all (151) : the at rx. Observables. UnsafeSubscribe (10256) : the at rx.internal.operators.OperatorSubscribeOn$SubscribeOnSubscriber.call(:100) at rx.internal.schedulers.CachedThreadScheduler$EventLoopWorker$1.call(:230) at rx.internal.schedulers.ScheduledAction.run(:55) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:458) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) at java.lang.Thread.run(Thread.java:764)Copy the code

The log shows that the main thread is waiting for the lock < 0x0901FB7F >, which is held by the thread tid=35. The main thread is loading the interstitials, and the thread is initializing the Admob, which means that loading the ads and initializing the AD parallel processing will trigger a deadlock. Typically, a deadlock is caused by two threads holding the locks each other needs, but the main thread does not hold any locks other than waiting for the lock < 0x0901FB7F >, which is obviously not common deadlock behavior.

How it happened (source code analysis)

Why WebSettings. GetDefaultUserAgent () will cause the thread is in a state of cannot be awakened? This answer, want to look for in the source code. The call process involves the loading and initialization of the WebView kernel. Starting from Android 7, WebView is no longer a built-in application, but a separate app (you can see the WebView option in the developer option). The loading process also becomes complicated. 1. WebViewFactory. GetProvider () (WebView to load the kernel); 2. WebViewFactoryProvider. GetStatics () initialization (WebView kernel)

👇 WebViewSettings. GetDefaultUserAgent ()

public static String getDefaultUserAgent(Context context) {
    return WebViewFactory.getProvider().getStatics().getDefaultUserAgent(context);
}
Copy the code

Description of main categories:

WebViewFactory: used to create a WebViewFactoryProvider. WebView used the realization of the main object classes are created by it, including the realization of the WebView WebViewFactoryProvider. The Statics: Because WebView and implementation class are implemented WebViewProvider interface, some static methods used by WebView cannot be put into the interface, so they are implemented by this class

Load the WebView kernel

Here is the sequence diagram for loading the kernel:

+---------------------+ +-------------+ +-------------------+ +---------------------------------+ | getUserDefaultAgent | | getProvider | | getProviderClass | | getWebViewContextAndSetProvider | +---------------------+ +-------------+ +-------------------+ +---------------------------------+ | | | | | | | | |------------------------------>| | | | | | | | | | | | |---------------------------------------->| | | | | | | | | | | | |--------------------------->| | | | | | | |  Context | | | |<---------------------------| | | | | | | Class<WebViewFactoryProvider> | | | |<----------------------------------------| | | | | | | WebViewFactoryProvider | | | |<------------------------------| |  | | | | |Copy the code

Main source:

👇 WebViewFactoryProvider. GetProvider ()

static WebViewFactoryProvider getProvider(a) {
    synchronized (sProviderLock) {
        // For now the main purpose of this function (and the factory abstraction) is to keep
        // us honest and minimize usage of WebView internals when binding the proxy.
        if(sProviderInstance ! =null) return sProviderInstance;

        // Remove some code that does not affect understanding

        Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()");
        try {
            Class<WebViewFactoryProvider> providerClass = getProviderClass();
            Method staticFactory = null;
            try {
                / / reflection WebViewChromiumFactoryProvider# create (WebViewDelegate) method
                staticFactory = providerClass.getMethod(CHROMIUM_WEBVIEW_FACTORY_METHOD, WebViewDelegate.class);
            } catch (Exception e) {
            }

            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactoryProvider invocation");
            try {
                // Create an instance of WebViewFactoryProvider implementation class
                sProviderInstance = (WebViewFactoryProvider) staticFactory.invoke(null.new WebViewDelegate());
                return sProviderInstance;
            } catch (Exception e) {
                throw new AndroidRuntimeException(e);
            } finally{ Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW); }}finally{ Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW); }}}Copy the code

👇 WebViewFacotry. GetProviderClass ()

private static Class<WebViewFactoryProvider> getProviderClass(a) {
    Context webViewContext = null;
    Application initialApplication = AppGlobals.getInitialApplication();

    try {
        Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getWebViewContextAndSetProvider()");
        try {
            // Or WebView Context
            webViewContext = getWebViewContextAndSetProvider();
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
        }

        Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()");
        try {
            // Add WebView resources to AssetManager. This step is very important and will be covered later
            initialApplication.getAssets().addAssetPathAsSharedLibrary(webViewContext.getApplicationInfo().sourceDir);
            ClassLoader clazzLoader = webViewContext.getClassLoader();

            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.loadNativeLibrary()");
            WebViewLibraryLoader.loadNativeLibrary(clazzLoader, getWebViewLibrary(sPackageInfo.applicationInfo));
            Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);

            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "Class.forName()");
            try {
                // Get the WebViewFactoryProvider implementation Class
                return getWebViewProviderClass(clazzLoader);
            } finally{ Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW); }}catch (ClassNotFoundException e) {
            throw new AndroidRuntimeException(e);
        } finally{ Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW); }}catch (MissingWebViewPackageException e) {
        throw newAndroidRuntimeException(e); }}Copy the code

👇 WebViewFactory. GetWebViewContextAndSetProvider ()

private static Context getWebViewContextAndSetProvider(a) throws MissingWebViewPackageException {
    InitialApplication is the application object of app
    Application initialApplication = AppGlobals.getInitialApplication();
    try {
        WebViewProviderResponse response = null;
        Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewUpdateService.waitForAndGetProvider()");
        try {
            Get the main WebView app information from IWebViewUpdateService, including packageInfo
            response = getUpdateService().waitForAndGetProvider();
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
        }
        if(response.status ! = LIBLOAD_SUCCESS && response.status ! = LIBLOAD_FAILED_WAITING_FOR_RELRO) {throw new MissingWebViewPackageException("Failed to load WebView provider: "
                    + getWebViewPreparationErrorReason(response.status));
        }
        // Register to be killed before fetching package info - so that we will be
        // killed if the package info goes out-of-date.
        Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "ActivityManager.addPackageDependency()");
        try {
            ActivityManager.getService().addPackageDependency(response.packageInfo.packageName);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
        }
        // 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 = pm.getPackageInfo(
                response.packageInfo.packageName,
                PackageManager.GET_SHARED_LIBRARY_FILES
                | PackageManager.MATCH_DEBUG_TRIAGED_MISSING
                // Make sure that we fetch the current provider even if its not
                // installed for the current user
                | PackageManager.MATCH_UNINSTALLED_PACKAGES
                // Fetch signatures for verification
                | PackageManager.GET_SIGNATURES
                // Get meta-data for meta data flag verification
                | PackageManager.GET_META_DATA);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
        }

        // Validate the newly fetched package info, throws MissingWebViewPackageException on
        // failure
        verifyPackageInfo(response.packageInfo, newPackageInfo);

        ApplicationInfo ai = newPackageInfo.applicationInfo;
        fixupStubApplicationInfo(ai, pm);

        Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW,
                "initialApplication.createApplicationContext");
        try {
            // Create the WebView's Application Context
            Context webViewContext = initialApplication.createApplicationContext(
                    ai,
                    Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
            sPackageInfo = newPackageInfo;
            return webViewContext;
        } finally{ Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW); }}catch (RemoteException | PackageManager.NameNotFoundException e) {
        throw new MissingWebViewPackageException("Failed to load WebView provider: "+ e); }}Copy the code

Loading process:

  1. Get the WebViewProviderResponse object via IWebViewUpdateService, which contains the PackageInfo of the WebView
  2. Use this PackageInfo to create a New PackageInfo through the PackageManager (maybe the old information is incomplete)
  3. Call it with your app’s Application objectcreateApplicationContext(newPackageInfo.applicationInfo)You can get the WebView’s Application Context
  4. So if you have the WebView’s Application Context object, you can get a lot of information, starting with the callAssetManager.addAssetPathAsSharedLibrary(Context)Load the WebView’s resources into its own AssetManager
  5. Second, and most importantly, you can get a ClassLoader object based on WebView APK, which reflects the Class. WebViewFactoryProvider is the interface, and the reflected Class is its implementation Class. Depending on the Android version, The reflected implementation classes are also different

Android 7 -> com.android.webview.chromium.WebViewChromiumFactoryProvider Android 8 -> com.android.webview.chromium.WebViewChromiumFactoryProviderForO Android 9 -> com.android.webview.chromium.WebViewChromiumFactoryProviderForP Android 10 -> com.android.webview.chromium.WebViewChromiumFactoryProviderForQ Android 11 -> com.android.webview.chromium.WebViewChromiumFactoryProviderForR

  1. Finally, reflection implements the static methods of the classcreate(WebViewDelegate)Get an instance of the WebViewFactoryProvider implementation class

Initialize the WebView kernel

Analysis WebViewFactoryProvider. GetStatics () need to pull down the Chromium source code, the whole project is very big, there are 28 g, the analysis was based on the tag: 87.0.4280.141.

👇 WebViewFactoryProvider. GetStatics ()

@Override
public Statics getStatics(a) {
    synchronized (mAwInit.getLock()) {
        SharedStatics sharedStatics = mAwInit.getStatics();
        if (mStaticsAdapter == null) {
            mStaticsAdapter = new WebViewChromiumFactoryProvider.Statics() {
                @Override
                public String findAddress(String addr) {
                    return sharedStatics.findAddress(addr);
                }

                @Override
                public String getDefaultUserAgent(Context context) {
                    return sharedStatics.getDefaultUserAgent(context);
                }
                // Delete code that does not affect understanding}; }}return mStaticsAdapter;
}
Copy the code

👇 WebViewChromiumAwInit. GetStatics ()

public SharedStatics getStatics(a) {
    synchronized (mLock) {
        if (mSharedStatics == null) {
            // TODO: Optimization potential: most these methods only need the native library
            // loaded and initialized, not the entire browser process started.
            // See also http://b/7009882
            ensureChromiumStartedLocked(true); }}return mSharedStatics;
}
Copy the code

👇 WebViewChromiumAwInit. EnsureChromiumStartedLocked (Boolean)

// This method is not private only because the downstream subclass needs to access it,
// it shouldn't be accessed from anywhere else.
/* package */ void ensureChromiumStartedLocked(boolean onMainThread) {
    assert Thread.holdsLock(mLock);

    if (mStarted) { // Early-out for the common case.
        return; } Looper looper = ! onMainThread ? Looper.myLooper() : Looper.getMainLooper(); ThreadUtils.setUiThread(looper);if (ThreadUtils.runningOnUiThread()) {
        // If it is in the main thread, call it directly
        startChromiumLocked();
        return;
    }

    // If in a child thread, send an asynchronous Handler to execute
    AwThreadUtils.postToUiThreadLooper(new Runnable() {
        @Override
        public void run(a) {
            synchronized(mLock) { startChromiumLocked(); }}});while(! mStarted) {try {                
            // Wait until startChromiumLocked finishes waking up the thread
            mLock.wait();
        } catch (InterruptedException e) {
            // Keep trying... eventually the UI thread will process the task we sent it.}}}Copy the code

Does this code give you an Epiphany? 👇

AwThreadUtils.postToUiThreadLooper(new Runnable() {
    @Override
    public void run(a) {
        synchronized(mLock) { startChromiumLocked(); }}});while(! mStarted) {try {
        mLock.wait();
    } catch (InterruptedException e) {
    }
}
Copy the code

Thread tid=35 (0x0901fb7f>) sends an asynchronous Handler to the main thread, and waits for the current thread to wake up after the Handler finishes executing. However, the main thread is waiting for the lock < 0x0901FB7F > to be released, and the sent Handler cannot be executed, causing a deadlock.

The solution

The solution is simple, the WebSettings getDefaultUserAgent () in MobileAds. The initialize () in front of the execution, avoid the lock < 0 x0901fb7f > load, initialize the WebView kernel, The load and initialization operations are performed only once. The solution has been proven on several xiaomi phones that are almost certainly available.

New problems and analysis

Bugfix after launch, the unexpected ResourceNotFoundException anomalies has been added, and probability is small

Caused by android.content.res.Resources$NotFoundException: Resource ID #0x20c0021 at android.content.res.ResourcesImpl.getValue(ResourcesImpl.java:216) at android.content.res.Resources.getInteger(Resources.java:1097) at org.chromium.ui.base.DeviceFormFactor.isTablet(DeviceFormFactor.java:2) at wm0.a(wm0.java:2) at GE.run(GE.java:3) at org.chromium.content.browser.BrowserStartupControllerImpl.g(BrowserStartupControllerImpl.java:12) at org.chromium.content.browser.BrowserStartupControllerImpl.j(BrowserStartupControllerImpl.java:9) at Cr.run(Cr.java:9) at  org.chromium.base.ThreadUtils.i(ThreadUtils.java:2) at KY3.j(KY3.java:32) at JY3.run(JY3.java:2) at android.os.Handler.handleCallback(Handler.java:789) at android.os.Handler.dispatchMessage(Handler.java:98) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6944) at java.lang.reflect.Method.invoke(Method.java) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)Copy the code

Error stack after restore:

Caused by android.content.res.Resources$NotFoundException: Resource ID #0x20c0021
       at android.content.res.ResourcesImpl.getValue(ResourcesImpl.java:216)
       at android.content.res.Resources.getInteger(Resources.java:1097)
       at org.chromium.ui.base.DeviceFormFactor.isTablet(DeviceFormFactor.java:2)
       at org.chromium.content.browser.DeviceUtilsImpl.addDeviceSpecificUserAgentSwitch()
       at org.chromium.content.browser.BrowserStartupControllerImpl.prepareToStartBrowserProcess$Runnable.run()
       at org.chromium.content.browser.BrowserStartupControllerImpl.prepareToStartBrowserProcess()
       at org.chromium.content.browser.BrowserStartupControllerImpl.startBrowserProcessesSync()
       at com.android.webview.chromium.AwBrowserProcess.start$Runnable.run()
       at org.chromium.base.ThreadUtils.runOnUiThreadBlocking()
       at com.android.webview.chromium.AwBrowserProcess.start()
       at com.android.webview.chromium.WebViewChromiumAwInit.startChromiumLocked()
       at com.android.webview.chromium.WebViewChromiumAwInit.ensureChromiumStartedLocked$Runnable.run()
       at android.os.Handler.handleCallback(Handler.java:789)
       at android.os.Handler.dispatchMessage(Handler.java:98)
       at android.os.Looper.loop(Looper.java:164)
       at android.app.ActivityThread.main(ActivityThread.java:6944)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)
Copy the code

How does resources.getInteger (int) find Resources? Why are resources disappearing?

Resource build and load Resource source code parsing

Resource loading process

Resources.getinteger (int) Call sequence diagram:

+-------+             +-----------+                              +---------------+                                         +---------------+                  
| User  |             | Resources |                              | ResourcesImpl |                                         | AssetManager  |                  
+-------+             +-----------+                              +---------------+                                         +---------------+                  
    |                       |                                            |                                                         |                          
    | getInteger(int)       |                                            |                                                         |                          
    |---------------------->|                                            |                                                         |                          
    |                       |                                            |                                                         |                          
    |                       | getValue(int, TypedValue, boolean)         |                                                         |                          
    |                       |------------------------------------------->|                                                         |                          
    |                       |                                            |                                                         |                          
    |                       |                                            | getResourceValue(int, int, TypedValue, boolean)         |                          
    |                       |                                            |-------------------------------------------------------->|                          
    |                       |                                            |                                                         |                          
    |                       |                                            |                                                         | nativeGetResourceValue() 
    |                       |                                            |                                                         |------------------------- 
    |                       |                                            |                                                         |                        | 
    |                       |                                            |                                                         |<------------------------ 
    |                       |                                            |                                                         |                          
    |                       |                                            |                                                 boolean |                          
    |                       |                                            |<--------------------------------------------------------|                          
    |                       |                                            |                                                         |                          
    |                       |                                            |                                                         |                          
    |                       |<-------------------------------------------|                                                         |                          
    |                       |                                            |                                                         |                          
    |                   int |                                            |                                                         |                          
    |<----------------------|                                            |                                                         |                          
    |                       |                                            |                                                         |     
Copy the code

NativeGetResourceValue is not likely to fail. The error may be where resources are loaded. How do Application resources and third-party resources load?

AssetManager. GetResourceValue (int, int, TypedValue, Boolean) 👇

boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue, boolean resolveRefs) {
    Preconditions.checkNotNull(outValue, "outValue");
    synchronized (this) {
        ensureValidLocked();
        final int cookie = nativeGetResourceValue(mObject, resId, (short) densityDpi, outValue, resolveRefs);
        if (cookie <= 0) {
            return false;
        }

        // Convert the changing configurations flags populated by native code.
        outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava(
                outValue.changingConfigurations);

        if (outValue.type == TypedValue.TYPE_STRING) {
            outValue.string = mApkAssets[cookie - 1].getStringFromPool(outValue.data);
        }
        return true; }}Copy the code

Resource build process

Start by figuring out how the resources of your Application are built, which will definitely involve loading. GetResources (); ContextWrapper (); ContextWrapper (); getResources(); The answer is ContextImpl, develop a ContextImpl in LoadedApk makeApplication (), the following is the sequence diagram of the process:

+-----------------+ +-----------+ +-------------+ +-------------------+ | ActivityThread | | LoadedApk | | ContextImpl |  | ResourcesManager | +-----------------+ +-----------+ +-------------+ +-------------------+ | | | | | makeApplication() | | | |------------------------>| | | | | | | | | createAppContext() | | | |-------------------------->| | | | | | | | getResources() | | | |<--------------------------| | | | | | | | getResources() | | | |--------------------------------------------->| | | | | | | | | getOrCreateResources() | | | |----------------------- | | | | | | | | |<---------------------- | | | | | | | | createResourcesImpl() | | | |---------------------- | | | | | | | | |<--------------------- | | | | | | | | createAssetManager() | | | |--------------------- | | | | | | | | |<-------------------- | | | | | | | Resources | | |<---------------------------------------------| | | | | | | Resources | | | |-------------------------->| | | | | | | |  Application | | |<----------------------------------------------------| | | | | |Copy the code

👇 LoadedApk. MakeApplication (Boolean, Instrumentation)

public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
    if(mApplication ! =null) {
        return mApplication;
    }

    Application app = null;

    String appClass = mApplicationInfo.className;
    if (forceDefaultAppClass || (appClass == null)) {
        appClass = "android.app.Application";
    }

    try {
        java.lang.ClassLoader cl = getClassLoader();
        if(! mPackageName.equals("android")) {
            initializeJavaContextClassLoader();
        }
        ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
        // Create Application and use appContext as mBase
        app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext);
        appContext.setOuterContext(app);
    } catch (Exception e) {
        if(! mActivityThread.mInstrumentation.onException(app, e)) {throw new RuntimeException(
                "Unable to instantiate application " + appClass
                + ":" + e.toString(), e);
        }
    }
    mActivityThread.mAllApplications.add(app);
    mApplication = app;
    // ...
}
Copy the code

👇 ContextImpl. CreateAppContext (ActivityThread, LoadedApk)

static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
    if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
    ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null.null.null.0.null);
    // Resources where the value is assigned
    context.setResources(packageInfo.getResources());
    return context;
}
Copy the code

👇 LoadedApk. GetResources ()

public Resources getResources(a) {
    if (mResources == null) {
        final String[] splitPaths;
        try {
            splitPaths = getSplitPaths(null);
        } catch (NameNotFoundException e) {
            // This should never fail.
            throw new AssertionError("null split not found");
        }

        mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
                getClassLoader());
    }
    return mResources;
}
Copy the code

ResourcesManager.getResources(IBinder,String,String[],String[],String[],int,Configuration,CompatibilityInfo,ClassLoader) 👇

public @Nullable Resources getResources(@Nullable IBinder activityToken,
        @Nullable String resDir,
        @Nullable String[] splitResDirs,
        @Nullable String[] overlayDirs,
        @Nullable String[] libDirs,
        int displayId,
        @Nullable Configuration overrideConfig,
        @NonNull CompatibilityInfo compatInfo,
        @Nullable ClassLoader classLoader) {
    try {
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
        final ResourcesKey key = newResourcesKey( resDir, splitResDirs, overlayDirs, libDirs, displayId, overrideConfig ! =null ? new Configuration(overrideConfig) : null.// CopycompatInfo); classLoader = classLoader ! =null ? classLoader : ClassLoader.getSystemClassLoader();
        return getOrCreateResources(activityToken, key, classLoader);
    } finally{ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); }}Copy the code

ResourcesManager. GetOrCreateResources 👇 (IBinder, ResourcesKey, this)

private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken, @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
    synchronized (this) {
        if(activityToken ! =null) {
            // Delete this part of the code
        } else {
            // Clean up any dead references so they don't pile up.
            ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);

            // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
            ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
            if(resourcesImpl ! =null) {
                return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
            }

            // We will create the ResourcesImpl object outside of holding this lock.
        }

        // Resources is the proxy class. ResourcesImpl is where the Resources are actually obtained
        ResourcesImpl resourcesImpl = createResourcesImpl(key);
        if (resourcesImpl == null) {
            return null;
        }

        // Add this ResourcesImpl to the cache.
        mResourceImpls.put(key, new WeakReference<>(resourcesImpl));

        final Resources resources;
        if(activityToken ! =null) {
            resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo);
        } else {
            resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
        }
        returnresources; }}Copy the code

👇 ResourcesManager. CreateResourcesImpl (ResourcesKey)

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
    final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
    daj.setCompatibilityInfo(key.mCompatInfo);

    / / create the AssetManager
    final AssetManager assets = createAssetManager(key);
    if (assets == null) {
        return null;
    }

    final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
    final Configuration config = generateConfig(key, dm);
    final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);

    return impl;
}
Copy the code

👇 ResourcesManager. CreateAssetManager (ResourcesKey)

protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
    final AssetManager.Builder builder = new AssetManager.Builder();

    // resDir can be null if the 'android' package is creating a new Resources object.
    // This is fine, since each AssetManager automatically loads the 'android' package
    // already.
    if(key.mResDir ! =null) {
        try {
            builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/.false /*overlay*/));
        } catch (IOException e) {
            Log.e(TAG, "failed to add asset path " + key.mResDir);
            return null; }}if(key.mSplitResDirs ! =null) {
        for (final String splitResDir : key.mSplitResDirs) {
            try {
                builder.addApkAssets(loadApkAssets(splitResDir, false /*sharedLib*/.false /*overlay*/));
            } catch (IOException e) {
                Log.e(TAG, "failed to add split asset path " + splitResDir);
                return null; }}}if(key.mOverlayDirs ! =null) {
        for (final String idmapPath : key.mOverlayDirs) {
            try {
                builder.addApkAssets(loadApkAssets(idmapPath, false /*sharedLib*/.true /*overlay*/));
            } catch (IOException e) {
                Log.w(TAG, "failed to add overlay path " + idmapPath);

                // continue.}}}if(key.mLibDirs ! =null) {
        for (final String libDir : key.mLibDirs) {
            if (libDir.endsWith(".apk")) {
                // Avoid opening files we know do not have resources,
                // like code-only .jar files.
                try {
                    builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/.false /*overlay*/));
                } catch (IOException e) {
                    Log.w(TAG, "Asset path '" + libDir +
                            "' does not exist or contains no resources.");

                    // continue.}}}}return builder.build();
}
Copy the code

Resources are divided into four categories: mResDir, mSplitResDir, mOverlayDirs and mLibDirs. What are the differences between them? Taking Snaptube App as an example, the values of the four variables are:

{
    "mResDir": "/data/app/com.snaptube.premium-zCPItxCKMtiPLZuponnKNA==/base.apk"."mLibDirs": [
        "/system/framework/org.apache.http.legacy.jar"."/data/app/com.google.android.webview-iYfutTXhDoC6vtZnRdoEeA==/base.apk"."/data/app/com.google.android.webview-iYfutTXhDoC6vtZnRdoEeA==/split_config.en.apk"."/data/app/com.google.android.webview-iYfutTXhDoC6vtZnRdoEeA==/split_config.zh.apk"."/data/app/com.google.android.trichromelibrary_432415233-dML7CdjKq4aFdFHYn4kICg==/base.apk"."/product/overlay/NavigationBarMode3Button/NavigationBarMode3ButtonOverlay.apk"."/vendor/overlay/oneplus_shape_circle/OnePlusIconShapeCircleOverlay.apk"]."mOverlayDirs": [
        "/product/overlay/NavigationBarMode3Button/NatigationBarMode3ButtonOverlay.apk"."/vendor/overlay/oneplus_shape_circle/OnePlusIconShapeCircleOverlay.apk"]."mSplitResDirs": null
}
Copy the code

MResDir obviously refers to the resource path of app, that is, the path of APK. MLibDirs refers to the resource path of the dependent third-party library, such as WebView resource mOverlayDirs should refer to the resource path of the floating component of the system. For example, the bottom navigation bar mSplitResDir does not understand…

Here, we have about the load of Resources and build understanding, and saw the WebView, resource storage locations to think back ResourcesNotFoundException unusual reason, will be in the WebView kernel at the time of initialization, MLibDirs has not been assigned yet. So why is it a probabilistic anomaly? Wasn’t the WebView kernel already loaded with its resources when it was loaded?

The first way to introduce third-party resources

With doubt, back WebViewFactory. GetProviderClass () loading place WebView resources WebViewFactory. GetProviderClass 👇 ()

initialApplication.getAssets().addAssetPathAsSharedLibrary(webViewContext.getApplicationInfo().sourceDir);
Copy the code

AssetManager. AddAssetPathAsSharedLibrary 👇 (String)

public int addAssetPathAsSharedLibrary(String path) {
    return addAssetPathInternal(path, false /*overlay*/.true /*appAsLib*/);
}
Copy the code

AssetManager. AddAssetPathInternal 👇 (String, Boolean, Boolean)

private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {
    Preconditions.checkNotNull(path, "path");
    synchronized (this) {
        ensureOpenLocked();
        final int count = mApkAssets.length;

        // See if we already have it loaded.
        for (int i = 0; i < count; i++) {
            if (mApkAssets[i].getAssetPath().equals(path)) {
                return i + 1; }}final ApkAssets assets;
        try {
            if (overlay) {
                // TODO(b/70343104): This hardcoded path will be removed once
                // addAssetPathInternal is deleted.
                final String idmapPath = "/data/resource-cache/"
                        + path.substring(1).replace('/'.The '@')
                        + "@idmap";
                assets = ApkAssets.loadOverlayFromPath(idmapPath, false /*system*/);
            } else {
                assets = ApkAssets.loadFromPath(path, false /*system*/, appAsLib); }}catch (IOException e) {
            return 0;
        }

        mApkAssets = Arrays.copyOf(mApkAssets, count + 1);
        mApkAssets[count] = assets;
        nativeSetApkAssets(mObject, mApkAssets, true);
        invalidateCachesLocked(-1);
        return count + 1; }}Copy the code

I was surprised to see that this method does not add the WebView resource path to mLibDirs, but only to the AssetManager mApkAssets array. Who added the WebView resource path to mLibDirs and when? Is it possible that the AssetManager was rebuilt so that mApkAssets lost the WebView?

The second way to introduce third-party resources

In fact, when creating a WebView instance, the WebView kernel adds resources in a different way, which saves the resource path in mLibDirs. Here is a sequence diagram of the whole process:

+---------+ +---------+ +-----------------+ +---------------------------------+ +-----------------+ +-----------------+ +-------------------+ | Outside | | WebView | | WebViewFactory | | WebViewChromiumFactoryProvider | | WebViewChromium | | WebViewDelegate | | ResourcesManager | +---------+ +---------+ +-----------------+ +---------------------------------+  +-----------------+ +-----------------+ +-------------------+ | | | | | | | | new WebView() | | | | | | |------------------->| | | | | | | | | | | | | | | ensureProviderCreated() | | | | | | |------------------------ | | | |  | | | | | | | | | | |<----------------------- | | | | | | | | | | | | | | getProvider() | | | | | | |---------------------------------------->| | | | | | | | | | | | | | WebViewChromiumFactoryProvider | | | | | | |<----------------------------------------| | | | | | | | | | | | | | createWebView() | | | | | | |-------------------------------------------------------------------->| | | | | | | | | | | | | | | new WebViewChromium() | | | | | | |------------------------------->| | | | | | | | | | | | | | | addWebViewAssetPath() | | |  | | | |------------------------------->| | | | | | | | | | | | | | | appendLibAssetForMainAssetPath() | | | | | | |------------------------------------------->| | | | | | | | | | | | | | | redirectResourcesToNewImplLocked() | | | | | | |----------------------------------- | | | | | | | | | | | | | | |<---------------------------------- | | | | | | | | | | | | | | findOrCreateResourcesImplForKeyLocked() | | | | | | |---------------------------------------- | | | | | | | | | | | | | | |<--------------------------------------- | | | | | | | | | | | | | | createResourcesImpl() | | | | | | |---------------------- | | | | | | | | | | | | | | |<--------------------- | | | | | | | | | | | | | | createAssetManager() | | | | | | |--------------------- | | | | | | | | | | | | | | |<-------------------- | | | | | | |  | | | | | | | | | | | | |<-------------------------------------------| | | | | | | | | | | | | | | | | | | |<-------------------------------| | | | | | | | | | | | | | | | | | | |<-------------------------------| | | | | | | | | | | | | | | | | | |<--------------------------------------------------------------------| | | | | | | | | | | | | | | | | | |<-------------------| | | | | | | | | | | | |Copy the code

We directly from WebViewDelegate. AddWebViewAssetPath (Context) started watching WebViewDelegate. AddWebViewAssetPath 👇 (Context)

public void addWebViewAssetPath(Context context) {
    final String[] newAssetPaths = WebViewFactory.getLoadedPackageInfo().applicationInfo.getAllApkPaths();
    final ApplicationInfo appInfo = context.getApplicationInfo();

    String[] newLibAssets = appInfo.sharedLibraryFiles;
    for (String newAssetPath : newAssetPaths) {
        // If newLibAssets does not have newAssetPath, a new array containing newAssetPath is created based on newLibAssets
        newLibAssets = ArrayUtils.appendElement(String.class, newLibAssets, newAssetPath);
    }

    if(newLibAssets ! = appInfo.sharedLibraryFiles) {// Update ApplicationInfo sharedLibraryFiles to be used as ContextImpl
        // mLibDirs
        appInfo.sharedLibraryFiles = newLibAssets;

        // Add the WebView resource path to mLibDirsResourcesManager.getInstance().appendLibAssetsForMainAssetPath(appInfo.getBaseResourcePath(), newAssetPaths); }}Copy the code

ResourcesManager. AppendLibAssetsForMainAssetPath (String, String []) 👇

public void appendLibAssetsForMainAssetPath(String assetPath, String[] libAssets) {
    synchronized (this) {
        // Save the ResourcesImpl to be updated. In this collection, ResourcesImpl is old, to be updated, and ResourcesKey is new
        final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys = new ArrayMap<>();

        final int implCount = mResourceImpls.size();
        for (int i = 0; i < implCount; i++) {
            final ResourcesKey key = mResourceImpls.keyAt(i);
            final WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
            finalResourcesImpl impl = weakImplRef ! =null ? weakImplRef.get() : null;
            // Update key.mResDir is ResourcesKey of apK path. Some resourceskeys have null mResDir
            if(impl ! =null && Objects.equals(key.mResDir, assetPath)) {
                String[] newLibAssets = key.mLibDirs;
                for (String libAsset : libAssets) {
                    newLibAssets = ArrayUtils.appendElement(String.class, newLibAssets, libAsset);
                }

                if(newLibAssets ! = key.mLibDirs) { updatedResourceKeys.put(impl,newResourcesKey( key.mResDir, key.mSplitResDirs, key.mOverlayDirs, newLibAssets, key.mDisplayId, key.mOverrideConfiguration, key.mCompatInfo)); } } } redirectResourcesToNewImplLocked(updatedResourceKeys); }}Copy the code

ResourcesKey represents the configuration of resources, which is similar to the index of the resource warehouse. Different resource configurations (screen size, screen density, and language) correspond to different ResourcesImpl. MResourceImpl Type ArrayMap

, resource configuration and resource ‘repository’ index table
,>

👇 ResourcesManager. RedirectResourcesToNewImplLocked (ArrayMap)

private void redirectResourcesToNewImplLocked(@NonNull final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys) {
    // Bail early if there is no work to do.
    if (updatedResourceKeys.isEmpty()) {
        return;
    }

    // Update any references to ResourcesImpl that require reloading.
    final int resourcesCount = mResourceReferences.size();
    for (int i = 0; i < resourcesCount; i++) {
        final WeakReference<Resources> ref = mResourceReferences.get(i);
        finalResources r = ref ! =null ? ref.get() : null;
        if(r ! =null) {
            final ResourcesKey key = updatedResourceKeys.get(r.getImpl());
            // If ResourcesImpl ResourcesKey of Resources is updated
            if(key ! =null) {
                Create a new ResourcesImpl
                final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key);
                if (impl == null) {
                    throw new Resources.NotFoundException("failed to redirect ResourcesImpl");
                }
                // Update the implementation of the Resourcesr.setImpl(impl); }}}// Update any references to ResourcesImpl that require reloading for each Activity.
    for (ActivityResources activityResources : mActivityResourceReferences.values()) {
        final int resCount = activityResources.activityResources.size();
        for (int i = 0; i < resCount; i++) {
            final WeakReference<Resources> ref = activityResources.activityResources.get(i);
            finalResources r = ref ! =null ? ref.get() : null;
            if(r ! =null) {
                final ResourcesKey key = updatedResourceKeys.get(r.getImpl());
                if(key ! =null) {
                    final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key);
                    if (impl == null) {
                        throw new Resources.NotFoundException("failed to redirect ResourcesImpl");
                    }
                    r.setImpl(impl);
                }
            }
        }
    }
}
Copy the code

👇 ResourcesManager. FindOrCreateResourcesImplForKeyLocked (ResourcesKey)

private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(@NonNull ResourcesKey key) {
    Exists from mResourceImpls, since the key is newly created, this method returns null
    ResourcesImpl impl = findResourcesImplForKeyLocked(key);
    if (impl == null) {
        impl = createResourcesImpl(key);
        if(impl ! =null) {
            mResourceImpls.put(key, newWeakReference<>(impl)); }}return impl;
}
Copy the code

👇 ResourcesManager. CreateResourcesImpl (ResourcesKey)

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
    final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
    daj.setCompatibilityInfo(key.mCompatInfo);

    // Create a new AssetManager
    final AssetManager assets = createAssetManager(key);
    if (assets == null) {
        return null;
    }

    final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
    final Configuration config = generateConfig(key, dm);
    final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);

    if (DEBUG) {
        Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
    }
    return impl;
}
Copy the code

To put it bluntly, if a WebView object is created, the object returned by application.getResources ().getimpl () is changed. Based on what I’ve learned, I have a bold guess: In WebViewFactory. GetProvider () will the WebView resources to AssetManager. AddAssetPathAsSharedLibrary added (String) After assetManager.mapkassets, before the WebViewChromiumAwInit sends a Handler to the main thread to asynchronously execute startChromiumLocked(), Somewhere in the middle place to perform a similar WebViewDelegate. AddWebViewAssetPath (Context) operation cause the AssetManager to recreate, because mLibDirs haven’t record the WebView resources, So the newly created AssetManager cannot find WebView resources.

Final solution

Based on the above assumptions, solutions have been drawn up: In WebViewFactory. GetProvider () and WebViewFactoryProvider getStatics between (), Call the WebViewDelegate. AddWebViewAssetPath (Context)

@Nullable
@SuppressLint({"PrivateApi", "DiscouragedPrivateApi"})
@SuppressWarnings({"rawtypes", "unchecked"})
private static Object getWebViewFactoryProvider(a) {
    try {
        Class clazz = Class.forName("android.webkit.WebViewFactory");
        Method method = clazz.getDeclaredMethod("getProvider");
        method.setAccessible(true);
        return method.invoke(null);
    } catch (Throwable e) {
        ProductionEnv.throwExceptForDebugging(e);
    }
    return null;
}

@SuppressLint("PrivateApi")
@SuppressWarnings({"rawtypes", "unchecked"})
private static boolean initWebComponents(Application context) {
    Object provider = getWebViewFactoryProvider();
    if (provider == null) {
        return false;
    }

    try {
        Class clazz = Class.forName("android.webkit.WebViewDelegate");
        Object instance = UnsafeAllocator.create().newInstance(clazz);
        Method method = clazz.getMethod("addWebViewAssetPath", Context.class);
        method.invoke(instance, context);
        ProductionEnv.debugLog(TAG, "addWebViewAssetPath");
    } catch (Throwable e) {
        ProductionEnv.throwExceptForDebugging(e);
    }

    try {
        Method method = provider.getClass().getMethod("getStatics");
        method.setAccessible(true);
        Object result = method.invoke(provider);
        ProductionEnv.debugLog(TAG, "getStatics: " + result);
    } catch (Throwable e) {
        ProductionEnv.throwExceptForDebugging(e);
        return false;
    }

    return true;
}
Copy the code

After repair version, ResourcesNotFoundException abnormal really disappeared! It shows that the guess is valid!

But this is not my final solution, considering the WebViewFactory. GetProvider () and WebViewFactoryProvider getStatics () internal execution is lock, with ads parallel execution of logic is meaningless, It will even block the main thread for a short period of time (loading interstitials must be placed in the main thread, in poor performance phones can be stuck for 1 second!) Instead of loading an AD after mobileads.initialize (), the AD will be processed faster and will block the main thread much less. Ironically, my initial goal was to load the interstitials ahead of time to improve the display rate, and then I encountered an ANR, a Crash, and the final solution was a rollback, doing nothing 🙂

The company widely recruit talents! High salary high welfare high growth! Android, Web front-end, Java development engineer, advertising strategy product manager, data product manager! Many other positions still have HC, waiting for you to come

Infinite benefits 】 【 1, 6 insurances, net pay accumulation fund of social security fund to pay 12%, salary to employees and CanShang confirmed 2 children, industry leading, minimum 15 pay, high performance, high return probation salary discount 3, double cease, monthly paid sick leave a day, at least 8 days annual leave each year 4, not clock in, flexibility to work, 5, standard MacBook Pro, Dell monitor, lifting table, etc. 6, free three meals, free coffee, snacks, snacks 7, after 21:50 taxi reimbursement 8, regular outing group construction, 9. Weekly sports club activities (badminton, table tennis, basketball, tennis, swimming, running) email: [email protected]