有幸在之前的工作中用到了perl腳本,個人非常喜歡它的簡約和文件操作性,有興趣的同學(xué)歡迎一起交流
wechat: whatistheman 備注:簡書 perl
最近做Android項目遇到了一個需求:項目使用了大量的ButtterKnife,由于ButtterKnife的依賴注入特性,使得項目整體編譯速度很慢,達(dá)10幾分鐘
為提高效率,考慮替換ButtterKnife改為原生的findViewById方式。
在項目中搜了下用到的地方有小幾萬處,故放棄人工修改,使用perl腳本批量替換,代碼如下,隨筆。
main.perl
#!/usr/bin/perl
use warnings;
use strict;
use constant JAVA_FILE => qr/^.*\S+.java\s*$/;
# 掃描目錄下的所有.java文件,進(jìn)行類文件級別的自動化替換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;
# 腳本調(diào)用前處理掉 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*\((.*)\).*$/;
# 有繼承無接口實現(xiàn)
use constant CLASS_DEFINITION_TYPE1 => qr/^.*class\s+\S+\s+(extends\s+\S+).*$/;
# 有繼承有接口實現(xiàn),但接口實現(xiàn)被排到了第二行
use constant CLASS_DEFINITION_TYPE1_1 => qr/^.*(implements).*$/;
# 有繼承有接口實現(xiàn)
use constant CLASS_DEFINITION_TYPE2 => qr/^.*class\s+\S+\s+extends\s+\S+\s+(implements).*$/;
# 無繼承無接口實現(xiàn)
use constant CLASS_DEFINITION_TYPE3 => qr/^.*class\s+(\S+)\s*\{.*$/;
# 無繼承有接口實現(xiàn)
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在同一行 或者 在一行半+半行的情況
# 傳入?yún)?shù) 文件目錄
my $foundFile = $ARGV[0];
open CONFIG,"<",$foundFile || die "Error, can`t open the file for read";
my @array = readline CONFIG;
close CONFIG;
# 記錄當(dāng)前文件調(diào)用ButterKnife.bind的次數(shù)
my $bindCount = 0;
# 記錄當(dāng)前文件類定義的次數(shù)
my $classDefCount = 0;
# 記錄當(dāng)前文件類的種類 1 activity 2 自定義view
my $classDefType = 0;
# 記錄當(dāng)前文件調(diào)用ButterKnife.bind的參數(shù) 前兩個為bind的參數(shù),第三個參數(shù)為所在行數(shù),""即不存在
my @bindParams = ("","","");
# 記錄import所在行
my @importLines = ();
# 記錄@OnClick所在行
my @onClickLines = ();
# 記錄@BindView所在行
my @bindViewLines = ();
my $split = '~';
# key layoutId value 函數(shù)體 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";
# 第一大步驟的循環(huán)中將會實現(xiàn)如下四個目的
# 1 判斷當(dāng)前文件首部是否有import butterknife庫,如果沒有引用則直接返回,如果有則記錄在變量中
# 2 判斷當(dāng)前文件是否調(diào)用了大于一次ButterKnife.bind 若存在調(diào)用則記錄其位置,若僅調(diào)用了一次bind則執(zhí)行后續(xù)操作,否則在文件首部插入注釋"perl check..."后退出
# 3 判斷當(dāng)前文件是否有定義大于一次的類定義 若不等于一次,則在文件首部插入注釋"perl check..."后退出
# 4 判斷當(dāng)前文件是否使用@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 "調(diào)用了大于一次ButterKnife.bind,插入注釋回退\n";
unshift(@array, "http://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行,留在后續(xù)刪除
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, "http://perl check,when the Java file define class type twice or more,do nothing,signed by wangpeng09\@kuaishou.com \n");
overrideFile(@array);
exit;
}
# 判斷當(dāng)前類文件是否為activity文件或自定義view文件 若是這兩種類型,則一定有繼承關(guān)系
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 調(diào)用ButterKnife.bind的次數(shù):$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行數(shù):($#importLines+1) \n";
# 3 找到import所在行刪除
for my $line(@importLines){
$array[$line] = "";
}
print "finish step 3 找到import所在行刪除 \n";
# 4 通過@bindParams 找到ButterKnife.bind所在行將其用接口表達(dá)方式代替
# 分兩類處理 ButterKnife.bind(this) 或 ButterKnife.bind(this, view)
if($bindParams[2] ne ""){
if(hasMethodParams()){
# 對于ButterKnife.bind(this, view)直接拿到view傳遞參數(shù)
$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所在行將其用接口表達(dá)方式代替 \n";
# 5 找到@OnClick的位置,刪除所在行并記錄控件id與方法名稱的對應(yīng)關(guān)系
for my $line(@onClickLines){
my $lineNum = (split(/$split/,$line))[0];
my $layoutIds = (split(/$split/,$line))[1];
# 找到@OnClick注解對應(yīng)的函數(shù)體 xxx(xx) 型參可能為空或View
my $func = "$array[$lineNum]$array[$lineNum+1]";
$func =~ s/\n//g;
# 精確匹配函數(shù)體 xxx(View x) or xxx() 并記錄layoutId與函數(shù)體的對應(yīng)關(guān)系
if($func =~ BUTTER_KNIFE_ONCLICK_FUNC){
$onClickMap{$layoutIds} = $1;
}
# 精準(zhǔn)匹配 @OnClick(xxx) 并刪除
$array[$lineNum] =~ s/\@OnClick\($layoutIds\)//g;
if($array[$lineNum] =~ EMPTY_LINE){
$array[$lineNum] = "";
}
}
print "finish step 5 找到\@OnClick的位置,刪除所在行并記錄控件id與方法名稱的對應(yīng)關(guān)系 \n";
# 6 找到@BindView
for my $line(@bindViewLines){
my $lineNum = (split(/$split/,$line))[0];
my $layoutId = (split(/$split/,$line))[1];
# 找到@BindView注解對應(yīng)的類型聲明 例如 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;
# 精準(zhǔn)匹配 @BindView(xxx) 并刪除
$array[$lineNum] =~ s/\@BindView\($layoutId\)//g;
if($array[$lineNum] =~ EMPTY_LINE){
$array[$lineNum] = "";
}
}
print "finish step 6 找到BindView 精準(zhǔn)匹配 \@BindView(xxx) 并刪除 \n";
# 7 找到文件內(nèi)的class定義位置,實現(xiàn)接口
for(my $i = 0;$i<=$#array;$i++){
my $tag;
# 在類的定義部分插入implements的實現(xiàn) 合并當(dāng)前行與下一行兩行判斷?。?!防止定義被分成兩行的情況
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 找到文件內(nèi)的class定義位置,實現(xiàn)接口 \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{
#在類的定義尾部'{'字符之后,添加對接口方法的實現(xiàn),并在實現(xiàn)中實現(xiàn)控件初始化和事件監(jiān)聽的定義
if($array[$_[0]] =~ /{/){
my $content = writeMethodContent();
$array[$_[0]] =~ s/\{/\{\n$implmentMethod_view\{\n$content\n \}\n/g;
}else{
addImplementsDef($_[0]+1);
}
}
# 合成實現(xiàn)接口方法的內(nèi)容 控件初始化、設(shè)置事件監(jiān)聽等
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 設(shè)置監(jiān)聽事件
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 = "http://\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 判斷接口函數(shù)是否需要帶view參數(shù)
sub hasMethodParams{
$bindParams[1] =~ s/ //g;
return $bindParams[1];
}
# 對處理后的內(nèi)容數(shù)組寫入并替換原文件
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;
}