日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Kotlin代码检查在美团的探索与实践

發(fā)布時間:2025/3/21 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Kotlin代码检查在美团的探索与实践 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

背景

Kotlin有著諸多的特性,比如空指針安全、方法擴展、支持函數(shù)式編程、豐富的語法糖等。這些特性使得Kotlin的代碼比Java簡潔優(yōu)雅許多,提高了代碼的可讀性和可維護性,節(jié)省了開發(fā)時間,提高了開發(fā)效率。這也是我們團隊轉(zhuǎn)向Kotlin的原因,但是在實際的使用過程中,我們發(fā)現(xiàn)看似寫法簡單的Kotlin代碼,可能隱藏著不容忽視的額外開銷。本文剖析了Kotlin的隱藏開銷,并就如何避免開銷進行了探索和實踐。

Kotlin的隱藏開銷

伴生對象

伴生對象通過在類中使用companion object來創(chuàng)建,用來替代靜態(tài)成員,類似于Java中的靜態(tài)內(nèi)部類。所以在伴生對象中聲明常量是很常見的做法,但如果寫法不對,可能就會產(chǎn)生額外開銷。比如下面這段聲明Version常量的代碼:

class Demo {fun getVersion(): Int {return Version}companion object {private val Version = 1} } 復(fù)制代碼

表面上看還算簡潔,但是將這段Kotlin代碼轉(zhuǎn)化成等同的Java代碼后,卻顯得晦澀難懂:

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();}} } 復(fù)制代碼

與Java直接讀取一個常量不同,Kotlin訪問一個伴生對象的私有常量字段需要經(jīng)過以下方法:

  • 調(diào)用伴生對象的靜態(tài)方法
  • 調(diào)用伴生對象的實例方法
  • 調(diào)用主類的靜態(tài)方法
  • 讀取主類中的靜態(tài)字段

為了訪問一個常量,而多花費調(diào)用4個方法的開銷,這樣的Kotlin代碼無疑是低效的。

我們可以通過以下解決方法來減少生成的字節(jié)碼:

  • 對于基本類型和字符串,可以使用const關(guān)鍵字將常量聲明為編譯時常量。
  • 對于公共字段,可以使用@JvmField注解。
  • 對于其他類型的常量,最好在它們自己的主類對象而不是伴生對象中來存儲公共的全局常量。
  • Lazy()委托屬性

    lazy()委托屬性可以用于只讀屬性的惰性加載,但是在使用lazy()時經(jīng)常被忽視的地方就是有一個可選的model參數(shù):

    • LazyThreadSafetyMode.SYNCHRONIZED:初始化屬性時會有雙重鎖檢查,保證該值只在一個線程中計算,并且所有線程會得到相同的值。
    • LazyThreadSafetyMode.PUBLICATION:多個線程會同時執(zhí)行,初始化屬性的函數(shù)會被多次調(diào)用,但是只有第一個返回的值被當(dāng)做委托屬性的值。
    • LazyThreadSafetyMode.NONE:沒有雙重鎖檢查,不應(yīng)該用在多線程下。

    lazy()默認(rèn)情況下會指定LazyThreadSafetyMode.SYNCHRONIZED,這可能會造成不必要線程安全的開銷,應(yīng)該根據(jù)實際情況,指定合適的model來避免不需要的同步鎖。

    基本類型數(shù)組

    在Kotlin中有3種數(shù)組類型:

    • IntArray,FloatArray,其他:基本類型數(shù)組,被編譯成int[],float[],其他
    • Array<T>:非空對象數(shù)組
    • Array<T?>:可空對象數(shù)組

    使用這三種類型來聲明數(shù)組,可以發(fā)現(xiàn)它們之間的區(qū)別:

    等同的Java代碼:

    后面兩種方法都對基本類型做了裝箱處理,產(chǎn)生了額外的開銷。
    所以當(dāng)需要聲明非空的基本類型數(shù)組時,應(yīng)該使用xxxArray,避免自動裝箱。

    for循環(huán)

    Kotlin提供了downTo、step、until、reversed等函數(shù)來幫助開發(fā)者更簡單的使用For循環(huán),如果單一的使用這些函數(shù)確實是方便簡潔又高效,但要是將其中兩個結(jié)合呢?比如下面這樣:

    上面的For循環(huán)中結(jié)合使用了downTo和step,那么等同的Java代碼又是怎么實現(xiàn)的呢?

    重點看這行代碼:

    IntProgression var10000 = RangesKt.step(RangesKt.downTo(10, 1), 2);

    這行代碼就創(chuàng)建了兩個IntProgression臨時對象,增加了額外的開銷。

    Kotlin檢查工具的探索

    Kotlin的隱藏開銷不止上面列舉的幾個,為了避免開銷,我們需要實現(xiàn)這樣一個工具,實現(xiàn)Kotlin語法的檢查,列出不規(guī)范的代碼并給出修改意見。同時為了保證開發(fā)同學(xué)的代碼都是經(jīng)過工具檢查的,整個檢查流程應(yīng)該自動化。

    再進一步考慮,Kotlin代碼的檢查規(guī)則應(yīng)該具有擴展性,方便其他使用方定制自己的檢查規(guī)則。

    基于此,整個工具主要包含下面三個方面的內(nèi)容:

  • 解析Kotlin代碼
  • 編寫可擴展的自定義代碼檢查規(guī)則
  • 檢查自動化
  • 結(jié)合對工具的需求,在經(jīng)過思考和查閱資料之后,確定了三種可供選擇的方案:

    ktlint

    ktlint是一款用來檢查Kotlin代碼風(fēng)格的工具,和我們的工具定位不同,需要經(jīng)過大量的改造工作才行。

    detekt

    detekt是一款用來靜態(tài)分析Kotlin代碼的工具,符合我們的需求,但是不太適合Android工程,比如無法指定variant(變種)檢查。另外,在整個檢查流程中,一份kt文件只能檢查一次,檢查結(jié)果(當(dāng)時)只支持控制臺輸出,不便于閱讀。

    改造Lint

    改造Lint來增加Lint對Kotlin代碼檢查的支持,一方面Lint提供的功能完全可以滿足我們的需求,同時還能支持資源文件和class文件的檢查,另一方面改造后的Lint和Lint很相似,學(xué)習(xí)上手的成本低。

    相對于前兩種方案,方案3的成本收益比最高,所以我們決定改造Lint成Kotlin Lint(KLint)插件。

    先來大致了解下Lint的工作流程,如下圖:

    很顯然,上圖中的紅框部分需要被改造以適配Kotlin,主要工作有以下3點:

    • 創(chuàng)建KotlinParser對象,用來解析Kotlin代碼
    • 從aar中獲取自定義KLint規(guī)則的jar包
    • Detector類需要定義一套新的接口方法來適配遍歷Kotlin節(jié)點回調(diào)時的調(diào)用

    Kotlin代碼解析

    和Java一樣,Kotlin也有自己的抽象語法樹。可惜的是目前還沒有解析Kotlin語法樹的單獨庫,只能通過Kotlin編譯器這個庫中的相關(guān)類來解析。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;}//可忽視,只是將文件轉(zhuǎn)成字符流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;}復(fù)制代碼

    以上這段代碼可以封裝成KotlinParser類,主要作用是將.Kt文件轉(zhuǎn)化成KtFile對象。 在檢查Kotlin文件時調(diào)用KtFile.acceptChildren(KtVisitorVoid)后,KtVisitorVoid便會多次回調(diào)遍歷到的各個節(jié)點(Node)的方法:

    KtVisitorVoid visitorVoid = new KtVisitorVoid(){@Overridepublic void visitClass(@NotNull KtClass klass) {super.visitClass(klass);}@Overridepublic void visitPrimaryConstructor(@NotNull KtPrimaryConstructor constructor) {super.visitPrimaryConstructor(constructor);}@Overridepublic void visitProperty(@NotNull KtProperty property) {super.visitProperty(property);}... }; ktPsiFile.acceptChildren(visitorVoid); 復(fù)制代碼

    自定義KLint規(guī)則的實現(xiàn)

    自定義KLint規(guī)則的實現(xiàn)參考了Android自定義Lint實踐這篇文章。

    上圖展示了aar中允許包含的文件,aar中可以包含lint.jar,這也是Android自定義Lint實踐這篇文章采用的實現(xiàn)方式。但是klint.jar不能直接放入aar中,當(dāng)然更不應(yīng)該將klint.jar重命名成lint.jar來實現(xiàn)目的。

    最后采用的方案是:

  • 通過創(chuàng)建klintrules這個空的aar,將klint.jar放入assets中;
  • 修改KLint代碼實現(xiàn)從assets中讀取klint.jar;
  • 項目依賴klintrulesaar時使用debugCompile來避免把klint.jar帶到release包。
  • Detector類中接口方法的定義

    既然是對Kotlin代碼的檢查,自然Detector類要定義一套新的接口方法。先來看一下Java代碼檢查規(guī)則提供的方法: https://tech.meituan.com/img/Kotlin-code-inspect/4.png)

    相信寫過Lint規(guī)則的同學(xué)對上面的方法應(yīng)該非常熟悉。為了盡量降低KLint檢查規(guī)則編寫的學(xué)習(xí)成本,我們參照J(rèn)avaPsiScanner接口,定義了一套非常相似的接口方法:

    KLint的實現(xiàn)

    通過對上述3個主要方面的改造,完成了KLint插件。

    由于KLint和Lint的相似,KLint插件簡單易上手:

  • 和Lint相似的編寫規(guī)范(參考最后一節(jié)的代碼);
  • 支持@SuppressWarnings("")等Lint支持的注解;
  • 具有和Lint的Options相同功能的klintOptions,如下:
  • mtKlint {klintOptions {abortOnError falsehtmlReport truehtmlOutput new File(project.getBuildDir(), "mtKLint.html")} } 復(fù)制代碼

    檢查自動化

    • 關(guān)于自動檢查有兩個方案:

    • 在開發(fā)同學(xué)commit/push代碼時,觸發(fā)pre-commit/push-hook進行檢查,檢查不通過不允許commit/push;
    • 在創(chuàng)建pull request時,觸發(fā)CI構(gòu)建進行檢查,檢查不通過不允許merge。
    • 這里更偏向于方案2,因為pre-commit/push-hook可以通過--no-verify命令繞過,我們希望所有的Kotlin代碼都是通過檢查的。

    KLint插件本身支持通過./gradlew mtKLint命令運行,但是考慮到幾乎所有的項目在CI構(gòu)建上都會執(zhí)行Lint檢查,把KLint和Lint綁定在一起可以省去CI構(gòu)建腳本接入KLint插件的成本。

    通過以下代碼,將lint task依賴klint task,實現(xiàn)在執(zhí)行Lint之前先執(zhí)行KLint檢查:

    //創(chuàng)建KLint task,并設(shè)置被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 }//創(chuàng)建Klint變種task,并設(shè)置被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} } 復(fù)制代碼

    檢查實時化

    雖然實現(xiàn)了檢查的自動化,但是可以發(fā)現(xiàn)執(zhí)行自動檢查的時機相對滯后,往往是開發(fā)同學(xué)準(zhǔn)備合代碼的時候,這時再去修改代碼成本高并且存在風(fēng)險。CI上的自動檢查應(yīng)該是作為是否有“漏網(wǎng)之魚”的最后一道關(guān)卡,而問題應(yīng)該暴露在代碼編寫的過程中。基于此,我們開發(fā)了Kotlin代碼實時檢查的IDE插件。

    通過這款工具,實現(xiàn)在Android Studio的窗口實時報錯,幫助開發(fā)同學(xué)第一時間發(fā)現(xiàn)問題及時解決。

    Kotlin代碼檢查實踐

    KLint插件分為Gradle插件和IDE插件兩部分,前者在build.gradle中引入,后者通過Android Studio安裝使用。

    KLint規(guī)則的編寫

    針對上面列舉的lazy()中未指定mode的case,KLint實現(xiàn)了對應(yīng)的檢查規(guī)則:

    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)));@Overridepublic List<Class<? extends PsiElement>> getApplicableKtPsiTypes() {return Arrays.asList(KtPropertyDelegate.class);}@Overridepublic KtVisitorVoid createKtPsiVisitor(KotlinContext context) {return new KtVisitorVoid() {@Overridepublic 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.");}}}};} } 復(fù)制代碼

    檢查結(jié)果

    Gradle插件和IDE插件共用一套規(guī)則,所以上面的規(guī)則編寫一次,就可以同時在兩個插件中使用:

    • CI上自動檢查對應(yīng)的檢測結(jié)果的html頁面:

    • Android Studio上對應(yīng)的實時報錯信息:

    總結(jié)

    借助KLint插件,編寫檢查規(guī)則來約束不規(guī)范的Kotlin代碼,一方面避免了隱藏開銷,提高了Kotlin代碼的性能,另一方面也幫助開發(fā)同學(xué)更好的理解Kotlin。

    參考資料

    • Exploring Kotlin's hidden costs
    • Android自定義Lint實踐

    作者介紹

    周佳,美團點評前端Android開發(fā)工程師,2016年畢業(yè)于南京信息工程大學(xué),同年加入美團點評到店餐飲事業(yè)群,參與大眾點評美食頻道的日常開發(fā)工作。

    總結(jié)

    以上是生活随笔為你收集整理的Kotlin代码检查在美团的探索与实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。