一、背景
在Android O版本之后,findViewById 函数现在返回的是 <T extends View>,所以以后 findViewById 就不需要强转了。如果项目中compileSdkVersion >= 26,使用findViewById就会提示警告,表示可以不用再写强转了。如下所示:
所以看到这部分的时候就觉得不舒服,而且AS代码区右侧会提示标黄的小警告,光标移上去会提示:Casting 'item.findViewById(R.id.xxx)' to 'TextView' is redundant.
故而觉得此事的解决对强迫症患者挺有必要的,而且也是代码规范迟早要做的事情。所以使用python编写了这个脚本工具。命名为afc.py (auto findViewById cut)。
二、分析
关于findViewById的用法,不外乎如下几类情况:
1.仅findViewById
a. TextView tv1 = (TextView)findViewById(R.id.tv);//无空格
b. TextView tv2 = (TextView) findViewById(R.id.tv);//有空格
c. TextView tv3 = (TextView)root.findViewById(R.id.tv);
d. TextView tv4 = (TextView) root.findViewById(R.id.tv);
正则1:\(\w+\)\s*(?=((\w+\.)?findViewById\(R\.id\.\w+\);))
2.findViewById后set操作
e. ((TextView)findViewById(R.id.tv)).setText("text");//设置文字
f. ((TextView) findViewById(R.id.tv)).setOnClickListener(xxx);//设置点击
g. ((TextView)root.findViewById(R.id.tv)).setText(“text”);
h. ((TextView) root.findViewById(R.id.tv)).setOnClickListener(xxx);
正则2:\(\(\w+\)\s*(\w+\.)?findViewById\(R\.id\.\w+\)\)(?=\.(setOnClickListener|set...))
正则3:(\w+\.)?findViewById\(R\.id\.\w+\)
这里我将他们对应的正则表达式也给了出来。其中e和g是不能忽略强转的。后面的方法是特定的类型才具有的。
三、正则
正则1: \(\w+\)\s*(?=((\w+\.)?findViewById\(R\.id\.\w+\);))
(?=...)代表...是在这个匹配的前面
\s*代表空格个数0-n个
\(\w+\)代表()和英文字符
(\w+\.)?代表若干英文字符和点的组合有0-1个
其他就是字面意思。所以正则1匹配的就是a,b,c,d这些类型的行。
正则2:\(\(\w+\)\s*(\w+\.)?findViewById\(R\.id\.\w+\)\)(?=\.(setOnClickListener|set...))
这个就好理解多了。意思就是匹配f,h这些类型的行。只要在最后的setOnClickListener|set...里面不去包含setText就可以顺利排除这些不能忽略强转的用法了。
正则3:(\w+\.)?findViewById\(R\.id\.\w+\)
含义略。这个正则存在的意义就是替换正则2匹配的内容,比如:正则2匹配出来了((TextView)root.findViewById(R.id.tv)),正则3从这中间拿出root.findViewById(R.id.tv)。从而进行最终的文本替换。
四、脚本
#!/usr/bin/python
# coding=utf-8
import os,re,fileinput,sys
#根据文件扩展名判断文件类型
def endWith(s, *endstring):
array = map(s.endswith,endstring)
if True in array:
return True
else:
return False
def searchFiles(dirname):
# 匹配该样式类型的行:(TextView)findViewById(R.id.tv);
pattern1 = re.compile(ur'\(\w+\)\s*(?=((\w+\.)?findViewById\(R\.id\.\w+\);))')
# 匹配该样式类型的行:findViewById(R.id.xxx) 或 root.findViewById(R.id.xxx)
pattern3Str = ur'(\w+\.)?findViewById\(R\.id\.\w+\)\)'
pattern3 = re.compile(pattern3Str)
# 区分下面两类的行:setText的类型转换不能忽略,而setOnClickListener可以
# ((TextView)findViewById(R.id.tv)).setText("text");
# ((TextView) findViewById(R.id.tv)).setOnClickListener(xxx);
# 可以忽略的加入下面数组
pattern2List = ['setOnClickListener']
# 匹配该样式类型的行 ((TextView) findViewById(R.id.tv)) 或 ((TextView)root.findViewById(R.id.tv))
pattern2Str = ur'\(\(\w+\)\s*' + pattern3Str + '(?=\.(' + '|'.join(pattern2List) + '))'
pattern2 = re.compile(pattern2Str)
count1 = 0
count2 = 0
for root,dirs,files in os.walk(dirname):
for file in files:
if endWith(file, '.java'):
# 打开文件
filename = root + os.sep + file #绝对路径
filename = filename.replace("\\","\\\\") #将路径中的单反斜杠替换为双反斜杠,因为单反斜杠可能会导致将路径中的内容进行转义了,replace函数中"\\"表示单反斜杠,"\\\\"表示双反斜杠
# fileinput模块支持文件的边读边写
for line in fileinput.input(filename, inplace=True):
# 返回一个含两个元素的元组,索引0为替换后的行,索引1为该行替换次数
result1 = re.subn(pattern1, "", line)
line = result1[0]
count1 = count1 + result1[1]
# 找到符合模式2的要替换的部分
toreplace = re.search(pattern2, line)
if toreplace != None:
# 从要替换中找到要替换为的部分
replaced = re.search(pattern3, toreplace.group(0))
if replaced != None:
# 执行最终的替换
result2 = re.subn(pattern2, replaced.group(0), line)
line = result2[0]
count2 = count2 + result2[1]
# 将模式1和模式2的结果写回文件
print line.rstrip()
print '成功!findViewById总转换数:%d个。' % (count1 + count2)
print '模式1的替换数:%d个;' % count1
print '模式2的替换数:%d个。' % count2
if __name__ == '__main__':
searchFiles(sys.argv[1])
五、使用及结果
使用方法非常简单,如下:
python afc.py (本地仓库路径)
例如:
代码修改完后,就去提pr集赞merge代码吧。遇到的问题:checkstyle的时候可能会有没有使用的import。
适用范围:凡compileSdkVersion >= 26的Android仓库同学均可使用。