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

背景

Kotlin有着诸多的特性,比如空指针安全、方法扩展、支持函数式编程、丰富的语法糖等。这些特性使得Kotlin的代码比Java简洁优雅许多,提高了代码的可读性和可维护性,节省了开发时间,提高了开发效率。这也是我们团队转向Kotlin的原因,但是在实际的使用过程中,我们发现看似写法简单的Kotlin代码,可能隐藏着不容忽视的额外开销。本文剖析了Kotlin的隐藏开销,并就如何避免开销进行了探索和实践。

Kotlin的隐藏开销

伴生对象

伴生对象通过在类中使用companion object来创建,用来替代静态成员,类似于Java中的静态内部类。所以在伴生对象中声明常量是很常见的做法,但如果写法不对,可能就会产生额外开销。比如下面这段声明Version常量的代码:

class Demo {fun getVersion(): Int {return Version}companion object {private val Version = 1}
}

表面上看还算简洁,但是将这段Kotlin代码转化成等同的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();}}
}

与Java直接读取一个常量不同,Kotlin访问一个伴生对象的私有常量字段需要经过以下方法:

  • 调用伴生对象的静态方法
  • 调用伴生对象的实例方法
  • 调用主类的静态方法
  • 读取主类中的静态字段

为了访问一个常量,而多花费调用4个方法的开销,这样的Kotlin代码无疑是低效的。

我们可以通过以下解决方法来减少生成的字节码:

  1. 对于基本类型和字符串,可以使用const关键字将常量声明为编译时常量。
  2. 对于公共字段,可以使用@JvmField注解。
  3. 对于其他类型的常量,最好在它们自己的主类对象而不是伴生对象中来存储公共的全局常量。

Lazy()委托属性

lazy()委托属性可以用于只读属性的惰性加载,但是在使用lazy()时经常被忽视的地方就是有一个可选的model参数:

  • LazyThreadSafetyMode.SYNCHRONIZED:初始化属性时会有双重锁检查,保证该值只在一个线程中计算,并且所有线程会得到相同的值。
  • LazyThreadSafetyMode.PUBLICATION:多个线程会同时执行,初始化属性的函数会被多次调用,但是只有第一个返回的值被当做委托属性的值。
  • LazyThreadSafetyMode.NONE:没有双重锁检查,不应该用在多线程下。

lazy()默认情况下会指定LazyThreadSafetyMode.SYNCHRONIZED,这可能会造成不必要线程安全的开销,应该根据实际情况,指定合适的model来避免不需要的同步锁。

基本类型数组

在Kotlin中有3种数组类型:

  • IntArrayFloatArray,其他:基本类型数组,被编译成int[]float[],其他
  • Array<T>:非空对象数组
  • Array<T?>:可空对象数组

使用这三种类型来声明数组,可以发现它们之间的区别:

等同的Java代码:

后面两种方法都对基本类型做了装箱处理,产生了额外的开销。

所以当需要声明非空的基本类型数组时,应该使用xxxArray,避免自动装箱。

For循环

Kotlin提供了downTostepuntilreversed等函数来帮助开发者更简单的使用For循环,如果单一的使用这些函数确实是方便简洁又高效,但要是将其中两个结合呢?比如下面这样:

上面的For循环中结合使用了downTostep,那么等同的Java代码又是怎么实现的呢?

重点看这行代码:

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

这行代码就创建了两个IntProgression临时对象,增加了额外的开销。

Kotlin检查工具的探索

Kotlin的隐藏开销不止上面列举的几个,为了避免开销,我们需要实现这样一个工具,实现Kotlin语法的检查,列出不规范的代码并给出修改意见。同时为了保证开发同学的代码都是经过工具检查的,整个检查流程应该自动化。

再进一步考虑,Kotlin代码的检查规则应该具有扩展性,方便其他使用方定制自己的检查规则。

基于此,整个工具主要包含下面三个方面的内容:

  1. 解析Kotlin代码
  2. 编写可扩展的自定义代码检查规则
  3. 检查自动化

结合对工具的需求,在经过思考和查阅资料之后,确定了三种可供选择的方案:

ktlint

ktlint 是一款用来检查Kotlin代码风格的工具,和我们的工具定位不同,需要经过大量的改造工作才行。

detekt

detekt 是一款用来静态分析Kotlin代码的工具,符合我们的需求,但是不太适合Android工程,比如无法指定variant(变种)检查。另外,在整个检查流程中,一份kt文件只能检查一次,检查结果(当时)只支持控制台输出,不便于阅读。

改造Lint

改造Lint来增加Lint对Kotlin代码检查的支持,一方面Lint提供的功能完全可以满足我们的需求,同时还能支持资源文件和class文件的检查,另一方面改造后的Lint和Lint很相似,学习上手的成本低。

相对于前两种方案,方案3的成本收益比最高,所以我们决定改造Lint成Kotlin Lint(KLint)插件。

先来大致了解下Lint的工作流程,如下图:

很显然,上图中的红框部分需要被改造以适配Kotlin,主要工作有以下3点:

  • 创建KotlinParser对象,用来解析Kotlin代码
  • 从aar中获取自定义KLint规则的jar包
  • Detector类需要定义一套新的接口方法来适配遍历Kotlin节点回调时的调用

Kotlin代码解析

和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(){@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);

自定义KLint规则的实现

自定义KLint规则的实现参考了Android自定义Lint实践这篇文章。

上图展示了aar中允许包含的文件,aar中可以包含lint.jar,这也是Android自定义Lint实践这篇文章采用的实现方式。但是klint.jar不能直接放入aar中,当然更不应该将klint.jar重命名成lint.jar来实现目的。

最后采用的方案是:

  1. 通过创建klintrules这个空的aar,将klint.jar放入assets中;
  2. 修改KLint代码实现从assets中读取klint.jar
  3. 项目依赖klintrulesaar时使用debugCompile来避免把klint.jar带到release包。

Detector类中接口方法的定义

既然是对Kotlin代码的检查,自然Detector类要定义一套新的接口方法。先来看一下Java代码检查规则提供的方法:

相信写过Lint规则的同学对上面的方法应该非常熟悉。为了尽量降低KLint检查规则编写的学习成本,我们参照JavaPsiScanner接口,定义了一套非常相似的接口方法:

KLint的实现

通过对上述3个主要方面的改造,完成了KLint插件。

由于KLint和Lint的相似,KLint插件简单易上手:

  1. 和Lint相似的编写规范(参考最后一节的代码);
  2. 支持@SuppressWarnings("")等Lint支持的注解;
  3. 具有和Lint的Options相同功能的klintOptions,如下:
mtKlint {klintOptions {abortOnError falsehtmlReport truehtmlOutput new File(project.getBuildDir(), "mtKLint.html")}
}

检查自动化

  • 关于自动检查有两个方案:

    1. 在开发同学commit/push代码时,触发pre-commit/push-hook进行检查,检查不通过不允许commit/push;
    2. 在创建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的窗口实时报错,帮助开发同学第一时间发现问题及时解决。

Kotlin代码检查实践

KLint插件分为Gradle插件和IDE插件两部分,前者在build.gradle中引入,后者通过Android Studio安装使用。

KLint规则的编写

针对上面列举的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)));@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.");}}}};}
}

检查结果

Gradle插件和IDE插件共用一套规则,所以上面的规则编写一次,就可以同时在两个插件中使用:

  • CI上自动检查对应的检测结果的HTML页面:

  • Android Studio上对应的实时报错信息:

总结

借助KLint插件,编写检查规则来约束不规范的Kotlin代码,一方面避免了隐藏开销,提高了Kotlin代码的性能,另一方面也帮助开发同学更好的理解Kotlin。

参考资料

  • Exploring Kotlin’s hidden costs
  • Android自定义Lint实践

作者介绍

  • 周佳,美团前端Android开发工程师,2016年毕业于南京信息工程大学,同年加入美团到店餐饮事业群,参与大众点评美食频道的日常开发工作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/478859.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

推荐几个出论文的好方向!

如果你准备发AI方向的论文&#xff0c;或准备从事科研工作或已在企业中担任AI算法岗的工作。那么我真诚的向大家推荐&#xff0c;贪心学院《机器学习高阶训练营》&#xff0c;目前全网上应该找不到类似体系化的课程。课程精选了四大主题进行深入的剖析讲解&#xff0c;四个模块…

开源开放 | 疾病科室、心血管系统疾病知识图谱发布,助力电子病历系统建设...

本文转载自公众号&#xff1a;OMAHA联盟。 资源发布OMAHA已建立“七巧板”医学术语集、“汇知”医学知识图谱、HiTA ICD编码服务、白皮书等资源&#xff0c;将于每月发布其中的一项或多项资源&#xff0c;敬请关注&#xff01;2009年&#xff0c;《中共中央国务院关于深化医药…

Android官方开发文档Training系列课程中文版:布局性能优化之布局层级优化

原文地址&#xff1a;http://android.xsoftlab.net/training/improving-layouts/index.html 引言 布局是直接影响用户体验的关键部分。如果实现的不好&#xff0c;那么布局很有可能会导致内存的紧张。Android的SDK包含的一些工具可以用来检查布局性能上的问题。结合本章的课程…

LeetCode 389. 找不同(位运算)

1. 题目 给定两个字符串 s 和 t&#xff0c;它们只包含小写字母。 字符串 t 由字符串 s 随机重排&#xff0c;然后在随机位置添加一个字母。 请找出在 t 中被添加的字母。 2. 解题 2.1 土办法&#xff0c;哈希map class Solution { public:char findTheDifference(string …

UAS-点评侧用户行为检索系统

背景 随着整个中国互联网下半场的到来&#xff0c;用户红利所剩无几&#xff0c;原来粗放式的发展模式已经行不通&#xff0c;企业的发展越来越趋向于精耕细作。美团的价值观提倡以客户为中心&#xff0c;面对海量的用户行为数据&#xff0c;如何利用好这些数据&#xff0c;并通…

面试官如何判断面试者的机器学习水平?

文 | 陈然知乎本文已获作者授权&#xff0c;禁止二次转载记得这大概是个三年前的问题&#xff0c;每年都会有新的答案让我持续学习。三年多前我作为最早的机器学习工程师之一加入 Tubi&#xff0c;从零开始设计招聘题目和流程&#xff0c;搭建团队&#xff0c;陆陆续续也面试了…

论文浅尝 - CVPR2020 | 基于网格特征的可视问答系统

论文笔记整理&#xff1a;李爽&#xff0c;天津大学。链接&#xff1a;https://arxiv.org/pdf/2001.03615v1.pdf动机随着“自下而上”注意力的普及&#xff0c;基于边界框(或区域)的视觉特征最近已经超越了传统的基于网格的卷积特征&#xff0c;成为视觉和语言任务的事实标准。…

:批量制作档案表,要从excel表格中将每个人的数据导入到docx档案

https://www.pythonf.cn/read/149081 Python自动将Excel数据填充到word的指定位置,Word,中 具体代码如下&#xff1a; #!/usr/bin/env python3 # -*- coding: utf-8 -*- from docxtpl import DocxTemplate from openpyxl import load_workbook import osdef replace(obj):if o…

LeetCode 1078. Bigram 分词

1. 题目 给出第一个词 first 和第二个词 second&#xff0c;考虑在某些文本 text 中可能以 “first second third” 形式出现的情况&#xff0c;其中 second 紧随 first 出现&#xff0c;third 紧随 second 出现。 对于每种这样的情况&#xff0c;将第三个词 “third” 添加到…

深度学习在OCR中的应用

背景 计算机视觉是利用摄像机和电脑代替人眼&#xff0c;使得计算机拥有类似于人类的对目标进行检测、识别、理解、跟踪、判别决策的功能。以美团业务为例&#xff0c;在商家上单、团单展示、消费评价等多个环节都会涉及计算机视觉的应用&#xff0c;包括文字识别、图片分类、目…

千呼万唤始出来——GPT-3终于开源!

文 | 小戏编 | 小轶GPT3终于开源&#xff01;不过&#xff0c;不是官方开的&#xff08;别打我Eleuther AI推出的名为GPT-Neo的开源项目&#xff0c;于今晨4点于twitter正式宣布&#xff1a;已经开源了复现版GPT-3的模型参数&#xff08;1.3B和2.7B级别&#xff09;&#xff0c…

论文浅尝 - AAAI2020 | 迈向建立多语言义元知识库:用于 BabelNet Synsets 义元预测...

论文笔记整理&#xff1a;潘锐&#xff0c;天津大学硕士。来源&#xff1a;AAAI 2020链接&#xff1a;https://arxiv.org/pdf/1912.01795.pdf摘要义原被定义为人类语言的最小语义单位。义原知识库&#xff08;KBs&#xff09;是一种包含义原标注词汇的知识库&#xff0c;它已成…

美团外卖iOS多端复用的推动、支撑与思考

前言 美团外卖2013年11月开始起步&#xff0c;随后高速发展&#xff0c;不断刷新多项行业记录。截止至2018年5月19日&#xff0c;日订单量峰值已超过2000万&#xff0c;是全球规模最大的外卖平台。业务的快速发展对技术支撑提出了更高的要求。为线上用户提供高稳定的服务体验&a…

论文浅尝 - WWW2020 | 从自然语言交互中提取开放意图

论文笔记整理&#xff1a;娄东方&#xff0c;浙江大学博士后&#xff0c;研究方向为事件抽取。Vedula N, Lipka N, Maneriker P, et al. Open Intent Extraction from Natural Language Interactions[C]//Proceedings of The Web Conference 2020. 2020: 2009-2020.来源&#x…

深度学习在文本领域的应用

背景 近几年以深度学习技术为核心的人工智能得到广泛的关注&#xff0c;无论是学术界还是工业界&#xff0c;它们都把深度学习作为研究应用的焦点。而深度学习技术突飞猛进的发展离不开海量数据的积累、计算能力的提升和算法模型的改进。本文主要介绍深度学习技术在文本领域的应…

LeetCode 1009. 十进制整数的反码(位运算)

1. 题目 每个非负整数 N 都有其二进制表示。例如&#xff0c; 5 可以被表示为二进制 “101”&#xff0c;11 可以用二进制 “1011” 表示&#xff0c;依此类推。注意&#xff0c;除 N 0 外&#xff0c;任何二进制表示中都不含前导零。 二进制的反码表示是将每个 1 改为 0 且…

新分类!全总结!最新Awesome-SLU-Survey资源库开源!

文 | 哈工大SCIR 覃立波、谢天宝等指导老师 | 哈工大SCIR 车万翔教授简介口语语言理解&#xff08;Spoken Language Understanding&#xff0c;SLU&#xff09;作为任务型对话系统的核心组件&#xff0c;目的是为了获取用户询问语句的框架语义表示&#xff08;semantics frame&…

技术实践 | 用 NetworkX + Gephi + Nebula Graph 分析权力的游戏人物关系(上篇)

本文转载自公众号&#xff1a;Nebula Graph Community 。我们都知道《权利的游戏》在全世界都很多忠实的粉丝&#xff0c;除去你永远不知道剧情下一秒谁会挂这种意外“惊喜”&#xff0c;当中复杂交错的人物关系也是它火爆的原因之一&#xff0c;而本文介绍如何通过 NetworkX 访…

美团外卖Android Crash治理之路

Crash率是衡量一个App好坏的重要指标之一&#xff0c;如果你忽略了它的存在&#xff0c;它就会愈演愈烈&#xff0c;最后造成大量用户的流失&#xff0c;进而给公司带来无法估量的损失。本文讲述美团外卖Android客户端团队在将App的Crash率从千分之三做到万分之二过程中所做的大…

全栈深度学习第7期: 研究方向这么多,哪些是有有趣又潜力的呢?

一起追剧鸭简介Berkeley全栈深度学习追剧计划是由夕小瑶的卖萌屋发起的优质公开课打卡项目&#xff0c;通过微信群为同期追剧的小伙伴提供交流平台。关于该计划的详请见这里。Berkeley深度学习追剧群目前已有1000小伙伴加入&#xff0c;公众号后台回复口令 深度学习追剧 入群。…