插件化系列之解决资源加载异常的问题
背景
-
目前我们国内的游戏 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)
-
最终经过综合考虑,我们采用了第二种方案,这个资源报错问题就不会再出现了
转载自:https://juejin.cn/post/7124303259715502110