插件化系列之解决资源加载异常的问题

avatar

背景

  • 目前我们国内的游戏 SDK 采用了插件化的技术,优点是 SDK 可以通过热更新来完成自更新,缺点是会遇到各种各样奇奇怪怪的问题,最近就我个人遇到的一些插件化问题来给大家做一次分享,主要分为两个部分:

    • 排查和解决资源加载不到导致的报错

    • 排查和解决 so 库加载不到导致的报错

  • 在正式进入主题前,我们需要简单普及一波插件化的小知识

    • 何为插件化:插件化就是将应用的内容进行拆分,分为了宿主和插件两个概念,通俗点讲,宿主部分就是代码直接打入到 classex.dex 文件,而插件部分是将代码打成一个 apk,然后在应用运行的时候进行动态加载。

    • 插件化的应用场景:

      • 缩减 apk 包体:随着业务的高速发展,应用的功能也会随着迭代会变得更加丰富,同时也会导致一个问题,就是我们的 apk 包体会变得很大,下载的等待时间会被拉长,这样会导致下载的转化量变少,这个时候如果使用插件化的技术,那么可以将一些不常用的功能打入到插件 apk 中,当用户使用到这些功能时,再从服务器下载并加载到应用中来,这样既能保证在功能不变的前提下,又能完成 apk 包体的缩减。

      • apk 功能热更新:从最近几年来看,目前用户更新应用的欲望比较低,这样会导致我们开发完功能,但是上线之后并没有多少人使用,短期内无法创造大的收益,在这种情况下,我们可以使用插件化的技术,将一些必要的功能列入到宿主中来(启动就会用到的类,例如 Application,LaunchActivity),而将一些非必要性的功能列入到插件中来,这个时候插件 apk 是可以随时更新的,不需要用户点更新和安装,我们只需要通过服务器下发最新版的插件 apk 即可完成更新,这样就能用户无感知的情况下完成功能的更新。

    • 插件化的实现原理:

      • 插件中的类如何加载:通过自定义一个 ClassLoader 类,并重写 loadClass 方法,当有类加载请求时,优先从插件的 apk 中找,找不到再从宿主 apk 中找,最后重写 Context 类中的 getClassLoader 方法,换成我们的自定义的 ClassLoader 对象。

      • 插件中的资源如何加载:通过反射调用 AssetManager 类中的 addAssetPath 方法,将插件的 apk 加载进去,然后创建一个自定义的 Resources 类,当有资源加载请求时,优先从插件的 apk 找,找不到再从宿主 apk 中找,最后重写 Context 类中的 getResources 方法,换成我们的自定义的 Resources 对象。

  • 好了,接下来让我们正式进入主题吧。

资源加载报错问题

  • 近期 Unity 开发人员(简称 CP)给我们反馈了一个问题,说是调用我们 SDK 登录的时候出现了崩溃
Process: com.xxx.xxx, PID: 24617
android.view.InflateException: Binary XML file line #4: Binary XML file line #4: Error inflating class <unknown>
Caused by: android.view.InflateException: Binary XML file line #4: Error inflating class <unknown>
Caused by: java.lang.reflect.InvocationTargetException
    at java.lang.reflect.Constructor.newInstance0(Native Method)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
    at android.view.LayoutInflater.createView(LayoutInflater.java:647)
    at com.android.internal.policy.PhoneLayoutInflater.onCreateView(PhoneLayoutInflater.java:58)
    at android.view.LayoutInflater.onCreateView(LayoutInflater.java:720)
    at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:788)
    at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:730)
    at android.view.LayoutInflater.rInflate(LayoutInflater.java:863)
    at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:824)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:515)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:374)
    at xxx.xxx.xxx.LoginView360.initView(LoginView360.java:78)
    at xxx.xxx.xxx.LoginView360.onAttachedToWindow(LoginView360.java:70)
    at android.view.View.dispatchAttachedToWindow(View.java:18347)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3397)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3404)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3404)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3404)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3404)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3404)
    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1761)
    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1460)
    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7183)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:949)
    at android.view.Choreographer.doCallbacks(Choreographer.java:761)
    at android.view.Choreographer.doFrame(Choreographer.java:696)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:935)
    at android.os.Handler.handleCallback(Handler.java:873)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:193)
    at android.app.ActivityThread.main(ActivityThread.java:6718)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
 Caused by: android.content.res.Resources$NotFoundException: Drawable (missing name) with resource ID #0x7f560131
 Caused by: android.content.res.Resources$NotFoundException: Unable to find resource ID #0x7f560131
    at android.content.res.ResourcesImpl.getResourceName(ResourcesImpl.java:255)
    at android.content.res.ResourcesImpl.loadDrawableForCookie(ResourcesImpl.java:785)
    at android.content.res.ResourcesImpl.loadDrawable(ResourcesImpl.java:631)
    at android.content.res.Resources.loadDrawable(Resources.java:897)
    at android.content.res.TypedArray.getDrawableForDensity(TypedArray.java:955)
    at android.content.res.TypedArray.getDrawable(TypedArray.java:930)
    at android.view.View.<init>(View.java:5010)
    at android.view.ViewGroup.<init>(ViewGroup.java:659)
    at android.widget.RelativeLayout.<init>(RelativeLayout.java:248)
    at android.widget.RelativeLayout.<init>(RelativeLayout.java:244)
    at android.widget.RelativeLayout.<init>(RelativeLayout.java:240)
    at java.lang.reflect.Constructor.newInstance0(Native Method)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
    at android.view.LayoutInflater.createView(LayoutInflater.java:647)
  • 我看到这个问题第一的反应是,会不会资源没有打到插件里面去?后面对插件 apk 进行了反编译,发现并没有这个问题

  • 那会不会获取资源时候用的就是宿主但就是没有用到插件的?让我们 debug 一下

  • 从这张截图上面,我们得到一个信息,AssetManager 中并没有插件的 apk,正常情况下 AssetManager 应该有三个 apk,分别是系统的 apk、宿主的 apk、插件的 apk,这里唯独少了插件的 apk,那么会不会是插件加载失败了呢?

  • 此时我的脑海中突然有一个大胆的想法,现在让我们试一下

  • 咦?咋这样就可以获取 Drawable 资源?那更加证明了插件是加载成功的,所以可能是插件加载失败原因可以排除了。

  • 为什么插件加载成功了,但是最终获取在获取插件资源的时候,为什么刚刚在 ResourcesImpl.getResourceName(int resid) 方法就没有看到 AssetManager 对象中有出现这个插件的 apk 呢?

Caused by: android.content.res.Resources$NotFoundException: Unable to find resource ID #0x7f560131
        at android.content.res.ResourcesImpl.getResourceName(ResourcesImpl.java:255)
        at android.content.res.ResourcesImpl.loadDrawableForCookie(ResourcesImpl.java:785)
        at android.content.res.ResourcesImpl.loadDrawable(ResourcesImpl.java:631)
        at android.content.res.Resources.loadDrawable(Resources.java:897)
        at android.content.res.TypedArray.getDrawableForDensity(TypedArray.java:955)
        at android.content.res.TypedArray.getDrawable(TypedArray.java:930)
        at android.view.View.<init>(View.java:5010)
  • 让我们先看一下堆栈所对应的源码实现是什么样的?

  • 看完了源码,我们基本可以捋出来一个完整的流程了:

    1. View 调用了 TypedArray.getDrawable

    2. TypedArray 再调用了Resources.loadDrawable 

    3. Resources 再去调用了 ResourcesImpl.getResourceName

  • 那么问题来了,TypedArray 中的 Resources 对象又是怎么赋值进去的?这个得看一下 TypedArray 对象是怎么创建的?

  • 我们到这里暂停一下,先捋一下 Activity 到 Context 的继承关系

Activity extends ContextThemeWrapper extends ContextWrapper extends Context

  • ContextWrapper getTheme 方法只是做了静态代理,可以先 pass 掉,再看一下 ContextThemeWrapper 类的 getTheme 方法实现

  • 咦?等等,我好像发现了什么东西? 先让我们试验一下

  • 抛异常是符合预期的,但是刚刚明明试过 getResources 方法是可以的,现在让我们再试验一下

  • 这样却可以?让我们看看 getResources 返回的 Resources 对象是什么?

  • 返回的是插件的 Resources 对象,所以没问题是正常的,所以应该是 mTheme 的问题了,我们先看一下 getTheme 方法的源码实现

  • ContextThemeWrapper.getTheme 源码的实现还是比较简单的,就是做了一下 mTheme 字段的缓存。等一下,缓存?是不是这个导致的呢?

  • 我忽然又有一个大胆的想法,把缓存清掉再试一下?话不多说,直接上手

  • 这个时候 mTheme 中的 AssetManager 就有了插件的 apk 路径,同时运行也正常了,所以问题的源头就是它没有错了。

  • 但是问题来了,为什么在我们的 Demo 或者其他游戏没有出现,偏偏这个游戏接我们的 SDK 就出现了,莫非?

public class EvtActivity extends NativeActivity {

    ......

    @Override

    public Resources getResources() {
        Resources resources;
        return (EvtHelper.getPSDK() == null || (resources = EvtHelper.getPSDK().getResources(super.getResources())) == null) ? super.getResources() : resources;
    }

    ......

}
  • 在这里,我们可以看到游戏方会判断 EvtHelper.getPSDK() 不为空才会调用我们 SDK 的方法,而 EvtHelper.getPSDK() 获取的是 sPlatformSDK 字段,那么这个字段是什么时候赋值的?

  • 我们可以看到是在 EvtHelper.preInitPlatformSDK 方法赋值的,那么这个方法又被谁调用了呢?让我们接着往下看

  • 我们可以看到是在 Activity.onCreate 方法调用的,那么这样写是否有问题呢?具体可分为两种情况:

    1. 假设 Activity.getTheme 有在 Activity.onCreate 之前调用:那么就会导致调用 ContextThemeWrapper.getTheme 的时候,是根据系统的 Resources 来给 mTheme 变量赋值,而不是使用插件的 Resources 来赋值,下次再调用 getTheme 方法时,由于 mTheme 字段之前赋值了,所以会复用之前的值,然后返回回去,间接导致调用了 getTheme 方法每次都是返回第一次初始化的那个对象。

    2. 假设 Activity.getTheme 没有在 Activity.onCreate 之前调用:不会存在 mTheme 字段缓存的问题,所以不会有问题。 

  • 上面就是我们的一些设想,但是实践出真理,让我们试一试,看看到底是哪个先走?

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }


    @Override
    public Resources.Theme getTheme() {
        return super.getTheme();
    }
}

  • 我们可以看到,是先走了 super.onCreate 方法,再走了 getTheme 方法,所以是有问题的,符合刚刚的猜想,现在让我们再验证一下这个猜想

  • 我们在 ContextThemeWrapper 类中,将 mTheme 字段赋值为空

  • 这样 mTheme 就从有值变成了空值,这样会重新进行初始化

  • 对比之前的,我们可以看到这里的 AssetManager 对象有了插件 apk,很好地证明了就是这个 mTheme 字段缓存引发的问题,那么我们该如何解决这一问题呢?

  • 追溯问题,根本原因还是因为 getResources 方法第一次调用的时候还是并非用的插件的 Resources 对象,所以才会间接导致 mTheme 字段赋值的时候用的是错误的 Resources (非插件的)对象进行初始化,由于做了缓存,所以 mTheme 只会赋值一次。

  • 解决方式思路大致分为两种:

    1. 提醒 CP 去除 EvtHelper 类封装,改成直接调用 SQwanCore 类

    2. 将初始化时机挪动到 attachBaseContext 方法中,提前初始化 EvtHelper 类(即调用 EvtHelper.preInitPlatformSDK)

  • 最终经过综合考虑,我们采用了第二种方案,这个资源报错问题就不会再出现了

未完待续,下一篇:【插件化系列之解决 so 加载异常的问题