一、背景
在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仓库同学均可使用。