记录一次换肤插件问题排查
PS:出现的问题已经修复并发布至Github:https://github.com/LittleFogCat/skin-support
问题出现
在项目中遇到了闪退的情况。
经过排查,发现跟第三方换肤插件 Android-skin-support 有关。只要当前触发换肤,则会偶然出现问题。
在经过多番搜索之后,了解到该 bug 是由于该第三方库新版本的 AndroidX 不兼容造成的。而该作者早已停止维护,所以只能另寻他法了。
摆在面前的有这么几条路:
- 网上搜索解决方案;
- 自行排查并修改其源码;
- 使用其他换肤插件;
- 不做换肤功能;
以上几个方案,优先度依次递减。
探索解决方案
1. 降版本
在 Github 该项目的 issue 中,发现了与我们遇到的相同问题:issue470。
可以得出结论:
- 的确是该第三方库与新版本 AndroidX 不兼容造成的问题;
- 该作者停止维护;
- 可以通过强制降低 AndroidX 版本来解决问题;
那么,摆在面前最简单的方式就是降低 AndroidX 版本了。当然,这只是权益之举,弃车保帅之法。
在降低了 AndroidX 版本之后,应用的确不会闪退了。但是问题是,应用并没有换肤,也就是说这个功能并没有实现。
由于第三方库无法调试,如果要实现功能,要么调试修改其代码,要么换一个换肤插件。前者需要阅读源码,耗费时间颇多,且不一定成功,有可能竹篮打水一场空;后者成本较高,如果换新的框架,不仅需要学习成本,重新开发模块,且服务器也需要重新部署。所以决定先尝试调试、修改其源码。
2. 排查问题
闪退问题
下载源码到本地,首先排查的是闪退的问题。
由于现在已经可以调试,所以将 AndroidX 升到正常版本。
经过排查,发现是该换肤插件引用的系统资源,在新版本的 AndroidX 中已被移除,所以出现了无法找到资源的 bug,造成闪退。
于是在经过移除、更换了对应资源之后,闪退的问题便修复了。
换肤不成功问题
换肤不成功这个问题,耗费了许多时间。由于不清楚该框架原理,像这种不出现闪退的问题很难跟踪。
在跟踪了 loadSkin
方法之后,发现其并没有直接进行换肤操作,而是起了一个 AsyncTask
,加载对应的 skin 文件,并通过 setupSkin
方法将 SkinCompatResources
字段赋值。也就是说, loadSkin
方法只是加载了对应的 skin 文件,并没有执行对应资源的实时替换。这一点让我想到了 Android 中的 Animation
,其也只是对 View 中的字段进行修改,而实际上的动画效果是靠 Android 的刷新机制来实现的。
这点线索断掉之后,有些没有头绪。在翻看代码的过程中,我发现了一些类的名称:SkinCompatTextView
、SkinCompatButton
、SkinCompatEditText
……如同一道闪电划过,我想起了一些事情。
众所周知,在使用了 AppCompatActivity
的情况下,其中的 TextView
、Button
等控件都会被替换成 AppCompatTextView
、AppCompatButton
等。这是如何实现的呢?是通过 LayoutInflater.Factory2
来实现的。关于这一点,我之前也写过文章,【Android】全局自定义字体的实现。
也就是说,只需要替换掉 LayoutInflate.mFactory2
字段,即可实现偷梁换柱的效果,也就是换肤插件实现的原理。
而为什么换肤插件失效了呢?这是因为其使用的 LayoutInflater.setFactory2
方法来设置的字段,而新版本 Android 对这一块做出了一些调整,具体是什么我也忘记了,不过最终的结果就是报错。虽然它给报错 catch 住了,但也造成了换肤功能的失效。
解决方案我在 【Android】全局自定义字体的实现 中也写了,那就是通过反射替代对应方法的调用,直接修改字段的值,就不赘述了。
在经过测试之后,我懵了。居然爆出 java.lang.NoSuchFieldException: mFactory
的错误。原来新版本 Android 中,将 原来是我自己把 mFactory
和 mFactory2
字段保护起来了。我靠。又过了好一阵思索,我发现测试机的版本只有 Android 8,并不是新版本……getDeclaredField
写成了 getField
……
改好之后,替换成功了!虽然还是没有成功换肤……但是离成功又近了一步。
最后,继续排查,发现没有成功换肤的原因是设置背景图片的对象是 contentFrame,即除去 ActionBar 之后 Activity 的根布局;而众所周知,每个 Android 初学者都知道,contentFrame 的类型是 FrameLayout。然而,实际上,它的类型却是 ContentFrameLayout
!这个可能是新版本的 Android 中的实现。不管怎么说,这个原因导致了换肤插件没有做适配。自己手动添上对应类的适配,终于解决了。
完结!
后记
后来又遇到了切换 Fragment 不显示的问题,最终排查结果是因为 Fragment 的容器类型是 FragmentContainerView
,不是换肤插件支持的控件,所以其宽高变成 0 了(?)至于为什么会这样,时间太晚无力再看了。把控件类型换成 FrameLayout,问题解决。
总的来说,这玩意儿能不用就不用吧……
后记2
我把原代码修改了一下,修复了一些问题,并发布到了Github,有需要的可以自取:https://github.com/LittleFogCat/skin-support