用perl腳本批量替換Android項目中的代碼

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容