Рубрики
Без рубрики

Плагин для анализа зависимостей Gradle: Использование анализа байт-кода для поиска неиспользуемых зависимостей

Использование анализа байт-кода для поиска неиспользуемых зависимостей. С тегами gradle, android, java, kotlin.

Это продолжение Надлежащий уход и подача вашей сборки Gradle , в которой я написал о том, как использовать плагин Gradle для анализа зависимостей , чтобы помочь поддерживать работоспособную сборку для ваших проектов Android, Java и Kotlin. В этом первом из серии постов мы обсудим, как работает этот плагин. 1

Для начала мы рассмотрим анализ байт-кода с помощью библиотеки ASM и то, насколько он важен для обнаружения неиспользуемых зависимостей . В будущих публикациях вы можете ожидать обсуждения анализа исходного кода с помощью ANTLR, анализа jar для таких возможностей, как загрузка сервисов и обработка аннотаций, управление зависимостями с помощью API-интерфейсов Gradle и многое другое.

Это сложный вопрос. Давайте начнем с его инвертирования: когда используется зависимость ? И что вообще является зависимостью? Давайте определим это так:

Зависимость. Jar (библиотека Java) или aar (библиотека Android), которая находится в пути к классам компиляции и/или времени выполнения вашего проекта. Он предоставляет набор из нуля или более файлов .class, ресурсов Java, ресурсов Android или компонентов Android (таких как Действия, службы и т. Д.). Может быть внешним или отдельным модулем в вашем проекте.

С этого момента я буду использовать “зависимость” и “библиотеку” взаимозаменяемо.

Теперь, когда мы знаем, что такое зависимость, мы можем выразить, что такое используемая зависимость:

Используемая зависимость. Библиотека, необходимая для компиляции вашего проекта или необходимая вашему проекту во время выполнения.

И поэтому неиспользуемая зависимость – это зависимость, которая вашему проекту не нужна при компиляции или во время выполнения.

Знание того, используется ли зависимость для компиляции

Не хочу вдаваться в подробности, но это сложный вопрос. Чтобы знать, используется ли зависимость во время компиляции, мы должны знать две основные вещи, принципиально:

  1. Что создает зависимость ? Что находится в зависимостях, скомпилированных байт-кодом?
  2. Что потребляет ваш проект ? Что содержится в скомпилированном байт-коде вашего проекта?

В настоящее время основное внимание в плагине уделяется анализу зависимостей во время компиляции. Он выполняет некоторый ограниченный анализ зависимостей во время выполнения (например, ничего, если зависимость предоставляет Java ServiceLoader s), но в остальном исключает этот домен. Таким образом, вы можете увидеть неверный совет (“удалите зависимость X, она не используется”), если эта зависимость используется только во время выполнения. Примером этого может служить случай, когда зависимость используется только через отражение. ( Этот вопрос освещает один такой случай.)

Анализ производителей (библиотек)

Задача DependencyReportTask – это то, что анализирует байт-код всех зависимостей, независимо от того, были ли они объявлены напрямую или были введены транзитивно из непосредственно объявленной зависимости. 2 Здесь мы полагаемся на библиотеку ASM 3 для некоторого базового анализа байт-кода. Вы можете увидеть анализ во всей его красе на asm.kt , но здесь я представлю упрощенный вид.

private fun analyzeJar(artifact: Artifact): AnalyzedJar {
  val zipFile = ZipFile(artifact.file)
  val analyzedClasses = zipFile.entries.toList()
    .filter { it.name.endsWith(".class")
    .map { classEntry ->
      val visitor = MyClassVisitor()
      val reader = zipFile
        .getInputStream(classEntry)
        .use { ClassReader(it.readBytes()) }
      reader.accept(visitor, 0)
      visitor // "return"
    }
    .map { it.analyzeClass() }
  return AnalyzedJar(analyzedClasses)
}

Подпись рассказывает историю: получив артефакт, верните проанализированную банку. Артефакт в данном случае представляет собой пользовательский тип данных, который переносит файл jar на диск. Начиная с этого jar, мы перебираем файлы классов, которые он содержит, посещая каждый из них с помощью реализации Asm ClassVisitor ; в данном случае MyClassVisitor . Этот посетитель создает нечто, называемое “анализируемым классом”, которое представляет собой представление скомпилированного файла класса, содержащего все, что нас волнует. Затем мы возвращаем Проанализированную банку , которая представляет собой оболочку вокруг набора Проанализированных классов . Эта реализация посетителя выглядит следующим образом

class MyClassVisitor : ClassVisitor(ASM8) {
  fun getAnalyzedClass(): AnalyzedClass {
    return AnalyzedClass(
      className = className,
      superClassName = superClassName,
      retentionPolicy = retentionPolicy,
      isAnnotation = isAnnotation,
      hasNoMembers = fieldCount == 0 && methodCount == 0,
      access = access,
      methods = methods,
      innerClasses = innerClasses,
      constantClasses = constantClasses
    )
  }

  override fun visit(
    version: Int, access: Int,
    name: String, signature: String?, superName: String?,
    interfaces: Array?
  ) {
    className = name
    superClassName = superName
    if (interfaces?.contains("java/lang/annotation/Annotation") == true) {
      isAnnotation = true
    }
    this.access = Access.fromInt(access)
  }
}

где остальная часть реализации опущена для краткости, но ее можно увидеть по ссылке выше. Однако вы можете получить представление о том, что имеет значение, исходя из имен параметров. И мы также можем получить представление о том, как работает ASM. Он анализирует байт-код, поэтому нам не нужно этого делать; он посещает каждый узел в файле класса и извлекает из него информацию, чтобы мы, разработчики инструментов, могли оперировать этой информацией на более высоком уровне.

Анализ потребителя (наш проект!)

Следующий шаг – понять, что наш проект, “потребитель”, использует во время компиляции. Это делается с помощью Задачи анализа списка классов , которая принимает в качестве основного входного сигнала — как вы уже догадались — список файлов классов. Результатом выполнения этой задачи является набор строк, которые являются полными именами классов ссылок на классы, которые появляются в байт-коде нашего проекта. Здесь также используется ASM, хотя и другой Посетитель класса реализация, потому что здесь у нас другая потребность. Мы больше не рассматриваем возможности, а скорее необработанные строки, представляющие ссылки на классы, которые появляются в нашем байт-коде. Здесь может помочь понять, что скомпилированный байт-код класса Java не содержит “операторов импорта”, как это делает наш исходный код Java. Вместо этого каждая ссылка на класс полностью соответствует требованиям на сайте использования. Имея это в виду, нам просто нужно посетить каждый узел в файле класса — члены, тела методов, аннотации, аннотации типов и суперкласс (который может быть просто "java/lang/Object" ) — и извлечь ссылки на типы.

Я не буду включать реализацию в этот пост для краткости, но, как всегда, вы можете взглянуть на источник здесь .

Анализ ABI

Вычисление ABI проекта (или двоичного интерфейса приложения) также включает анализ байт-кода. Однако, чтобы сохранить эту дискуссию сфокусированной, я откладываю обсуждение вычисления ABI до будущей статьи.

Связывая это вместе

Мы знаем, что производят производители и что потребляют потребители. Эти два входных данных передаются в задачу Злоупотребление зависимостями (простите меня, я часто красочен в своих соглашениях об именах), которая перебирает все созданные классы и проверяет, используются ли они потребителем. Те, которые не используются, следовательно, ну, в общем, неиспользуемые . Вооруженный этой информацией, плагин теперь может выдать совет “зависимость X не используется вашим проектом, поэтому вы должны удалить ее!”

Пример вывода консоли для проекта Android:

> Task :app:aggregateAdvice
Unused dependencies which should be removed:
- implementation("androidx.appcompat:appcompat:1.1.0-rc01")
- implementation("androidx.core:core-ktx:1.0.1")
- implementation("com.google.dagger:dagger-android-support:2.24")
- implementation(project(":entities"))
- implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72")

Transitively used dependencies that should be declared directly as indicated:
- implementation("org.jetbrains.kotlin:kotlin-stdlib:1.3.72")
- implementation("javax.inject:javax.inject:1")
- implementation("com.google.dagger:dagger:2.24")

Existing dependencies which should be modified to be as indicated:
- api("com.squareup.moshi:moshi:1.8.0") (was implementation)
- api("com.squareup.retrofit2:converter-moshi:2.5.0") (was implementation)
- api(project(":entities")) (was implementation)

Dependencies which could be compile-only:
- compileOnly("androidx.annotation:annotation:1.1.0") (was implementation)

В этом посте мы описали, как использовать ASM для анализа байт-кода проекта и его зависимостей, и как эту информацию можно использовать для определения того, используется ли зависимость. Мы включили ссылки на исходный код, которые довольно тщательно решают эту проблему.

Вот и все, в двух словах. Настройтесь в следующий раз на обсуждение анализа исходного кода с помощью ANTLR .

Одной из самых сложных частей алгоритма “неправильного использования зависимостей”, упомянутого выше, является рекурсивная функция, которую я лаконично назвал ” relate ” и которая настолько плотна, что в итоге мне пришлось написать 26 строк KDoc, чтобы она запомнила, как она работала. На ранней стадии разработки плагина был момент, когда Square не смогла выполнить анализ, потому что плагин просто вращался часами и часами; проверка профиля процессора показала нам, что одной из точек доступа была эта функция relate .

(Да, это совершенно ясно!)

Я инструментировал алгоритм и увидел, что один и тот же узел в графе зависимостей, в случае даже небольшого проекта, посещался потенциально тысячи раз (и каждый раз, выходящий за рамки первого, был дублирующим). Я смог использовать очень простую стратегию кэширования, чтобы пропустить узлы, которые уже были проанализированы, и мне удалось снизить рабочую нагрузку на 99%. Это очень простое исправление означает, что для большинства реальных проектов (вплоть до гаргантюа Square и включая его) производительность превосходна и не должна препятствовать внедрению в ваш проект.

Профили – это мощная штука.

Еще раз спасибо Стефану Николя за отзыв о раннем проекте вместе с профилями, которые я обсуждал выше.

1 Для получения дополнительной информации о возможностях плагина, пожалуйста, смотрите вики . вверх 2 В общем, каждый модуль в вашем проекте является бенефициаром графика зависимостей. На верхнем уровне находятся все зависимости, которые вы объявили напрямую, но ниже находятся переходные зависимости, от которых зависят зависимости верхнего уровня (или прямые); они часто доступны для прямого использования, несмотря на то, что они не объявлены вашим проектом. вверх 3 В традициях прекрасных библиотек с открытым исходным кодом никто не знает, что означает это название. вверх

Оригинал: “https://dev.to/autonomousapps/dependency-analysis-gradle-plugin-using-bytecode-analysis-to-find-unused-dependencies-509n”