【Android】插件化系列之加载异常问题如何解决?

背景

目前我们国内的游戏 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)

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

解决完资源加载报错的问题后,Unity 开发人员(简称 CP)又给我们反馈了另外一个问题

Process: xxx.xxx, PID: 21699
    java.lang.RuntimeException: Unable to start activity ComponentInfo{xxx.xxx.MainActivityFullScreen}: java.lang.IllegalArgumentException: Unable to find native library xgame using classloader: com.plugin.core.loader.ApkClassLoader[DexPathList[[zip file "/data/user/0/xxx.xxx.xxx/app_plugin/sq_plugin_xxxxxx.apk"],nativeLibraryDirectories=[/system/lib64]]]
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3177)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3314)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:113)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:71)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2043)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:224)
        at android.app.ActivityThread.main(ActivityThread.java:7096)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:604)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:928)
     Caused by: java.lang.IllegalArgumentException: Unable to find native library xgame using classloader: com.plugin.core.loader.ApkClassLoader[DexPathList[[zip file "/data/user/0/xxx.xxx.xxx/app_plugin/sq_plugin_xxxxxx.apk"],nativeLibraryDirectories=[/system/lib64]]]
        at android.app.NativeActivity.onCreate(NativeActivity.java:161)
        at org.evt.lib.EvtActivity.onCreate(EvtActivity.java:18)
        at android.app.Activity.performCreate(Activity.java:7271)
        at android.app.Activity.performCreate(Activity.java:7262)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3157)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3314) 
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) 
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:113) 
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:71) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2043) 
        at android.os.Handler.dispatchMessage(Handler.java:106) 
        at android.os.Looper.loop(Looper.java:224) 
        at android.app.ActivityThread.main(ActivityThread.java:7096) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:604) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:928)

问题筛选过程

这个游戏采用的是 android.app.NativeActivity 类来加载 so 库,可以看到它是 getClassLoader (即获取到的是插件的 ApkClassLoader 对象), 然后进行 findLibrary 的操作,但是我们 ApkClassLoader 没有重写这个方法,即表现为只会查找插件中的 so 库,如果没有找到就报错。

我这边又去下载了其他游戏的安装包,发现里面的 so 库是可以正常加载的,原因是它加载 so 库的时候是用了另外一种方式,System.loadLibrary(Ps:最常见加载 so 库的就是用这种方式),和前面的游戏采用的方式不同。


于是我写了一段测试代码,直接调用 System.loadLibrary 进行 debug,实践证明并没有问题,因为它的 ClassLoader 和宿主的 ClassLoader 对象是同一个,自然是可以找得到。



问题结论

这个游戏加载的 so 库所用到的 ClassLoader 是插件的 ApkClassLoader,而插件的 ApkClassLoader 并没有重写 findLibrary 方法的逻辑,所以就会导致插件的 ApkClassLoader 找不到宿主的 so 库。

修复方案

重写 ApkClassLoader.findLibrary 方法,优先去找插件的 so 库,找不到就去找宿主的 so 库。

public class ApkClassLoader extends DexClassLoader {

    private ClassLoader mGrandParent;
    private Method mFindLibraryMethod;

    public ApkClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, librarySearchPath, parent);
        mGrandParent = parent;
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        ......
        Class<?> clazz = findLoadedClass(className);
        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }

            if (clazz == null) {
                try {
                    clazz = mGrandParent.loadClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

        return clazz;
    }

    // 加入以下代码
    @Override
    public String findLibrary(String name) {
        String path = super.findLibrary(name);
        if (path != null) {
            return path;
        }

        if (mGrandParent instanceof BaseDexClassLoader) {
            return ((BaseDexClassLoader) mGrandParent).findLibrary(name);
        }

        try {
            if (mFindLibraryMethod == null) {
                mFindLibraryMethod = ClassLoader.class.getDeclaredMethod("findLibrary", String.class);
                mFindLibraryMethod.setAccessible(true);
            }
            // 如果插件获取不到,则交由父加载器进行获取
            return (String) mFindLibraryMethod.invoke(mGrandParent, name);
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
            return null;
        }
    }
}

加上之后问题被解决,非常 nice

完结,撒花 ✿✿ヽ(°▽°)ノ✿

作者:37手游安卓团队
链接:https://juejin.cn/post/7124519307651842078

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

一、架构师筑基必备技能

1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……

在这里插入图片描述

二、Android百大框架源码解析

1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程

在这里插入图片描述

三、Android性能优化实战解析

  • 腾讯Bugly:对字符串匹配算法的一点理解
  • 爱奇艺:安卓APP崩溃捕获方案——xCrash
  • 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
  • 百度APP技术:Android H5首屏优化实践
  • 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
  • 携程:从智行 Android 项目看组件化架构实践
  • 网易新闻构建优化:如何让你的构建速度“势如闪电”?

在这里插入图片描述

四、高级kotlin强化实战

1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始

  • Kotlin 写 Gradle 脚本是一种什么体验?

  • Kotlin 编程的三重境界

  • Kotlin 高阶函数

  • Kotlin 泛型

  • Kotlin 扩展

  • Kotlin 委托

  • 协程“不为人知”的调试技巧

  • 图解协程:suspend

在这里插入图片描述

五、Android高级UI开源框架进阶解密

1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
在这里插入图片描述

六、NDK模块开发

1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习

在这里插入图片描述

七、Flutter技术进阶

1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)

在这里插入图片描述

八、微信小程序开发

1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……

在这里插入图片描述

全套视频资料:

一、面试合集
在这里插入图片描述
二、源码解析合集

在这里插入图片描述
三、开源框架合集

在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值