Kotlin有着諸多的特性,好比空指針安全、方法擴展、支持函數式編程、豐富的語法糖等。這些特性使得Kotlin的代碼比Java簡潔優雅許多,提升了代碼的可讀性和可維護性,節省了開發時間,提升了開發效率。這也是咱們團隊轉向Kotlin的緣由,可是在實際的使用過程當中,咱們發現看似寫法簡單的Kotlin代碼,可能隱藏着不容忽視的額外開銷。本文剖析了Kotlin的隱藏開銷,並就如何避免開銷進行了探索和實踐。html
伴生對象經過在類中使用companion object
來建立,用來替代靜態成員,相似於Java中的靜態內部類。因此在伴生對象中聲明常量是很常見的作法,但若是寫法不對,可能就會產生額外開銷。好比下面這段聲明Version
常量的代碼:前端
class Demo { fun getVersion(): Int { return Version } companion object { private val Version = 1 } }
表面上看還算簡潔,可是將這段Kotlin代碼轉化成等同的Java代碼後,卻顯得晦澀難懂:android
public class Demo { private static final int Version = 1; public static final Demo.Companion Companion = new Demo.Companion(); public final int getVersion() { return Companion.access$getVersion$p(Companion); } public static int access$getVersion$cp() { return Version; } public static final class Companion { private static int access$getVersion$p(Companion companion) { return companion.getVersion(); } private int getVersion() { return Demo.access$getVersion$cp(); } } }
與Java直接讀取一個常量不一樣,Kotlin訪問一個伴生對象的私有常量字段須要通過如下方法:git
爲了訪問一個常量,而多花費調用4個方法的開銷,這樣的Kotlin代碼無疑是低效的。github
咱們能夠經過如下解決方法來減小生成的字節碼:express
const
關鍵字將常量聲明爲編譯時常量。@JvmField
註解。lazy()
委託屬性能夠用於只讀屬性的惰性加載,可是在使用lazy()
時常常被忽視的地方就是有一個可選的model參數:編程
lazy()
默認狀況下會指定LazyThreadSafetyMode.SYNCHRONIZED
,這可能會形成沒必要要線程安全的開銷,應該根據實際狀況,指定合適的model來避免不須要的同步鎖。api
在Kotlin中有3種數組類型:數組
IntArray
,FloatArray
,其餘:基本類型數組,被編譯成int[]
,float[]
,其餘Array<T>
:非空對象數組Array<T?>
:可空對象數組使用這三種類型來聲明數組,能夠發現它們之間的區別:安全
等同的Java代碼:
後面兩種方法都對基本類型作了裝箱處理,產生了額外的開銷。
因此當須要聲明非空的基本類型數組時,應該使用xxxArray,避免自動裝箱。
Kotlin提供了downTo
、step
、until
、reversed
等函數來幫助開發者更簡單的使用For循環,若是單一的使用這些函數確實是方便簡潔又高效,但要是將其中兩個結合呢?好比下面這樣:
上面的For循環中結合使用了downTo
和step
,那麼等同的Java代碼又是怎麼實現的呢?
重點看這行代碼:
IntProgression var10000 = RangesKt.step(RangesKt.downTo(10, 1), 2);
這行代碼就建立了兩個IntProgression
臨時對象,增長了額外的開銷。
Kotlin的隱藏開銷不止上面列舉的幾個,爲了不開銷,咱們須要實現這樣一個工具,實現Kotlin語法的檢查,列出不規範的代碼並給出修改意見。同時爲了保證開發同窗的代碼都是通過工具檢查的,整個檢查流程應該自動化。
再進一步考慮,Kotlin代碼的檢查規則應該具備擴展性,方便其餘使用方定製本身的檢查規則。
基於此,整個工具主要包含下面三個方面的內容:
結合對工具的需求,在通過思考和查閱資料以後,肯定了三種可供選擇的方案:
ktlint是一款用來檢查Kotlin代碼風格的工具,和咱們的工具定位不一樣,須要通過大量的改造工做才行。
detekt是一款用來靜態分析Kotlin代碼的工具,符合咱們的需求,可是不太適合Android工程,好比沒法指定variant(變種)檢查。另外,在整個檢查流程中,一份kt
文件只能檢查一次,檢查結果(當時)只支持控制檯輸出,不便於閱讀。
改造Lint來增長Lint對Kotlin代碼檢查的支持,一方面Lint提供的功能徹底能夠知足咱們的需求,同時還能支持資源文件和class文件的檢查,另外一方面改造後的Lint和Lint很類似,學習上手的成本低。
相對於前兩種方案,方案3的成本收益比最高,因此咱們決定改造Lint成Kotlin Lint(KLint)插件。
先來大體瞭解下Lint的工做流程,以下圖:
很顯然,上圖中的紅框部分須要被改造以適配Kotlin,主要工做有如下3點:
和Java同樣,Kotlin也有本身的抽象語法樹。惋惜的是目前尚未解析Kotlin語法樹的單獨庫,只能經過Kotlin編譯器這個庫中的相關類來解析。KLint用的是kotlin-compiler-embeddable:1.1.2-5
庫。
public KtFile parseKotlinToPsi(@NonNull File file) { try { org.jetbrains.kotlin.com.intellij.openapi.project.Project ktProject = KotlinCoreEnvironment.Companion.createForProduction(() -> { }, new CompilerConfiguration(), CollectionsKt.emptyList()).getProject(); this.psiFileFactory = PsiFileFactory.getInstance(ktProject); return (KtFile) psiFileFactory.createFileFromText(file.getName(), KotlinLanguage.INSTANCE, readFileToString(file, "UTF-8")); } catch (IOException e) { e.printStackTrace(); } return null; } //可忽視,只是將文件轉成字符流 public static String readFileToString(File file, String encoding) throws IOException { FileInputStream stream = new FileInputStream(file); String result = null; try { result = readInputStreamToString(stream, encoding); } finally { try { stream.close(); } catch (IOException e) { // ignore } } return result; }
以上這段代碼能夠封裝成KotlinParser
類,主要做用是將.Kt
文件轉化成KtFile
對象。 在檢查Kotlin文件時調用KtFile.acceptChildren(KtVisitorVoid)
後,KtVisitorVoid
便會屢次回調遍歷到的各個節點(Node)的方法:
KtVisitorVoid visitorVoid = new KtVisitorVoid(){ @Override public void visitClass(@NotNull KtClass klass) { super.visitClass(klass); } @Override public void visitPrimaryConstructor(@NotNull KtPrimaryConstructor constructor) { super.visitPrimaryConstructor(constructor); } @Override public void visitProperty(@NotNull KtProperty property) { super.visitProperty(property); } ... }; ktPsiFile.acceptChildren(visitorVoid);
自定義KLint規則的實現參考了Android自定義Lint實踐這篇文章。
上圖展現了aar中容許包含的文件,aar中能夠包含lint.jar,這也是Android自定義Lint實踐這篇文章採用的實現方式。可是klint.jar
不能直接放入aar中,固然更不該該將klint.jar
重命名成lint.jar
來實現目的。
最後採用的方案是:
klintrules
這個空的aar,將klint.jar
放入assets中;klint.jar
;klintrules
aar時使用debugCompile來避免把klint.jar
帶到release包。既然是對Kotlin代碼的檢查,天然Detector類要定義一套新的接口方法。先來看一下Java代碼檢查規則提供的方法: https://tech.meituan.com/img/Kotlin-code-inspect/4.png)
相信寫過Lint規則的同窗對上面的方法應該很是熟悉。爲了儘可能下降KLint檢查規則編寫的學習成本,咱們參照JavaPsiScanner接口,定義了一套很是類似的接口方法:
經過對上述3個主要方面的改造,完成了KLint插件。
因爲KLint和Lint的類似,KLint插件簡單易上手:
@SuppressWarnings("")
等Lint支持的註解;mtKlint { klintOptions { abortOnError false htmlReport true htmlOutput new File(project.getBuildDir(), "mtKLint.html") } }
關於自動檢查有兩個方案:
pre-commit/push-hook
進行檢查,檢查不經過不容許commit/push;pull request
時,觸發CI構建進行檢查,檢查不經過不容許merge。這裏更偏向於方案2,由於pre-commit/push-hook
能夠經過--no-verify
命令繞過,咱們但願全部的Kotlin代碼都是經過檢查的。
KLint插件自己支持經過./gradlew mtKLint
命令運行,可是考慮到幾乎全部的項目在CI構建上都會執行Lint檢查,把KLint和Lint綁定在一塊兒能夠省去CI構建腳本接入KLint插件的成本。
經過如下代碼,將lint task
依賴klint task
,實如今執行Lint以前先執行KLint檢查:
//建立KLint task,並設置被Lint task依賴 KLint klintTask = project.getTasks().create(String.format(TASK_NAME, ""), KLint.class, new KLint.GlobalConfigAction(globalScope, null, KLintOptions.create(project))) Set<Task> lintTasks = project.tasks.findAll { it.name.toLowerCase().equals("lint") } lintTasks.each { lint -> klintTask.dependsOn lint.taskDependencies.getDependencies(lint) lint.dependsOn klintTask } //建立Klint變種task,並設置被Lint變種task依賴 for (Variant variant : androidProject.variants) { klintTask = project.getTasks().create(String.format(TASK_NAME, variant.name.capitalize()), KLint.class, new KLint.GlobalConfigAction(globalScope, variant, KLintOptions.create(project))) lintTasks = project.tasks.findAll { it.name.startsWith("lint") && it.name.toLowerCase().endsWith(variant.name.toLowerCase()) } lintTasks.each { lint -> klintTask.dependsOn lint.taskDependencies.getDependencies(lint) lint.dependsOn klintTask } }
雖然實現了檢查的自動化,可是能夠發現執行自動檢查的時機相對滯後,每每是開發同窗準備合代碼的時候,這時再去修改代碼成本高而且存在風險。CI上的自動檢查應該是做爲是否有「漏網之魚」的最後一道關卡,而問題應該暴露在代碼編寫的過程當中。基於此,咱們開發了Kotlin代碼實時檢查的IDE插件。
經過這款工具,實如今Android Studio的窗口實時報錯,幫助開發同窗第一時間發現問題及時解決。
KLint插件分爲Gradle插件和IDE插件兩部分,前者在build.gradle
中引入,後者經過Android Studio
安裝使用。
針對上面列舉的lazy()中未指定mode
的case,KLint實現了對應的檢查規則:
public class LazyDetector extends Detector implements Detector.KtPsiScanner { public static final Issue ISSUE = Issue.create( "Lazy Warning", "Missing specify `lazy` mode ", "see detail: https://wiki.sankuai.com/pages/viewpage.action?pageId=1322215247", Category.CORRECTNESS, 6, Severity.ERROR, new Implementation( LazyDetector.class, EnumSet.of(Scope.KOTLIN_FILE))); @Override public List<Class<? extends PsiElement>> getApplicableKtPsiTypes() { return Arrays.asList(KtPropertyDelegate.class); } @Override public KtVisitorVoid createKtPsiVisitor(KotlinContext context) { return new KtVisitorVoid() { @Override public void visitPropertyDelegate(@NotNull KtPropertyDelegate delegate) { boolean isLazy = false; boolean isSpeifyMode = false; KtExpression expression = delegate.getExpression(); if (expression != null) { PsiElement[] psiElements = expression.getChildren(); for (PsiElement psiElement : psiElements) { if (psiElement instanceof KtNameReferenceExpression) { if ("lazy".equals(((KtNameReferenceExpression) psiElement).getReferencedName())) { isLazy = true; } } else if (psiElement instanceof KtValueArgumentList) { List<KtValueArgument> valueArguments = ((KtValueArgumentList) psiElement).getArguments(); for (KtValueArgument valueArgument : valueArguments) { KtExpression argumentValue = valueArgument.getArgumentExpression(); if (argumentValue != null) { if (argumentValue.getText().contains("SYNCHRONIZED") || argumentValue.getText().contains("PUBLICATION") || argumentValue.getText().contains("NONE")) { isSpeifyMode = true; } } } } } if (isLazy && !isSpeifyMode) { context.report(ISSUE, expression,context.getLocation(expression.getContext()), "Specify the appropriate thread safety mode to avoid locking when it’s not needed."); } } } }; } }
Gradle插件和IDE插件共用一套規則,因此上面的規則編寫一次,就能夠同時在兩個插件中使用:
藉助KLint插件,編寫檢查規則來約束不規範的Kotlin代碼,一方面避免了隱藏開銷,提升了Kotlin代碼的性能,另外一方面也幫助開發同窗更好的理解Kotlin。
周佳,美團點評前端Android開發工程師,2016年畢業於南京信息工程大學,同年加入美團點評到店餐飲事業羣,參與大衆點評美食頻道的平常開發工做。