用perl脚本批量替换Android项目中的代码
2019-12-26 本文已影响0人
移动开发的95后
有幸在之前的工作中用到了perl脚本,个人非常喜欢它的简约和文件操作性,有兴趣的同学欢迎一起交流
wechat: whatistheman 备注:简书 perl
最近做Android项目遇到了一个需求:项目使用了大量的ButtterKnife,由于ButtterKnife的依赖注入特性,使得项目整体编译速度很慢,达10几分钟
为提高效率,考虑替换ButtterKnife改为原生的findViewById方式。
在项目中搜了下用到的地方有小几万处,故放弃人工修改,使用perl脚本批量替换,代码如下,随笔。
main.perl
#!/usr/bin/perl
use warnings;
use strict;
use constant JAVA_FILE => qr/^.*\S+.java\s*$/;
# 扫描目录下的所有.java文件,进行类文件级别的自动化替换ButterKnife操作
# 模块根目录
my $dir = $ARGV[0];
my $pkgName = "";
scan_file($dir);
sub scan_file{
my @files = glob($_[0]);
foreach (@files){
if(-d $_){
my $path = "$_/*";
scan_file($path);
}elsif(-f $_ && $_ =~ JAVA_FILE){
system "./checkUseBF.perl $_ $pkgName";
}
}
}
checkUseBF.perl
#!/usr/bin/perl
use warnings;
use strict;
# 脚本调用前处理掉 mUnbinder = ButterKnife.bind(this, view); 这种情况
use constant BUTTER_KNIFE => qr/^\s*import\s+butterknife.(\S+)\s*$/;
use constant BUTTER_KNIFE_ROOT_BIND => qr/^\s*ButterKnife.bind\((.*)\)\;.*$/;
use constant BUTTER_KNIFE_BINDVIEW => qr/^\s*\@BindView\((\S+)\).*$/;
use constant BUTTER_KNIFE_BINDVIEW_DEF => qr/^\s*\@BindView\(\S+\)\s+(\S+\s*\S+)\;.*$/;
use constant BUTTER_KNIFE_ONCLICK => qr/^\s*\@OnClick\((.*)\).*$/;
use constant BUTTER_KNIFE_ONCLICK_FUNC => qr/^\s*\@OnClick\(.*\).*\s+(\S+\s*\(.*\)).*$/;
use constant BUTTER_KNIFE_ONCLICK_FUNC_PARAM => qr/^\s*(\S+)\s*\((.*)\).*$/;
# 有继承无接口实现
use constant CLASS_DEFINITION_TYPE1 => qr/^.*class\s+\S+\s+(extends\s+\S+).*$/;
# 有继承有接口实现,但接口实现被排到了第二行
use constant CLASS_DEFINITION_TYPE1_1 => qr/^.*(implements).*$/;
# 有继承有接口实现
use constant CLASS_DEFINITION_TYPE2 => qr/^.*class\s+\S+\s+extends\s+\S+\s+(implements).*$/;
# 无继承无接口实现
use constant CLASS_DEFINITION_TYPE3 => qr/^.*class\s+(\S+)\s*\{.*$/;
# 无继承有接口实现
use constant CLASS_DEFINITION_TYPE4 => qr/^.*class\s+\S+\s+(implements).*$/;
use constant IMPORT_R => qr/^\s*import\s+\S+\.R2\;.*$/;
use constant IMPORT => qr/^\s*import\s+\S+\;.*$/;
use constant PARENT_CLASS_TYPE => qr/^.*\s*extends\s+(\S+).*$/;
use constant EMPTY_LINE => qr/^\s*\n$/;
# TODO 考虑两个bind在同一行 或者 在一行半+半行的情况
# 传入参数 文件目录
my $foundFile = $ARGV[0];
open CONFIG,"<",$foundFile || die "Error, can`t open the file for read";
my @array = readline CONFIG;
close CONFIG;
# 记录当前文件调用ButterKnife.bind的次数
my $bindCount = 0;
# 记录当前文件类定义的次数
my $classDefCount = 0;
# 记录当前文件类的种类 1 activity 2 自定义view
my $classDefType = 0;
# 记录当前文件调用ButterKnife.bind的参数 前两个为bind的参数,第三个参数为所在行数,""即不存在
my @bindParams = ("","","");
# 记录import所在行
my @importLines = ();
# 记录@OnClick所在行
my @onClickLines = ();
# 记录@BindView所在行
my @bindViewLines = ();
my $split = '~';
# key layoutId value 函数体 xxx(View x) or xxx() 例如 leftBtnClick(View v)
my %onClickMap = ();
# key layoutId value 控件类名+实例 例如 TextView mAuthNameView
my %bindViewMap = ();
# 待插入的接口名称
# user change var
my $insertImplments = "doBindView";
# user change var
my $insertImplmentsType = "IViewBinder";
# user change var
my $param_name = "view";
my $param_type = "View";
# user change var
# 待插入的接口定义
my $implmentMethod_view = " \@Override\n public void $insertImplments \($param_type $param_name\) ";
# user change var
my $importDef = "import com\.smile\.gifmaker\.mvps\.IViewBinder\;\n";
print "start all $foundFile\n";
# 第一大步骤的循环中将会实现如下四个目的
# 1 判断当前文件首部是否有import butterknife库,如果没有引用则直接返回,如果有则记录在变量中
# 2 判断当前文件是否调用了大于一次ButterKnife.bind 若存在调用则记录其位置,若仅调用了一次bind则执行后续操作,否则在文件首部插入注释"perl check..."后退出
# 3 判断当前文件是否有定义大于一次的类定义 若不等于一次,则在文件首部插入注释"perl check..."后退出
# 4 判断当前文件是否使用@OnClick、@BindView,若有则记录所在行位置
for(my $i = 0;$i<=$#array;$i++){
#找到ButterKnife.bind所在行
if($array[$i] =~ BUTTER_KNIFE_ROOT_BIND){
my $params = $1;
if($params =~ ","){
$bindParams[0] = (split(/,/,$params))[0];
$bindParams[1] = (split(/,/,$params))[1];
}else{
$bindParams[0] = $params;
}
$bindParams[2] = $i;
$bindCount++;
if($bindCount > 1){
print "调用了大于一次ButterKnife.bind,插入注释回退\n";
unshift(@array, "//perl check,when the Java file uses method 'Butterknife.bind' twice or more,do nothing,signed by wangpeng09\@kuaishou.com \n");
overrideFile(@array);
exit;
}
}elsif($array[$i] =~ BUTTER_KNIFE){
# 记录import行,留在后续删除
push(@importLines,$i);
}elsif($array[$i] =~ BUTTER_KNIFE_ONCLICK){
push(@onClickLines,"$i$split$1");
}elsif($array[$i] =~ BUTTER_KNIFE_BINDVIEW){
push(@bindViewLines,"$i$split$1");
}elsif($array[$i] =~ CLASS_DEFINITION_TYPE1
|| $array[$i] =~ CLASS_DEFINITION_TYPE2
|| $array[$i] =~ CLASS_DEFINITION_TYPE3
|| $array[$i] =~ CLASS_DEFINITION_TYPE4){
$classDefCount++;
if($classDefCount > 1){
print "存在大于一次的类定义,插入注释回退\n";
unshift(@array, "//perl check,when the Java file define class type twice or more,do nothing,signed by wangpeng09\@kuaishou.com \n");
overrideFile(@array);
exit;
}
# 判断当前类文件是否为activity文件或自定义view文件 若是这两种类型,则一定有继承关系
if($array[$i] =~ PARENT_CLASS_TYPE){
my $parentClass = $1;
if($parentClass =~ /Activity/){
$classDefType = 1;
}elsif($parentClass =~ /View/
|| $parentClass =~ /Layout/){
$classDefType = 2;
}
}
}
}
print "finish step 1 调用ButterKnife.bind的次数:$bindCount param1 : $bindParams[0] param2 : $bindParams[1] param3 : $bindParams[2]+1\n";
if($#importLines < 0){
print "no import\n";
exit;
}
print "finish step 2 引用import行数:($#importLines+1) \n";
# 3 找到import所在行删除
for my $line(@importLines){
$array[$line] = "";
}
print "finish step 3 找到import所在行删除 \n";
# 4 通过@bindParams 找到ButterKnife.bind所在行将其用接口表达方式代替
# 分两类处理 ButterKnife.bind(this) 或 ButterKnife.bind(this, view)
if($bindParams[2] ne ""){
if(hasMethodParams()){
# 对于ButterKnife.bind(this, view)直接拿到view传递参数
$array[$bindParams[2]] =~ s/ButterKnife\.bind\(.*\)\;/$insertImplments\($bindParams[1]\)\;/g;
}else{
# 对于ButterKnife.bind(this),分情况讨论 1 activity 2 自定义view
if($classDefType == 1){
# activity
$array[$bindParams[2]] =~ s/ButterKnife\.bind\(.*\)\;/$insertImplments\(getWindow\(\)\.getDecorView\(\)\)\;/g;
}elsif($classDefType == 2){
# 自定义view
$array[$bindParams[2]] =~ s/ButterKnife\.bind\(.*\)\;/$insertImplments\(this\)\;/g;
}
}
}
print "finish step 4 找到ButterKnife.bind所在行将其用接口表达方式代替 \n";
# 5 找到@OnClick的位置,删除所在行并记录控件id与方法名称的对应关系
for my $line(@onClickLines){
my $lineNum = (split(/$split/,$line))[0];
my $layoutIds = (split(/$split/,$line))[1];
# 找到@OnClick注解对应的函数体 xxx(xx) 型参可能为空或View
my $func = "$array[$lineNum]$array[$lineNum+1]";
$func =~ s/\n//g;
# 精确匹配函数体 xxx(View x) or xxx() 并记录layoutId与函数体的对应关系
if($func =~ BUTTER_KNIFE_ONCLICK_FUNC){
$onClickMap{$layoutIds} = $1;
}
# 精准匹配 @OnClick(xxx) 并删除
$array[$lineNum] =~ s/\@OnClick\($layoutIds\)//g;
if($array[$lineNum] =~ EMPTY_LINE){
$array[$lineNum] = "";
}
}
print "finish step 5 找到\@OnClick的位置,删除所在行并记录控件id与方法名称的对应关系 \n";
# 6 找到@BindView
for my $line(@bindViewLines){
my $lineNum = (split(/$split/,$line))[0];
my $layoutId = (split(/$split/,$line))[1];
# 找到@BindView注解对应的类型声明 例如 ImageView mImageView;
my $func = "$array[$lineNum]$array[$lineNum+1]";
$func =~ s/\n//g;
# 精确匹配类型声明 例如 ImageView mImageView;
if($func =~ BUTTER_KNIFE_BINDVIEW_DEF){
$bindViewMap{$layoutId} = $1;
}
$layoutId =~ s/ //g;
# 精准匹配 @BindView(xxx) 并删除
$array[$lineNum] =~ s/\@BindView\($layoutId\)//g;
if($array[$lineNum] =~ EMPTY_LINE){
$array[$lineNum] = "";
}
}
print "finish step 6 找到BindView 精准匹配 \@BindView(xxx) 并删除 \n";
# 7 找到文件内的class定义位置,实现接口
for(my $i = 0;$i<=$#array;$i++){
my $tag;
# 在类的定义部分插入implements的实现 合并当前行与下一行两行判断!!!防止定义被分成两行的情况
if($array[$i] =~ CLASS_DEFINITION_TYPE1){
$tag = $1;
if($array[$i+1] =~ CLASS_DEFINITION_TYPE1_1){
# 有继承有接口,但接口定义在下一行
$tag = $1;
$array[$i+1] =~ s/$tag/$tag $insertImplmentsType,/g;
addImplementsDef($i+1);
}else{
# 有继承无接口
$array[$i] =~ s/$tag/$tag implements $insertImplmentsType/g;
addImplementsDef($i);
}
}elsif($array[$i] =~ CLASS_DEFINITION_TYPE2){
$tag = $1;
# 有继承有接口
$array[$i] =~ s/$tag/$tag $insertImplmentsType,/g;
addImplementsDef($i);
}elsif($array[$i] =~ CLASS_DEFINITION_TYPE3){
$tag = $1;
# 无继承无接口
$array[$i] =~ s/$tag/$tag implements $insertImplmentsType/g;
addImplementsDef($i);
}elsif($array[$i] =~ CLASS_DEFINITION_TYPE4){
$tag = $1;
# 无继承有接口
$array[$i] =~ s/$tag/$tag $insertImplmentsType,/g;
addImplementsDef($i);
}
}
print "finish step 7 找到文件内的class定义位置,实现接口 \n";
for(my $i = 0;$i<=$#array;$i++){
if($array[$i] =~ IMPORT_R){
$array[$i] =~ s/R2/R/g;
last;
}
}
for(my $i = 0;$i<=$#array;$i++){
if($array[$i] =~ IMPORT){
$array[$i] = $importDef.$array[$i];
last;
}
}
print "finish step 8 第一个import之前插入新增接口的import行 \n";
overrideFile(@array);
print "finish all $foundFile\n";
################################################################################ function define ################################################################################
sub addImplementsDef{
#在类的定义尾部'{'字符之后,添加对接口方法的实现,并在实现中实现控件初始化和事件监听的定义
if($array[$_[0]] =~ /{/){
my $content = writeMethodContent();
$array[$_[0]] =~ s/\{/\{\n$implmentMethod_view\{\n$content\n \}\n/g;
}else{
addImplementsDef($_[0]+1);
}
}
# 合成实现接口方法的内容 控件初始化、设置事件监听等
sub writeMethodContent{
# step1 控件初始化
my $widgetInit = "";
foreach my $key(keys %bindViewMap){
my @widgets = (split(/ /,$bindViewMap{$key}));
my $paramClass = $widgets[0];
my $paramName = $widgets[$#widgets];
my $layoutId = $key;
$layoutId =~ s/R2/R/g;
$widgetInit .= " $paramName = \($paramClass\)findViewById\($layoutId\)\;\n";
}
# step2 设置监听事件
my $widgetSetListener = "";
foreach my $key(keys %onClickMap){
my $methodDef = $onClickMap{$key};
my @layoutIds = (split(/,/,$key));
for my $item(@layoutIds){
$item =~ s/R2/R/g;
my $methodName = "//\n";
my $methodparams = "";
my $lamadaParam = "view";
if($methodDef =~ BUTTER_KNIFE_ONCLICK_FUNC_PARAM){
$methodName = $1;
$methodparams = $2;
$methodparams =~ s/ //g;
}
if($methodparams ne ""){
$methodparams = $lamadaParam;
}
$widgetSetListener .= " if\($param_name\.findViewById\($item\) \!\= null\) \{\n $param_name\.findViewById\($item\)\.setOnClickListener\($lamadaParam \-\> \{$methodName\($methodparams\)\;\}\)\;\n \}\n";
}
}
return $widgetInit.$widgetSetListener;
}
# 空字符串返回false 否则返回true 判断接口函数是否需要带view参数
sub hasMethodParams{
$bindParams[1] =~ s/ //g;
return $bindParams[1];
}
# 对处理后的内容数组写入并替换原文件
sub overrideFile{
my @list = @_;
system "rm $foundFile";
system "touch $foundFile";
open NEW_FILE,">",$foundFile || die "Error, can`t open the file for writing";
foreach my $line(@list){
print NEW_FILE "$line";
}
close NEW_FILE;
}