WebView 死锁(Admob 并行请求广告引发的 ANR 的解决思路)

4,317 阅读6分钟

阅读该文章你可以了解哪些知识?

  • WebView 的加载过程
  • WebView 资源的装载过程
  • Resource 加载资源的过程
  • AssetManager 的构建原理

背景

公司应用 Zapee 基于 Admob 在冷启动接入插屏广告后发现,在限定 7s 内,广告展示率仅有 20+%。为了进一步提升开屏广告的展示率,将 Admob 初始化和加载开屏广告从串行改成并行,广告展示率提升到了 30+%。在随后的版本,在部分小米手机上,开始出现打开 app 卡在欢迎界面的情况。

发生了什么事情(错误信息分析)

通过 adb bugreport {output} 命令导出错误记录,提取出 ANR 日志中关键的部分

...
...
"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.initialize(:10)
  at com.pugc.ads.Ads$a.ˊ(:202)
  at com.pugc.ads.Ads$a.ˊ(:123)
  at com.pugc.ads.Ads$a$a.ˊ(:154)
  at com.pugc.ads.Ads$a$a.call(:151)
  at rx.Observable.unsafeSubscribe(:10256)
  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)

从日志中发现主线程正在等待 锁<0x0901fb7f> ,而这个锁被 tid=35 的线程所持有,主线程正在加载插屏广告,而线程正在初始化 Admob,也就是说加载广告和广告初始化并行处理会触发死锁。一般来说,死锁是两个线程互相持有对方需要的锁造成的,但是主线程除了等待 锁<0x0901fb7f> 之外并不持有其它锁,显然这不是常见的死锁行为。

怎么发生的(源码分析)

为什么 WebSettings.getDefaultUserAgent() 会导致线程处于无法被唤醒的状态?这个答案,要从源码里寻找。调用过程涉及到 WebView 内核的加载以及初始化,从 Android 7 开始,WebView 不再是内置的程序,变成了单独的 app(你会开发者选项内看到有个 WebView 选项),加载过程也变得复杂,我们分两部分分析:1. WebViewFactory.getProvider()(加载 WebView 内核);2. WebViewFactoryProvider.getStatics()(初始化 WebView 内核)

WebViewSettings.getDefaultUserAgent()👇

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

主要类说明:

WebViewFactory:主要用于创建 WebViewFactoryProvider WebViewFactoryProvider:WebView 所用到的主要对象的实现类都是由它创建,包括 WebView 的实现 WebViewFactoryProvider.Statics:因为 WebView 以及实现类都是实现了 WebViewProvider 接口,WebView 使用的一些静态方法无法放入接口,所以都收归到这个类来实现

加载 WebView 内核

以下是加载内核的时序图:

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

主要源码:

WebViewFactoryProvider.getProvider()👇

static WebViewFactoryProvider getProvider() {
    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;

        // 删除了一些不影响理解的代码

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

            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactoryProvider invocation");
            try {
                // 创建 WebViewFactoryProvider 实现类的实例
                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);
        }
    }
}

WebViewFacotry.getProviderClass()👇

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

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

        Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()");
        try {
            // 将 WebView 的资源加入 AssetManager,这步很重要,后面会讲
            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 {
                // 获得 WebViewFactoryProvider 实现类的 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 new AndroidRuntimeException(e);
    }
}

WebViewFactory.getWebViewContextAndSetProvider()👇

private static Context getWebViewContextAndSetProvider() throws MissingWebViewPackageException {
    // initialApplication 是 app 的 application 对象
    Application initialApplication = AppGlobals.getInitialApplication();
    try {
        WebViewProviderResponse response = null;
        Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewUpdateService.waitForAndGetProvider()");
        try {
            // 从 IWebViewUpdateService 获取 WebView app 的主要信息,包括 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 {
            // 创建 WebView 的 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);
    }
}

加载过程:

  1. 通过 IWebViewUpdateService 获得 WebViewProviderResponse 对象,里面包含了 WebView 的 PackageInfo
  2. 用这个 PackageInfo 通过 PackageManager 创建 New PackageInfo(可能旧的信息不全)
  3. 再用自身 app 的 Application 对象调用 createApplicationContext(newPackageInfo.applicationInfo) 便可获得 WebView 的 Application Context
  4. 有了 WebView 的 Application Context 对象便可拿到很多信息,首先是调用 AssetManager.addAssetPathAsSharedLibrary(Context) 将 WebView 的资源加载到自身的 AssetManager 内
  5. 其次,最重要的是,可以获得基于 WebView apk 生成的 ClassLoader 对象,反射出 Class,WebViewFactoryProvider 是接口,反射出的 Class 是它的实现类,根据 Android 版本的不同,反射出的实现类也不一样

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. 最后,反射实现类的静态方法 create(WebViewDelegate) 得到 WebViewFactoryProvider 实现类的实例

初始化 WebView 内核

分析 WebViewFactoryProvider.getStatics() 需要将 Chromium 的源码拉取下来,整个工程很大,有 28G,此次分析基于 tag: 87.0.4280.141

WebViewFactoryProvider.getStatics()👇

@Override
public Statics getStatics() {
    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);
                }
                // 删除不影响理解的代码
            };
        }
    }
    return mStaticsAdapter;
}

WebViewChromiumAwInit.getStatics()👇

public SharedStatics getStatics() {
    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;
}

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()) {
        // 如果在主线程,直接调用
        startChromiumLocked();
        return;
    }

    // 如果在子线程,发个异步 Handler 执行
    AwThreadUtils.postToUiThreadLooper(new Runnable() {
        @Override
        public void run() {
            synchronized (mLock) {
                startChromiumLocked();
            }
        }
    });
    while (!mStarted) {
        try {                
            // 一直等待,直到 startChromiumLocked 执行完唤醒线程
            mLock.wait();
        } catch (InterruptedException e) {
            // Keep trying... eventually the UI thread will process the task we sent it.
        }
    }
}

看到下面这段代码你是不是恍然大悟了?👇

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

原来死锁的原因是 「线程 tid=35」 在持有 锁<0x0901fb7f> 的情况下,向主线程发送了个异步执行的 Handler,并 wait 当前线程等待 Handler 执行完被唤醒,然而主线程此时正在等待 锁<0x0901fb7f> 释放,发送的 Handler 无法被执行,从而造成了死锁。

解决办法

解决办法也很简单,将 WebSettings.getDefaultUserAgent()MobileAds.initialize() 前面执行,避免在 锁<0x0901fb7f> 内加载、初始化 WebView 内核,加载和初始化操作只会执行一次。该方案在几台几乎必现的小米手机上得到了验证。

新的问题以及分析

bugfix 上线后,意外的新增了 ResourceNotFoundException 异常,而且概率还不小

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)

还原后的错误堆栈:

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)

从堆栈里看不出原因,解决问题的思路回归问题的本质:Resources.getInteger(int) 是如何寻找资源的?为什么资源消失了?

Resource 构建以及加载资源源码解析

加载资源过程

Resources.getInteger(int) 调用时序图:

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

从 AssetManager 内也看不问题,nativeGetResourceValue 不太可能出错,出错的可能是装载资源的地方,那 Application 的资源以及第三方的资源是如何装载的呢?

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;
    }
}

资源构建过程

首先先搞清楚 Application 的资源是如何构建的,构建的过程肯定涉及到了装载。我们知道 Activity、Application、Service 都继承至 ContextWrapper,获取 getResources() 实际上是 mBase 实现类去完成的,那这个 mBase 实现类是谁呢?答案是 ContextImplContextImpl 的创建在 LoadedApk.makeApplication() 内,以下是整个过程的时序图:

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

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);
        // 创建 Application 并将 appContext 作为 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;
    // ...
}

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 被赋值的地方
    context.setResources(packageInfo.getResources());
    return context;
}

LoadedApk.getResources()👇

public Resources getResources() {
    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;
}

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 = new ResourcesKey(
                resDir,
                splitResDirs,
                overlayDirs,
                libDirs,
                displayId,
                overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                compatInfo);
        classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
        return getOrCreateResources(activityToken, key, classLoader);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
    }
}

ResourcesManager.getOrCreateResources(IBinder,ResourcesKey,ClassLoader)👇

private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken, @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
    synchronized (this) {
        if (activityToken != null) {
            // 删除此部分代码
        } 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 相当于代理类,ResourcesImpl 才是真正获取资源的地方
        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);
        }
        return resources;
    }
}

ResourcesManager.createResourcesImpl(ResourcesKey)👇

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

    // 创建 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;
}

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();
}

可以看到,资源分为四类,分别是mResDirmSplitResDirmOverlayDirsmLibDirs,它们之间的区别是什么?以 Snaptube App 为例,四个变量的值分别为:

{
    "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
}

mResDir 很显然是指 app 的资源路径,即 apk 的路径 mLibDirs 指依赖的第三方库的资源路径,例如 WebView 资源 mOverlayDirs 应该是指系统浮层组件的资源路径,例如底部导航栏 mSplitResDir 不太理解...

到这里,我们对 Resources 的加载以及构建有了大概的了解,以及看到了 WebView 资源存放的位置,回过头来思考 ResourcesNotFoundException 异常出现的原因,会不会在 WebView 内核在初始化的时候,mLibDirs 还没有赋值呢?那为什么是概率性发生异常?在加载 WebView 内核时,不是已经装载它的资源了吗?

引入第三方资源的第一种方式

带着疑惑,重新回到 WebViewFactory.getProviderClass() 装载 WebView 资源的地方 WebViewFactory.getProviderClass()👇

initialApplication.getAssets().addAssetPathAsSharedLibrary(webViewContext.getApplicationInfo().sourceDir);

AssetManager.addAssetPathAsSharedLibrary(String)👇

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

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('/', '@')
                        + "@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;
    }
}

看到这,我惊奇的发现,这种方式并没有将 WebView 的资源路径添加进 mLibDirs,而仅仅是添加进 AssetManager 的 mApkAssets 的数组内,那 WebView 的资源路径是谁在什么时候添加进 mLibDirs 的呢?有没有可能 AssetManager 被重建了导致 mApkAssets 丢失了 WebView 的资源呢?

引入第三方资源的第二种方式

事实上,在创建 WebView 实例时,WebView 内核会用另外一种方式添加资源,这种方式会将资源路径保存在 mLibDirs,以下是整个过程的时序图:

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

我们直接从 WebViewDelegate.addWebViewAssetPath(Context) 开始看 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) {
        // 如果 newLibAssets 不存在 newAssetPath,则基于 newLibAssets 创建包含 newAssetPath 的新数组
        newLibAssets = ArrayUtils.appendElement(String.class, newLibAssets, newAssetPath);
    }

    if (newLibAssets != appInfo.sharedLibraryFiles) {
        // 更新 ApplicationInfo 的 sharedLibraryFiles,创建 ContextImpl 时,sharedLibraryFiles 将作为
        // mLibDirs
        appInfo.sharedLibraryFiles = newLibAssets;

        // 将 WebView 资源路径添加进 mLibDirs
        ResourcesManager.getInstance().appendLibAssetsForMainAssetPath(appInfo.getBaseResourcePath(), newAssetPaths);
    }
}

ResourcesManager.appendLibAssetsForMainAssetPath(String,String[])👇

public void appendLibAssetsForMainAssetPath(String assetPath, String[] libAssets) {
    synchronized (this) {
        // 保存将要更新的 ResourcesImpl,在这个集合里,ResourcesImpl 是旧的,将要更新的,ResourcesKey 则是新的
        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);
            final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
            // 仅更新 key.mResDir 是 apk 路径的 ResourcesKey,有些 ResourcesKey 的 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, new ResourcesKey(
                            key.mResDir,
                            key.mSplitResDirs,
                            key.mOverlayDirs,
                            newLibAssets,
                            key.mDisplayId,
                            key.mOverrideConfiguration,
                            key.mCompatInfo));
                }
            }
        }

        redirectResourcesToNewImplLocked(updatedResourceKeys);
    }
}

ResourcesKey 代表资源的配置,这相当于资源仓库(ResourcesImpl)的索引一样,不同的资源配置(屏幕尺寸、屏幕密度、语言)对应的 ResourcesImpl 不一样 ResourcesImpl 资源的具体实现,里面包含了 AssetManager,该类是最终获取资源的所在 mResourceImpl 类型 ArrayMap<ResourcesKey, ResourcesImpl>,资源配置与资源‘仓库’的索引表

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);
        final Resources r = ref != null ? ref.get() : null;
        if (r != null) {
            final ResourcesKey key = updatedResourceKeys.get(r.getImpl());
            // 如果  Resources 的 ResourcesImpl 的 ResourcesKey 被更新了
            if (key != null) {
                // 创建新的 ResourcesImpl
                final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key);
                if (impl == null) {
                    throw new Resources.NotFoundException("failed to redirect ResourcesImpl");
                }
                // 更新该 Resources 的实现
                r.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);
            final Resources 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);
                }
            }
        }
    }
}

ResourcesManager.findOrCreateResourcesImplForKeyLocked(ResourcesKey)👇

private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(@NonNull ResourcesKey key) {
    // 从 mResourceImpls 内存在,因为 key 是新创建的,这个方法返回 null
    ResourcesImpl impl = findResourcesImplForKeyLocked(key);
    if (impl == null) {
        impl = createResourcesImpl(key);
        if (impl != null) {
            mResourceImpls.put(key, new WeakReference<>(impl));
        }
    }
    return impl;
}

ResourcesManager.createResourcesImpl(ResourcesKey)👇

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

    // 创建新的 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;
}

直白的来说,如果创建了 WebView 对象,Application.getResources().getImpl() 返回的对象就被改变了。结合前面了解到的信息,我有个大胆的猜测:在 WebViewFactory.getProvider() 将 WebView 资源以 AssetManager.addAssetPathAsSharedLibrary(String) 的方式添加进 AssetManager.mApkAssets 之后,在 WebViewChromiumAwInit 向主线程发送了 Handler 异步执行 startChromiumLocked() 之前,中间某处地方执行了类似 WebViewDelegate.addWebViewAssetPath(Context) 的操作导致 AssetManager 被重新创建,因为 mLibDirs 还未记录 WebView 资源,所以新创建的 AssetManager 找不到 WebView 资源。

最终解决方案

基于上面的假设,拟定了解决办法:在 WebViewFactory.getProvider()WebViewFactoryProvider.getStatics() 之间,调用一下 WebViewDelegate.addWebViewAssetPath(Context)

@Nullable
@SuppressLint({"PrivateApi", "DiscouragedPrivateApi"})
@SuppressWarnings({"rawtypes", "unchecked"})
private static Object getWebViewFactoryProvider() {
    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;
}

修复后的版本,ResourcesNotFoundException 异常确实消失了!说明猜测是成立的!

但这并不是我最终的解决方案,考虑到 WebViewFactory.getProvider()WebViewFactoryProvider.getStatics() 内部执行时是有锁的,跟加载广告的逻辑并行执行没有意义,甚至还会阻塞主线程一小段时间(加载插屏广告必须放在主线程,在性能差的手机能卡顿 1 秒!),倒不如在 MobileAds.initialize() 之后去加载广告,万事俱备,之后加载广告的处理速度也会更快,阻塞主线程的时间大大减少。讽刺的是,我最初的目的是为了提前加载插屏广告以提高展示率,然后遇到了一个 ANR,一个 Crash,最终解决方案确是回滚操作,什么都不做 :-)