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年毕业于南京信息工程大学,同年加入美团点评到店餐饮事业群,参与大众点评美食频道的平常开发工做。