前言
RemoteViews顾名思义就是远程的View,它并不是一个真正的View,而是一个View的描述结构,用来将本进程的View在其他进程中显示,一般使用场景有两种:通知栏和桌面小部件。
最近就遇到了应用使用RemoteViews不当,导致莫名其妙的玄幻问题,最后还得从系统源码的角度,才定位到真正原因。
起因
在一次发版过程中,有测试反馈,某个测试场景下,Launcher会崩溃。崩溃的直接原因是报展开某进程提供的RemoteViews时,布局xml没有指定layout_witdh,“You must supply a layout_width attribute...”:
对应的布局文件,明明指定了layout_witdh:
应用侧一通查,一笔一笔回退版本(需要快速出版本时的惯用操作),发现是一笔毫不相关的改动,引入的问题。但明眼人一看,这笔提交肯定不是真正原因,我们没有应用源码,那从系统侧探寻真正的原因。
深究
其实一个很小的改动就可以避免崩溃,把layout_width中的@dimen改成数值即可,那就是资源查找的问题了。
让我们来看看RemoteViews展开过程中,是如何查找本来位于远程apk的资源文件的。由于BUG比较好复现,直接断点(问题在系统sdk源码,但是运行在Launcher进程中):
RemoteViews的inflate过程中,如果传入了root,其xml的最外层View的layout相关属性,需要由root计算(对应的是本进程的Resource),其中就包含了layout_width、layout_height等。如果xml有引用dimen等资源,也会在本地进程(Launcher)的Resource中查找。
由于系统本身是根据资源id查找资源(不管其名称如何),资源id是自动生成并且进程独立,因此拿着驴唇不对马嘴的id在本地资源中查找时,可能会找到,也可能会找不到,也可能找到但不对(大概率)。
所以一笔看似不相干的改动,导致了资源id的重排,从而导致本身能找到的id(虽然也是个错的)找不到了,或者找到的资源并不是一个合法的数值。
那么这个RemoteViews的root哪里来的呢?为什么走到了root != null的分支?原来,在定制的Launcher的代码中发现,其加载远端布局时,自动创建一个FrameLayout作为root传入:
结案
至此真相大白,于时让对端进程在外层布局禁止使用@开头的引用资源,Launcher修改的话影响比较大(⊙﹏⊙)。
RemoteViews官网并没有提到这些用法细节,在遇到问题时,只能从源码中寻找答案,加上断点辅助,可以快速定位,找到原因。