задний план
Kotlin имеет множество функций, таких как безопасность нулевых указателей, расширение методов, поддержка функционального программирования, богатый синтаксический сахар и т. д. Эти функции делают код Kotlin намного проще и элегантнее, чем Java, что улучшает читабельность и удобство сопровождения кода, экономит время разработки и повышает эффективность разработки. Это также причина, по которой наша команда обратилась к 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, несомненно, неэффективен.
Мы можем уменьшить байтовый код, сгенерированный следующими решениями:
- Для примитивных типов и строк вы можете использовать
const
ключевое слово объявляет константы как константы времени компиляции. - Для публичных полей вы можете использовать
@JvmField
аннотация. - Для других типов констант общедоступные глобальные константы лучше хранить в их собственном объекте основного класса, а не в объекте-компаньоне.
Свойство делегата Lazy()
lazy()
Делегированные свойства можно использовать для ленивой загрузки свойств только для чтения, но при использованииlazy()
Часто упускается из виду, что есть необязательный параметр модели:
- LazyThreadSafetyMode.SYNCHRONIZED: при инициализации свойства выполняется проверка двойной блокировки, чтобы гарантировать, что значение вычисляется только в одном потоке и что все потоки получат одно и то же значение.
- LazyThreadSafetyMode.PUBLICATION: несколько потоков будут выполняться одновременно, и функция для инициализации свойства будет вызываться несколько раз, но в качестве значения делегированного свойства будет использоваться только первое возвращаемое значение.
- LazyThreadSafetyMode.NONE: проверка двойной блокировки не выполняется и не должна использоваться в многопоточности.
lazy()
указан по умолчаниюLazyThreadSafetyMode.SYNCHRONIZED
Это может привести к ненужным издержкам безопасности потока, поэтому следует указать соответствующую модель, чтобы избежать нежелательных синхронных блокировок в соответствии с реальной ситуацией.
Основной тип массива
В Kotlin есть 3 типа массивов:
-
IntArray
,FloatArray
, другое: массив примитивных типов, скомпилированный вint[]
,float[]
,разное -
Array<T>
: массив непустых объектов -
Array<T?>
: Массив объектов, допускающих значение NULL.
Используйте эти три типа для объявления массивов и обнаружения различий между ними:
Эквивалентный код Java:
Оба последних метода упаковывают примитивные типы, что приводит к дополнительным накладным расходам.
Поэтому, когда вам нужно объявить непустой массив примитивных типов, вы должны использовать xxxArray, чтобы избежать автоупаковки.
для цикла
Котлин предоставляетdownTo
,step
,until
,reversed
и другие функции, чтобы облегчить разработчикам использование циклов For. Если одно использование этих функций действительно удобно, лаконично и эффективно, но что, если объединить два из них? Например следующее:
Приведенный выше цикл For используется в сочетании сdownTo
а такжеstep
, то как же реализован эквивалентный код Java?
Сосредоточьтесь на этой строке кода:
IntProgression var10000 = RangesKt.step(RangesKt.downTo(10, 1), 2);
Эта строка кода создает дваIntProgression
Временные объекты, добавляющие дополнительные накладные расходы.
Изучение инструментов проверки Kotlin
Скрытые накладные расходы Kotlin больше, чем перечисленные выше.Чтобы избежать накладных расходов, нам нужно реализовать такой инструмент, чтобы проверять синтаксис Kotlin, перечислять нестандартный код и давать предложения по модификации. В то же время, чтобы убедиться, что код разработчиков проверяется инструментом, весь процесс проверки должен быть автоматизирован.
После дальнейшего рассмотрения правила проверки кода Kotlin должны быть расширяемыми, чтобы другие пользователи могли настраивать свои собственные правила проверки.
Исходя из этого, весь инструмент в основном включает в себя следующие три аспекта:
- Разбор кода Котлина
- Напишите расширяемые пользовательские правила проверки кода
- Автоматизация контроля
В сочетании с потребностью в инструментах после размышлений и анализа данных были определены три альтернативы:
ktlint
ktlintЭто инструмент для проверки стиля кода Kotlin, он отличается от нашего позиционирования и требует большой доработки.
detekt
detektЭто инструмент для статического анализа кода Kotlin, который нам подходит, но не подходит для Android-проектов, например, нельзя указать проверку вариантов. Кроме того, на протяжении всего процесса проверкиkt
Проверяйте файл только один раз, результаты теста (на тот момент) поддерживают только консольный вывод, их нелегко читать.
Модернизированный ворс
Преобразуйте Lint, чтобы увеличить поддержку Lint для проверки кода Kotlin.С одной стороны, функции, предоставляемые Lint, могут полностью удовлетворить наши потребности, и в то же время он может поддерживать проверку файлов ресурсов и файлов классов.С другой стороны, преобразованный Lint очень похож на Lint. Изучите Низкая стоимость, чтобы начать работу.
По сравнению с первыми двумя схемами схема 3 имеет самое высокое соотношение цены и качества, поэтому мы решили преобразовать Lint в плагин Kotlin Lint (KLint).
Во-первых, давайте получим общее представление о рабочем процессе Lint, как показано ниже:
Очевидно, что красный квадрат на рисунке выше нужно изменить, чтобы адаптировать его к Kotlin.Основная работа состоит из следующих трех пунктов:
- Создайте объект KotlinParser для анализа кода Kotlin.
- Получите пакет jar пользовательских правил KLint от aar
- Класс Detector должен определить новый набор методов интерфейса для адаптации вызова при обходе обратного вызова узла 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
объект.
Вызывается при проверке файлов KotlinKtFile.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
Ссылка на реализацию пользовательских правил KLintПрактика пользовательского Lint для AndroidЭта статья.
На рисунке показаны файлы, содержащиеся AAR допустимый, AAR может содержать Lint.jar, который являетсяПрактика пользовательского Lint для AndroidВ этой статье используется метод реализации. ноklint.jar
Его нельзя напрямую поместить в aar, и, конечно же, его не следует помещать вklint.jar
переименован вlint.jar
для достижения цели.
Наконец, программа:
- путем создания
klintrules
Этот пустой аар, будетklint.jar
положить в активы; - Измените код KLint для чтения из активов
klint.jar
; - зависимости проекта
klintrules
Используйте debugCompile, когда используется aar, чтобы не помещатьklint.jar
к пакету выпуска.
Определение методов интерфейса в классе Detector
Поскольку это проверка кода Kotlin, естественно, что класс Detector должен определить новый набор методов интерфейса. Давайте взглянем на методы, предоставляемые правилами проверки кода Java: https://tech.meituan.com/img/Kotlin-code-inspect/4.png)
Я считаю, что студенты, которые написали правила Lint, должны быть хорошо знакомы с описанным выше методом. Чтобы свести к минимуму затраты на обучение написанию правил проверки KLint, мы обращаемся к интерфейсу JavaPsiScanner и определяем набор очень похожих методов интерфейса:
Реализация КЛинт
Благодаря преобразованию трех основных аспектов, приведенных выше, плагин KLint завершен.
Из-за схожести между KLint и Lint плагин KLint прост в использовании:
- Спецификации написания аналогичны Lint (см. код в последнем разделе);
- служба поддержки
@SuppressWarnings("")
и другие аннотации, поддерживаемые Lint; - klintOptions имеет ту же функцию, что и параметры Lint, а именно:
mtKlint {
klintOptions {
abortOnError false
htmlReport true
htmlOutput new File(project.getBuildDir(), "mtKLint.html")
}
}
Автоматизация контроля
-
Есть два варианта автоматической проверки:
- Срабатывает, когда разрабатывается код одноклассника.
pre-commit/push-hook
Проверить, если проверка не удалась, фиксация/пуш не разрешены; - при создании
pull request
Когда сборка CI инициируется для проверки, слияние не допускается, если проверка завершается неудачно.
Вариант 2 здесь более предпочтителен, т.к.
pre-commit/push-hook
в состоянии пройти--no-verify
Обход команды, мы хотим, чтобы весь код Kotlin проходил проверки. - Срабатывает, когда разрабатывается код одноклассника.
Плагин KLint изначально поддерживает./gradlew mtKLint
Команда выполняется, но, учитывая, что почти все проекты выполняют проверку Lint для сборок CI, объединение KLint и Lint вместе экономит затраты на подключение сценариев сборки CI к подключаемым модулям KLint.
С помощью следующего кодаlint task
полагатьсяklint task
, реализация выполняет проверку KLint перед выполнением Lint:
//创建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
}
}
Проверить в режиме реального времени
Хотя автоматизация проверки реализована, можно обнаружить, что сроки выполнения автоматической проверки относительно отстают. Часто разработчики готовятся к объединению кода. В это время модифицировать дорого и рискованно. код. Автоматическая проверка на КИ должна быть последней контрольной точкой для «пропавшей рыбы», а проблема должна быть выявлена в процессе кодирования. На основе этого мы разработали плагин IDE для проверки кода Kotlin в реальном времени.
С помощью этого инструмента можно создавать отчеты об ошибках в режиме реального времени в окне Android Studio, помогая разработчикам находить проблемы и вовремя их решать.
Практика проверки кода Kotlin
Плагин KLint разделен на две части: плагин Gradle и плагин IDE.build.gradle
вводится в последнем черезAndroid Studio
Установите и используйте.
Написание правил KLint
Для выше перечисленныхlazy()中未指定mode
В случае 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 имеют общий набор правил, поэтому приведенные выше правила можно написать один раз и использовать в обоих плагинах одновременно:
- HTML-страница, которая автоматически проверяет соответствующие результаты обнаружения на CI:
- Соответствующее сообщение об ошибке в реальном времени в Android Studio:
Суммировать
С помощью плагина KLint написание правил проверки для ограничения нестандартного кода Kotlin, с одной стороны, позволяет избежать скрытых накладных расходов, повысить производительность кода Kotlin, а с другой стороны, помогает разработчикам лучше понять Kotlin.
использованная литература
об авторе
Чжоу Цзя, главный инженер-разработчик Android в Meituan Dianping, окончил Нанкинский университет информационных технологий в 2016 году. В том же году он присоединился к бизнес-группе Meituan Dianping, занимающейся кейтерингом, и участвовал в ежедневном развитии Dianping Food Channel.