Android 自定義Lint檢測(cè)

1.背景

一個(gè)大型項(xiàng)目往往需要幾人甚至是十幾人參與開(kāi)發(fā),大家編碼習(xí)慣不同,導(dǎo)致一個(gè)項(xiàng)目往往會(huì)出現(xiàn)幾個(gè)LogUtils類。經(jīng)常出現(xiàn)Log的tag以人名命名。盡管軟件組長(zhǎng)嚴(yán)令禁止,可是難免還是有漏網(wǎng)之魚。以下是常見(jiàn)的問(wèn)題:

  • LogUtil有多個(gè)

  • Log tag 以人名命名

  • Glide.with傳入Application,沒(méi)有考量生命周期

  • 使用BitmapFactory.decodeResource 加載圖片,同一個(gè)資源多次調(diào)用會(huì)重復(fù)加載

  • 直接使用new Thread 去開(kāi)啟一個(gè)線程

  • 資源文件命令各種各樣

2.Lint介紹

lint是android studio自帶的靜態(tài)代碼分析工具,能夠?qū)?Android 源代碼進(jìn)行掃描和檢查,并發(fā)現(xiàn)可優(yōu)化的代碼和潛在性的異常,從而方便開(kāi)發(fā)人員盡早地予以處理。通常在做apk的性能優(yōu)化時(shí),Lint也可以為我們提供幫助?!続nalyze】->【InSpect Code】 掃描整個(gè)項(xiàng)目。可以檢測(cè)圖片是否 重復(fù),優(yōu)化xml布局 等等

image-20211101103528279.png

3.自定義Lint實(shí)現(xiàn)

當(dāng)前環(huán)境:

  • Android studio 2020.3.1

  • gradle-6.5-all.zip

  • build:gradle:4.1.1

步驟1:新建一個(gè)項(xiàng)目

步驟2:new 一個(gè) 名為【check】Android library

步驟3:new 一個(gè)名為【lintrule】java library

App 的build.gradle:

implementation project(path: ':check')

check的 build.gradle

/**
 *在庫(kù)項(xiàng)目中使用這個(gè)新的配置來(lái)進(jìn)行要包含在已發(fā)布的AAR中的lint檢查,如下所示。這意味著使用庫(kù)的項(xiàng)目也會(huì)應(yīng)用這些lint檢查
 */
lintPublish project(':lintrule')

lintrule的 build.gradle

apply plugin: 'java'
dependencies {
    compileOnly 'com.android.tools.lint:lint-api:27.0.1'
    compileOnly 'com.android.tools.lint:lint-checks:27.0.1'
}
jar {
    manifest {
        attributes("Lint-Registry-v2": "com.example.lintrule.LintRegistry") //更改自己的注冊(cè)器
    }
}

注意在高版本的gradle中,build.gradle 引入java插件是通過(guò)這種方式:

plugins {
    id 'java-library'
    id 'kotlin'
}

通過(guò)這種方式驗(yàn)證的,我做過(guò)實(shí)驗(yàn)最終無(wú)法顯示lint提示

項(xiàng)目環(huán)境搭建完畢,接下來(lái)正式開(kāi)始編碼。在lintrule 庫(kù)中創(chuàng)建一個(gè)文件LogDetector.java 繼承Detector:

public class LogDetector extends Detector implements SourceCodeScanner {
    public static String TAG="LogDetector  ";
    public static final Issue ISSUE = Issue.create(
            "LogId", //第一無(wú)二的id即可 
            "不要直接使用Log", //描述信息
            "不要直接使用Log",     // 描述信息
            Category.MESSAGES,
            5,
            Severity.WARNING,
            new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE)//文件類型意味著只掃描java文件
    );

    @Nullable
    @Override
    //根據(jù)名稱 去檢查方法
    public List<String> getApplicableMethodNames() {
        return Arrays.asList("v", "d", "i", "w", "e");//Log.d()  Log.e() 等方法名
    }

    // 類似還有visitClass 包括Gradle的訪問(wèn)
    @Override
    public void visitMethodCall(@NotNull JavaContext context, @NotNull UCallExpression node, @NotNull PsiMethod method) {
        boolean isMemberInClass = context.getEvaluator().isMemberInClass(method, "android.util.Log");
        boolean isMemberInSubClassOf = context.getEvaluator().isMemberInSubClassOf(method, "android.util.Log", true);
        System.out.println(TAG+obj.getClass().getName());//可以通過(guò)gradlew lint 看到打印信息
        if (isMemberInClass || isMemberInSubClassOf) {
            context.report(ISSUE, node, context.getLocation(node), "不要直接使用Log");//report 上報(bào)提示信息給開(kāi)發(fā)者
        }
    }
}

Detector:中文意思 探測(cè)器,檢測(cè)

SourceCodeScanner:指定掃描文件類型,提供對(duì)應(yīng)的方法,還有Detector.GradleScanner,ClassScanner 等等

有了Detector,還需要將Detector 注入到lint 體系中去。 重寫IssueRegistry 類的getIssues方法

public class LintRegistry extends IssueRegistry {
    @NotNull
    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(
                LogDetector.ISSUE 
        );
    }
}

之后一定要在lintrule的 build.gradle 加入下面代碼

jar {
    manifest {
        attributes("Lint-Registry-v2": "com.example.lintrule.LintRegistry")//自己注冊(cè)器的包名+類名
    }
}

網(wǎng)上很多文章都要求如下配置, 實(shí)際測(cè)試發(fā)現(xiàn)不需要。只要加入上面代碼 即可!


image-20211101200320313.png

之后:build gradle 或者 gradlw lint 都可以刷新lint規(guī)則,萬(wàn)一還不行,就重啟看看

ps: 在【Terminal】中可以通過(guò) gradlew lint 命令 查看 日志的輸出

實(shí)驗(yàn)結(jié)果:


image-20211101212313089.png

以上就是Lint的基本使用。lint 可以檢測(cè)到方法名。那么它能否檢測(cè)參數(shù)值呢?。如何給Glide.with 傳入Application 提示?

4.檢測(cè)Glide.with傳入Application

public class GlideWithDetector extends Detector implements SourceCodeScanner {
    public static final Issue ISSUE = Issue.create(
            "glideWithId",
             "Glide.with盡量別傳入Application",
           "Glide.with盡量別傳入Application", // no need
            Category.MESSAGES,
            7,
            Severity.WARNING,
            new Implementation(GlideWithDetector.class, Scope.JAVA_FILE_SCOPE)
    );

    @Nullable
    @Override
    public List<String> getApplicableMethodNames() {
        return Arrays.asList("with");
    }

    @Override
    public void visitMethodCall(@NotNull JavaContext context, @NotNull UCallExpression node, @NotNull PsiMethod method) {
        boolean isMemberInClass = context.getEvaluator().isMemberInClass(method, "com.bumptech.glide.Glide");
        boolean isMemberInSubClassOf = context.getEvaluator().isMemberInSubClassOf(method, "com.bumptech.glide.Glide", true);
        if (isMemberInClass || isMemberInSubClassOf) {
            System.out.println("Glide2: "+node.getValueArguments().stream().count());
            String obj = node.getValueArguments().get(0).asSourceString();
            if (obj.toLowerCase().contains("application".toLowerCase())) { //檢驗(yàn) application 
                context.report(ISSUE, node, context.getLocation(node), "Glide.with盡量別傳入Application");
            }
        }
    }
}

UCallExpression 中文表達(dá)式 如: Log.d("TAG","1111111")
PsiMethod 單純的指 Log.d()的方法
獲取參數(shù)的類型: method.getParameters()[0].getType()
獲取參數(shù)值: String obj = node.getValueArguments().get(0).asSourceString();

之后將GlideWithDetector 添加進(jìn).

public class LintRegistry extends IssueRegistry {
    @NotNull
    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(
                LogDetector.ISSUE,
                GlideWithDetector.ISSUE,
        );
    }
}

既然lint 可以掃描。build.gradle 我是不是可以根據(jù)lint 來(lái)梳理 各個(gè)組件之間的依賴關(guān)系呢?參考了一些文章:https://juejin.cn/post/6963444269419872264#heading-8 發(fā)現(xiàn)的確是可行的。

5.實(shí)現(xiàn)module組件依賴關(guān)系可視化

自定義javaBean: TreeNode

public class TreeNode {
    private String currentName;//當(dāng)前module的名字
    private List<TreeNode>chidrenNodes;//當(dāng)前module下面的子module

    public TreeNode(String currentName) {
        this.currentName = currentName;
    }

    public TreeNode(String currentName, List<TreeNode> chidrenNodes) {
        this.currentName = currentName;
        this.chidrenNodes = chidrenNodes;
    }

    public String getCurrentName() {
        return currentName;
    }

    public List<TreeNode> getChidrenNodes() {
        return chidrenNodes;
    }
}

DependencyDetector 代碼實(shí)現(xiàn)

public class DependencyDetector extends Detector implements Detector.GradleScanner { //繼承GradleScanner

    private TreeNode mTreeNode;
    public static final Issue ISSUE = Issue.create(
            "dependeId",
            "不需要提示",
            "不需要提示",
            Category.MESSAGES,
            5,
            Severity.WARNING,
            new Implementation(DependencyDetector.class, EnumSet.of(Scope.GRADLE_FILE))
    );
    
    @Override
    public void beforeCheckRootProject(@NotNull Context context) {
        mTreeNode = getNodes(context.getMainProject());
        super.beforeCheckRootProject(context);
        stringBuffer=new StringBuffer();
        printNode(mTreeNode);
        System.out.println(stringBuffer.toString());
    }


    // 創(chuàng)建一個(gè)TreeNode 數(shù)據(jù)結(jié)構(gòu)    遞歸填充個(gè)節(jié)點(diǎn)
    public TreeNode getNodes(Project project) {
        List<Project> projects = project.getDirectLibraries();//獲取子project,肯定是包括我們的依賴module
        List<TreeNode> nodes = new ArrayList<>();
        List<String> strings = new ArrayList<>();//存在多次掃描的情況
        TreeNode mTreeNode = new TreeNode(project.getName(), nodes);
        if (projects == null || projects.size() == 0) {
            return mTreeNode;
        }
        for (int i = 0; i < projects.size(); i++) {
            Project mProject = projects.get(i);
            if (mProject.isGradleProject() && !strings.contains(mProject.getName())) {
                nodes.add(getNodes(mProject));
                strings.add(mProject.getName());
            }
        }
        return mTreeNode;
    }
    
    
    StringBuffer stringBuffer;
    //將組件組裝成,其中A,B 代表module名稱
    //  A-->B   
    //  A-->c
    //  B-->D
    //  B-->C
    public void printNode(TreeNode mTreeNode) {
        for (int i = 0; i < mTreeNode.getChidrenNodes().size(); i++) {
            TreeNode childTreeNode=mTreeNode.getChidrenNodes().get(i);
            stringBuffer.append(""+mTreeNode.getCurrentName()+"-->"+childTreeNode.getCurrentName()+"\n");
            printNode(childTreeNode);
        }
    }
}

然后在gradlew lint 將輸出的日志 copy出來(lái):如 :

app-->module2
module2-->module5
app-->module3
app-->check
check-->module4
check-->module5

將此字符串復(fù)制這個(gè)字符串生成圖形 網(wǎng)站上。能快速展示出各個(gè)module之間的依賴關(guān)系。幫助新人快速理解項(xiàng)目

image-20211101211642311.png

事件做到這步,基本上算完成了。

以上就是我對(duì)lint的學(xué)習(xí)。lint的玩法還有很多,等待各位去挖掘。

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

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

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