1、问题
最近写layout时遇到一个奇怪的问题,layout布局中有两个控件属性都引用了在其后定义的控件id,编译时其中一个报id找不到,但是另一个却没有报错,而且布局显示都正常。布局layout大致如下:
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toLeftOf="@id/btn"
android:textStyle="bold" />
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toLeftOf="@id/arrow" />
<ImageView
android:id="@+id/arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/arrow" />
如上layout所示,Button控件引用了id arrow,TextView控件又引用了id btn。最终报的是找不到btn id,将Button控件挪到TextView前面后发现,编译正常,没有报错,程序运行后布局也没有错乱现象,完全没有影响。
2、分析
这不科学,按照常规来说,被引用的id应该必须先于引用它的控件声明。这里大概可以分为三种情况:
- 可以先添加被引用的控件,声明id属性,然后再添加引用它的控件
- 也可以后添加被引用的控件,但是在添加引用它的控件时需要使用@+id先添加被引用控件的id,例如android:layout_toLeftOf=”@+id/arrow”
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toLeftOf="@+id/arrow" />
<ImageView
android:id="@id/arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/arrow" />
- 使用android sdk自带的id,例如@android:id/text1
以上三种情况,不管哪种,都是保证在引用控件id时,该id已经存在R文件中。可是我们这里这三种情况均不符合,为什么也是可以的呢?
难道是arrow的某些属性比较特殊,例如layout_alignParentRight属性,会导致控件优先被解析?去掉该属性测试下也没有报错,这应该不是问题根源所在。因为Android对于xml解析是从上往下递归进行的;另外,layout中控件的位置确定应该在是运行时加载这个布局文件才会去measure、layout的。
换个思路,既然没有报错,那么应该是在解析这个layout时,已经存在了该arrow id。我们知道在编译打包阶段,aapt会为所有的资源生成唯一id(R文件)。在解析layout时,aapt会去R文件中检查id的正确性。那么也就是说项目中其他地方也定义了相同的id,并且先于该layout被解析了。
这样出现的问题也就可以解释了,该layout所在的项目main依赖了一个底层的base项目,base项目中已经定义了arrow id。打包时,aapt会先将base打包成aar,因此R文件中已经存在了arrow id,导致我们的layout没有报找不到id的错误。另外,事实上btn id在main项目中也存在多处重复定义,但是却报了找不到id的错误,原因应该是另一处的double_user所在的layout并没有先于被解析导致的。
3、总结
虽然引用后定义的控件id可能不会报错,但是还是应该遵循先定义,后引用的规则。否则,不知道什么时候底层依赖的aar改了id,上层就会报错,修改起来将很麻烦。
4、脚本开发
为了彻底解决这样的潜在bug,特地学习了下python,写了个小脚本,用于检查项目中存在问题的layout文件。脚本主要针对ReleativeLayout中的相对位置属性进行扫描检查,代码如下:
#!/usr/bin/env python
import xml.etree.ElementTree as XmlTree
import os
dir = '../Demo/build/intermediates/res/merged/common/debug/layout/'
attrib_id = '{http://schemas.android.com/apk/res/android}id'
attrib_toleft = '{http://schemas.android.com/apk/res/android}layout_toLeftOf'
attrib_toright = '{http://schemas.android.com/apk/res/android}layout_toRightOf'
attrib_above = '{http://schemas.android.com/apk/res/android}layout_above'
attrib_below = '{http://schemas.android.com/apk/res/android}layout_below'
attrib_tostart = '{http://schemas.android.com/apk/res/android}layout_toStartOf'
attrib_end = '{http://schemas.android.com/apk/res/android}layout_toEndOf'
attrs = [attrib_toleft, attrib_toright, attrib_above, attrib_below, attrib_tostart, attrib_end]
#parse each layout to find error
def checklayout(layoutname):
xmltree = XmlTree.parse(dir + layoutname)
root = xmltree.getroot()
ids = []
for element in root.iter():
for key in element.attrib.keys():
value = element.attrib.get(key)
if key == attrib_id:
id = value.split('/')[1]
ids.append(id)
if key in attrs:
target = element.attrib.get(key).split('/')
if target[0] != '@+id' and target[0] != '@android:id' and target[1] not in ids:
return layoutname
return ''
#main function to start this script
def main():
print '******loading******'
layouts = [filename for filename in os.listdir(dir) if os.path.isfile(dir + filename)
and os.path.splitext(filename)[1] == '.xml']
result = []
for layout in layouts:
error_layout = checklayout(layout)
if error_layout != '':
result.append(error_layout)
print result
main()