用perl脚本批量替换Android项目中的代码

有幸在之前的工作中用到了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;
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容