先來講我爲何要作增量掃描這個事情,畢竟代碼掃描已經老生常談了,業界方案一搜一大堆,有什麼好講的,大部人看到這篇文章的時候確定這麼想吧,可是注意今天我要分享的不是全量掃描,我分享的是從無到有實現增量掃描的過程,有的時候實現一個方案歷來不是重點,咱們對於方案的認知程度纔是咱們本身最重要的收穫 ̄▽ ̄ 。html
再來講說怎麼樣的代碼掃描纔算是高效的,我是這麼理解的:java
不能增量檢查的代碼掃描都是耍流氓,之前的代碼一大堆問題,誰有耐心所有去解決
不能自動化的代碼掃描都是欺騙咱們感情,不是每一個人都有良好的意識每次都去檢查的
不能撤銷提交的代碼掃描都是本身騙本身,檢查出來問題不改,這樣的代碼掃描要來何用
不能持續集成的代碼掃描都是不專業的,問題要快上線了才發現,這樣的代碼掃描風險多高
開發缺的歷來就不是工具,咱們缺的是無縫嵌入的自動化流程、自我Code Review的意識,意識比工具重要。android
這裏扯了一些大道理,你們諒解,口號喊得響,你們纔有興趣看嘛。後面全是乾貨,你們放心,嘿嘿。git
OkLint做爲一個Gradle插件,使用起來超簡單,他能在你提交時發現增量問題,撤銷提交併給你發郵件。github
在根目錄下的build.gradle寫web
allprojects {
apply plugin: 'oklint'
}複製代碼
在講具體實現以前,先來說講我對於高效的代碼掃描是怎麼想的。shell
高效的代碼掃描我以爲有五個方案:apache
方案一是Android Studio自帶的錯誤提示功能,他有個好處就是實時發現問題,缺點就是有些問題隱藏在花花綠綠的代碼裏,你要指定你想檢查的問題爲error才能暴露出來,這樣就須要在每臺電腦上都改動一下,太麻煩了。api
方案二是Android Studio的增量代碼掃描功能,缺點就是不能自動化,不能在團隊內很好落實,不利於統計問題和持續集成。
android-studio
方案三是用Sonar持續集成,可是他有個問題是不能增量,咱們團隊用過,最後由於之前問題太多根本推行不起來,相信好多團隊都是這樣吧。
方案四是用Android Gradle插件2.3.0之後提供的新功能 baseline,他也是全量掃描,可是他能增量顯示問題,這個方案後期和Sonar持續集成,能夠做爲Plan B。
方案五是我如今用的方案,增量代碼掃描和git hooks搭配使用,味道更好。剛開始的思路是在git commit以前掃描增量代碼,結果發現lint掃描比較慢(我嘗試改了,改了之後確實快了可是有些問題就掃描不到了,畢竟掃描代碼仍是須要整個項目的代碼才能更好的找到問題)。後面我聽取了同事峯哥的意見,採用另一個思路,偷偷在git commit以後去掃描。有些人要問了爲何不在gitlab上的webhook裏面執行,嗯你很機智,這樣實現也有很大的優勢,可是我更想及時檢查每一次改動,越早發現越好解決問題。
我的以爲上面五個方案,方案四和方案五左右開弓效果更好。方案五負責及時檢查每一次改動,方案四負責發現全量代碼潛在的問題。
方案作出來了,要是不對比一下,就沒辦法愉快地吹NB了。
功能 | OkLint | Lint命令行 | Android Gradle 插件 | Android Studio |
---|---|---|---|---|
增量 | 能夠 | 不行 | 不行,2.3.0後支持全量掃描增量顯示 | 能夠 |
自動化 | 能夠 | 不行 | 不行 | 不行 |
持續集成 | 能夠 | 不行 | 不行 | 不行 |
代碼回滾 | 能夠 | 不行 | 不行 | 能夠 |
只掃描優先級高的問題 | 能夠 | 配置麻煩 | 配置麻煩 | 配置麻煩 |
Android有本身的Code Lint
,可是他只能全量掃描,並且無法只掃描優先級高的。當然Android Studio能夠在提交前面執行code analysis,可是做爲一個團隊你很難落實讓每一個人每次提交代碼都去執行,就算執行了你也不能保證他必定去改正這個問題,就算他改了這個問題,你也不能保證多個分支合併的代碼沒有問題,因此一個能自動在git commit時掃描增量代碼的工具仍是頗有必要的。
思路其實很簡單的,流程很簡單
gradle插件copy git hooks------> git hooks自動執行增量掃描的任務------> git diff找到增量代碼------> lint-api.jar調用project.addfile() 掃描增量代碼------>javamail發送問題郵件------>git reset回滾代碼
好了如今你已經獲得個人大乘佛法了,你能夠屁顛屁顛地回大唐娶妻生子走向人生巔峯了,我保證我不阻止你。
這個命令感謝個人另外一個同事馬老闆,他坐爲旁邊,我每次急躁的時候他都耐心幫我找答案。
private List<String> getPostCommitChange() {
ArrayList<String> filterList = new ArrayList<String>()
try {
String projectDir = getProject().getProjectDir()
String commond = "git diff --name-only --diff-filter=ACMRTUXB HEAD~1 HEAD~0 $projectDir"
String changeInfo = commond.execute(null, project.getRootDir()).text.trim()
if (changeInfo == null || changeInfo.empty) {
return filterList
}
String[] lines = changeInfo.split("\\n")
return lines.toList()
} catch (Exception e) {
return filterList
}
}複製代碼
用git diff命令找到剛提交的commit都改動了哪些文件,我講一下他的每一個參數的意思
這裏着重說一下在gralde裏寫命令的一個注意點,要執行帶有單引號的命令會執行爲空的問題
譬如
git status -s | grep -v '^D'//列出當前要提交的commit變更了哪些文件並排除刪除的文件複製代碼
你覺得"git status -s | grep -v '^D'".execute
就好了嗎,太天真了,執行結果爲空,剛開始我覺得只要加上轉義符就行,結果仍是不行。後面反覆實驗發現要這麼寫
["/bin/bash", "-c", "git status -s | grep -v '^D'"].execute()複製代碼
原理比較長,怕你們看的似懂非懂,我先給結果,這樣比較好。看到一些不明白的名詞能夠先忽略掉,後面原理裏面會提,我儘可能講的淺顯易懂。
我寫了一個增量掃描的task,而後寫了一個LintClinet
,這個LintClient
會掃描代碼,它繼承android gradle的LintGradleClient
,task會調用這個client的run方法,run方法就是掃描方法。
而增量掃描的關鍵性代碼是修改LintGradleClient
的createLintRequest
方法,往project加入要掃描的文件
@Override
protected LintRequest createLintRequest(@NonNull List<File> files) {
//注意這個project是com.android.tools.lint.detector.api.project
LintRequest lintRequest = super.createLintRequest(files);
for (Project project : lintRequest.getProjects()) {
project.addFile(changefile);//加入要掃描的文件
addChangeFiles(project);
}
return lintRequest;
}複製代碼
有個注意點我要提一下LintGradleClient
構造函數須要參數,除了variant能夠爲空,其餘都不能爲空。由於不在android gradle插件內部,因此有些參數獲取須要動一些腦筋。
LintGradleClient(
IssueRegistry registry,//掃描規則
LintCliFlags flags,
org.gradle.api.Project gradleProject,//gradle 項目
AndroidProject modelProject,// android項目
File sdkHome,// android sdk目錄
Variant variant,//編譯的Variant
BuildToolInfo buildToolInfo) {//編譯工具包複製代碼
篇幅有限,參數講太多反而把你們搞糊塗,我就講一個參數,如何獲取AndroidProject
private AndroidProject getAndroidProject() {
GradleConnector gradleConn = GradleConnector.newConnector()
gradleConn.forProjectDirectory(getProject().getProjectDir())
AndroidProject modelProject = gradleConn.connect().getModel(AndroidProject.class)
return modelProject
}複製代碼
剛開始想的很簡單呀,命令行 Lint不是也能掃描代碼嗎,那裏面確定有指定掃描文件和目錄的參數吧,別說還真有, --sources <dir>
,結果一試,發現是有結果,可是掃描出來的問題根本不是那個文件的問題呀,而後我同事說在他電腦卻提示不能掃描gradle項目,一會兒就矇蔽了,無從下手的感受,剛開始我覺得命令沒用對,可是改來改去都不對,後面我嘗試去除裏面的gradle project判斷限制,而後指定掃描文件,仍是掃描不出該有的問題,我就先暫停這個方案的研究。
既然上面這條路走不通,我就去找android studio的源碼看他是怎麼實現增量掃描的,結果在Android Studio源碼裏面,搜索lint根本沒有找到任何相關的代碼,後面發現實際上是在另外的Plugin源碼裏。不過他依賴於Intellij Module
,Module會找到每一個類,那我又沒有Module這個上下文,這麼說這個方案仍是走不通。
那就再換一個思路,Android Gradle插件不是也能夠實現Lint掃描,那我改一改不就能夠增量掃描,結果一拿到他的代碼就感受無從下手,改來改去都不對呀,不知道哪一行代碼能夠實現增量掃描,就算後面完成了增量掃碼,掃描也很慢。
帶着上面的幾個坑,我研究了Lint內部的實現原理找到了增量代碼掃描的實現方法
我先講一下關於Lint的預備知識,而後再來說上面幾個問題,方便你們更好理解
###Lint掃描內部原理
其實不管是Lint命令行、android gradle插件、android studio都依賴了兩個jar
- lint-api.jar:lint-api是代碼掃描的具體實現
lint-check.jar:lint-check是默認的掃描規則
lint-api.jar內部實現原理:
LintDriver調用analyze()分析LintRequest中的文件------>checkProject----->runFileDetectors----->check對應文件的Visitor,譬如JavaPsiVisitor分析java文件,AsmVisitor分析class文件等
下面講講三種方式分別怎麼實現的
Lint命令行:
lint.sh------>lint.jar------>LintCliClient 的run(IssueRegistry registry, List<File> files)------>LintDriver analyze分析 project
Lint Gradle Task:
Lint.groovy------>LintGradleClient的run(IssueRegistry registry)------>LintDriver analyze分析 LintGradleProject
Android Studio:
AndroidLintGlobalInspectionContext------> performPreRunActivities-----> LintDriver analyze分析IntellijLintProject
明白了原理,咱們回到上面兩個問題
爲何命令行Lint 掃描不出增量代碼的問題
我舉個例子:
譬若有個TestActivity裏面寫了靜態的activity變量,LeakDetector會去檢查這個狀況,可是直接lint --sources app/src/com/demo/TestActivity.java .
你會發現掃描不出這個錯誤或者提示'app' is a Gradle project. To correctly analyze Gradle projects, you should run "gradlew :lint" instead. [LintError]
,其實這兩個問題都是同一個緣由。
LeakDetector會去判斷靜態變量是否是Activity類,可是變量的PsiField倒是com.demo.TestActivity
不是'android'開頭,這樣就掃描不出問題了。
@Override
public void visitField(PsiField field) {
String fqn= field.getType().getCanonicalText();
if (fqn.startsWith("android.")) {//fqn變量是com.demo.TestActivity
if (isLeakCandidate(cls, mContext.getEvaluator())
&& !isAppContextName(cls, field)) {
String message = "Do not place Android context classes in static fields; "
+ "this is a memory leak (and also breaks Instant Run)";
report(field, modifierList, message);
}
}
}複製代碼
那爲何fqn不是android.app.activity
呢,由於lint命令行會把lib目錄下面jar的class加入掃描造成抽象語法樹,可是gradle項目是compile jar的,不在lib目錄下面,這就是爲何高版本的lint裏面提示不能掃描gradle項目。這也側面說明了命令行lint走不通
android studio是怎麼實現lint增量掃描的
android studio內部會掃描IntellijLintProject
中的文件,IntellijLintProject
是由create(IntellijLintClient client, List<VirtualFile> files,Module... modules)
生成的,那就只要找到文件加入project的代碼就能找到增量代碼掃描的方案了。
if (project != null) {
project.setDirectLibraries(Collections.<Project>emptyList());
if (file != null) {
project.addFile(VfsUtilCore.virtualToIoFile(file));
}
}複製代碼
那爲何addfile之後LintDriver
會增量掃描呢,拿java文件掃描舉個例子,LintDriver
會判斷subset是否是爲空,不爲空就不掃描JavaSourceFolders
,只掃描增量文件。
List<File> files = project.getSubset();
if (files != null) {//判斷是否是要增量掃描
checkIndividualJavaFiles(project, main, checks, files);
} else {
List<File> sourceFolders = project.getJavaSourceFolders();
List<File> testFolders = scope.contains(Scope.TEST_SOURCES)
? project.getTestSourceFolders() : Collections.emptyList();
checkJava(project, main, sourceFolders, testFolders, checks);
}複製代碼
雖然Lint支持配置lint.xml去忽略Issue,可是隻能一個個忽略,個人方案是設置優先級低的規則爲Severity.IGNORE,LintDirver會忽略Severity.IGNORE的規則
@Override
public Severity getSeverity(Issue issue) {
Severity severity = super.getSeverity(issue);
if (onlyHighPriority) {
if (issue.getCategory().compareTo(Category.USABILITY) < 0 && issue.getPriority() > 4) {//只掃描優先級比較高的規則
return severity;
}
return Severity.IGNORE;
}
return severity;
}複製代碼
Git Hooks提供了post-commit實現commit以後自動執行任務,可是你會發如今post-commit裏寫 ./gradlew Lint,仍是要等lint任務執行完了才commit成功。我發現只要在shell腳本里加入&>/dev/null就能夠後臺執行了。
nohup ./gradlew LintIncrement &>/dev/null &複製代碼
若是Git Hooks腳本須要每臺電腦本身去複製,這明顯不利於團隊合做,並且不方便後面更新腳本,我選擇用Gradle命令複製到指定目錄,可是這裏有個問題,gradle插件能帶資源文件嗎,若是沒有專門學過gradle說不定一時無從下手,還好我恰好之前看過fastdex裏面是怎麼解決的,經過getResourceAsStream能夠複製Gradle插件resources下面的文件
public static void copyResourceFile(String name, File dest) throws IOException {
FileOutputStream os = null;
File parent = dest.getParentFile();
if (parent != null && (!parent.exists())) {
parent.mkdirs();
}
InputStream is = null;
try {
is = FileUtils.class.getResourceAsStream("/" + name);
os = new FileOutputStream(dest, false);
byte[] buffer = new byte[BUFFER_SIZE];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
} finally {
if (is != null) {
is.close();
}
if (os != null) {
os.close();
}
}
}複製代碼
複製腳本installGitHooks是這樣實現的,finalizedBy保證它在build任務後面自動執行,它會把/resource/post-commit文件複製到工程.git/hooks/post-commit。chmod -R +x .git/hooks/必定要寫,否則沒有權限
private void createGitHooksTask(Project project) {
def preBuild = project.tasks.findByName("preBuild")
if (preBuild == null) {
throw new GradleException("lint need depend on preBuild and clean task")
return
}
def installGitHooks = project.getTasks().create("installGitHooks")
.doLast {
File postCommitFile = new File(project.rootProject.rootDir, PATH_POST_COMMIT)
if (lintIncrementExtension.isCheckPostCommit()) {
FileUtils.copyResourceFile("post-commit", postCommitFile)
} else {
if (preCommitDestFile.exists()) {
preCommitDestFile.delete()
}
}
Runtime.getRuntime().exec("chmod -R +x .git/hooks/")
}
preBuild.finalizedBy installGitHooks
}複製代碼
首先在gradle插件的build.gradle裏面加入javamail
的依賴,剛開始我是直接compile了,可是運行之後提示我沒找到javamail的類,原來是要ant能找到javamail的類才行
configurations {
antClasspath
}
dependencies {
antClasspath 'ant:ant-javamail:1.+'
antClasspath 'javax.activation:activation:1.1.1'
antClasspath 'javax.mail:mail:1.+'
}
ClassLoader antClassLoader = org.apache.tools.ant.Project.class.classLoader
configurations.antClasspath.each { File jar ->
antClassLoader.addURL( jar.toURI().toURL() )
}複製代碼
而後在gralde裏面執行發送任務
void send(File file) {
getProject().ant.mail(
from: fromMail,// 發件方
tolist: toList,//收件方
ccList: ccList,//抄送方
message: message,//消息內容
subject: subject,//標題
mailhost: mailhost,//SMTP轉發服務器
messagemimetype: "text/html",//消息格式
files: file.getAbsolutePath()//發送文件目錄
)
}複製代碼
這裏有幾個注意點
mailhost
填入不須要SSL 認證的smtp服務器,否則你就須要輸入帳號和密碼才能發送郵件\n
,由於messagemimetype
是html格式,要使用<br>
if (lintClient.haveErrors() ) {
"git reset HEAD~1".execute(null, project.getRootDir())
}複製代碼
我原來看了幾篇Lint原理分析就打算去實現增量掃描,而後發現看和作仍是不同的,中間遇到好多問題,還好gradle插件能夠調試。
第一步 點擊edit configurations
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005
發現android gradle最新的幾個版本對於lint作了一些優化,我順便提一下。
./gradlew lint
會更快,Google實現了LintCharSequence
來完成數據的存儲和傳參,實現了內存中只有一份拷貝lint-report.html
是material design,更好看、更方便查問題回過頭來看,其實增量掃描也很簡單,就一行關鍵性代碼project.addfile(file)
。
最後講一下你們關心的開源問題吧,那要等在公司內部穩定運行之後在公司Github地址開源,畢竟咱們是一款嚴肅的產品嘛。