ByteWeaver от OK.TECH это легковесное решение для авторов андроидных приложений и библиотек, которое позволяет им совершать некоторые манипуляции с байткодом во время сборки приложения.
Доклад от автора ByteWeaver на Mobius 2024 очень подробно описывает решение, и содержит исчерпывающее руководство с примерами.
Статья на Хабре от автора ByteWeaver в каком-то смысле повторяет доклад, и содержит те же примеры.
ByteWeaver выполнен в виде плагина для Gradle. В свою очередь ByteWeaver использует инфраструктуру Android Gradle Plugin для того, чтобы встроиться в процесс сборки андроидного приложения или библиотеки. На этапе обработки байт-кода (после компиляции и подключения транзитивных зависимостей, но до обфускации) ByteWeaver обрабатывает классы по одному согласно указанным спецификациям на языке конфигурирования ByteWeaver.
ByteWeawer поддерживает классы, скопмилированные из Java или Kotlin, не важно, однако в случае Kotlin может потребоваться дополнительная работа, чтобы понять, какой байткод сгенерировал компилятор.
В вашем <project>/settings.gradle.kts добавьте репозиторий с проектом ByteWeaver:
pluginManagement {
repositories {
// здесь другие репозитории c вашими зависимостями
maven { setUrl("https://artifactory-external.vkpartner.ru/artifactory/maven/") }
}
}Если вы в вашем проекте уже используете Tracer то этот шаг можно пропустить.
В вашем <project>/<app_module>/build.gradle.kts подключите плагин ByteWeaver актуальной версии:
plugins {
id("ru.ok.byteweaver").version("1.0.0")
}Инструкция для Groovy
Если ваши билд-скрипты написаны на Groovy то инструкция по подключению в целом такая же с поправкой на синтаксис Groovy.
В вашем <project>/settings.gradle добавьте репозиторий с проектом ByteWeaver:
pluginManagement {
repositories {
// другие репозитории c вашими зависимостями
maven { url 'https://artifactory-external.vkpartner.ru/artifactory/maven/' }
}
}В вашем <project>/<app_module>/build.gradle подключите плагин ByteWeaver актуальной версии:
plugins {
id 'ru.ok.byteweaver' version '1.0.0'
}Инструкция для Legacy Groovy
Если вы используете более старую версию Gradle и конструкция plugins вам недоступна, то инструкция по подключению плагина ByyeWeaver несколько отличается.
В вашем корневом <project>/build.gradle добавьте репозиторий и зависимость на модуль с проектом ByteWeaver актуальной версии:
buildscript {
repositories {
maven {
url "https://artifactory-external.vkpartner.ru/artifactory/maven/"
}
}
dependencies {
classpath 'ru.ok.byteweaver:byteweaver-plugin:1.0.0'
}
}В вашем <project>/<app_module>/build.gradle подключите плагин ByteWeaver:
apply plugin 'ru.ok.byteweaver'То, какие ByteWeaver обрабатывает классы и методы, а также какие преобразования он применяет, описывается на языке конфигурации ByteWeaver. Этот несложный язык описан далее, но для того, чтобы конфигурации применились, необходимо указать путь до них плагину.
В вашем <project>/<app_module>/build.gradle.kts (в том, в котором вы подключали плагин) задаем следующий блок:
byteweaver {
create("debug") {
srcFiles += "byteweaver/patch-foo.conf"
}
create("release") {
srcFiles += "byteweaver/patch-bar.conf"
}
}Здесь мы видим, что для build type debug будет использоваться преобразование из файла byteweaver/patch-foo.conf, а для build type release из byteweaver/patch-bar.conf.
Точно также можно задавать несколько преобразований для одного build type или не задавать их вовсе. Если в вашем проекте используются другие build types или flavors, можно задавать конфигурацию и для них.
Инструция для Groovy
Если в вашем проекте билд-скрипты написаны на Groovy то синтаксис слегка отличается.
В вашем <project>/<app_module>/build.gradle (в том, в котором вы подключали плагин) задаем следующий блок:
byteweaver {
debug {
srcFiles += 'byteweaver/patch-foo.conf'
}
release {
srcFiles += 'byteweaver/patch-bar.conf'
}
}В первую очередь нужно описать какие классы подвергаются преобразованиям.
Здесь и далее примеры на языке конфигурации ByteWeaver.
Явно указываем класс io.reactivex.rxjava3.internal.operators.single.SingleFromCallable:
class io.reactivex.rxjava3.internal.operators.single.SingleFromCallable {
}
Все классы, которые наследуют от android.view.View:
class * extends android.view.View {
}
Все классы, которые реализуют java.lang.Runnable (обратите внимание, что используется ключевое слово extends):
class * extends java.lang.Runnable {
}
Любой класс:
class * {
}
Любой класс, который лежит в пакете ru.ok.android (и подпакетах) и аннотирован @SomeAnnotation:
@SomeAnnotation
class ru.ok.android.* {
}
Также в языке конфигурации ByteWeaver поддерживаются импорты:
import ru.ok.android.app.NotificationsLogger;
import java.lang.String;
Более того, импорты обязатьельны (см. java.lang.String). Никакого неявного импорта java.lang.* как в Java и кучи пакетов как в Котлине нет.
Внутри блоков классов нужно указать блоки методов, которые будут обрабатываться ByteWeaver.
Метод класса, наследующего от android.app.Activity, который называется onCreate, принимает android.os.Bundle и ничего не возвращает (ключевое слово void):
class * extends android.app.Activity {
void onCreate(android.os.Bundle) {
}
}
Метод класса, реализующего java.lang.Runnable, который называется run, не имеет аргументов и ничего не возвращает:
class * extends java.lang.Runnable {
void run() {
}
}
Любой метод, в любом классе, вне зависимости от имени, типов аргументов и возвращаемого значения, но аннотированный @ru.ok.android.commons.os.AutoTraceCompat:
class * {
@ru.ok.android.commons.os.AutoTraceCompat
* *(***) {
}
}
Любой метод:
class * {
* *(***) {
}
}
Важная информация, как ByteWeaver обрабатывает методы:
- Не указываются модификаторы видимости
public/protected/private - Не указываются также модификаторы
final/static/synchronized - Совсем-совсем не указываются котлиновские
internal/override - Абстрактные (и интерфейсные) методы пропатчить не получится
- Методы по умолчанию в интерфейсах пропатчить получится и для этого не нужно указывать модификатор
default - Статические методы возможно пропатчить и для этого не нужно указывать модификатор
static - Чтобы пропатчить конструктор используйте имя
<init>и тип возвращаемого значенияvoid - Чтобы пропатчить статический инициализатор класса используйте
void <clinit>()
ByteWeaver позволяет добавлять вызовы методов в начало тела ваших методов.
В любой метод аннотированный @AutoTraceCompat вставить вызов метода androidx.tracing.Trace.beginSection с параметром trace (о нем ниже):
class * {
@ru.ok.android.commons.os.AutoTraceCompat
* *(***) {
before void androidx.tracing.Trace.beginSection(trace);
}
}
Это примерно эквивалентно, как если бы вы вручную переписали класс:
public class Main {
@AutoTraceCompat
public static void main(String[] args) {
System.out.println("Hellow World");
}
}... получили бы:
public class Main {
public static void main(String[] args) {
androidx.tracing.Trace.beginSection("Main.main(String[])");
System.out.println("Hellow World");
}
}Как ByteWeaver вставляет вызовы в начало методов:
- Вставляется всегда вызов статической функции, при этом модификатор
staticуказывать не нужно - Вставляется всегда вызов функции, которая ничего не возвращает, но тип
voidуказывать нужно! - Вызываем либо функцию без параметров, либо с единственным параметром
trace - Параметр
traceимееи типStringи содержит имя вызывающего класса и метода (и типы параметров вызывающего метода) - Значение параметра
traceгенерируется до обработки обфускатором
ByteWeaver позволяет добавлять вызовы методов в конец тела ваших методов.
В конец любого метода аннотированный @AutoTraceCompat вставить вызов метода androidx.tracing.Trace.endSection() с параметром trace (о нем ниже):
class * {
@ru.ok.android.commons.os.AutoTraceCompat
* *(***) {
after void androidx.tracing.Trace.endSection();
}
}
Это примерно эквивалентно, как если бы вы вручную переписали класс:
public class Main {
@AutoTraceCompat
public static void main(String[] args) {
System.out.println();
}
}... получили бы:
public class Main {
public static void main(String[] args) {
try {
System.out.println("Hellow World");
} finally {
androidx.tracing.Trace.endSection();
}
}
}Инструкция для подключения androidx.tracing
В вашем <app>/build.gradle.kts добавьте зависимость:
dependencies {
implementation("androidx.tracing:tracing:1.2.0")
}Как ByteWeaver вставляет вызовы в начало методов:
- Вставляется всегда вызов статической функции, при этом модификатор
staticуказывать не нужно - Вставляется всегда вызов функции, которая ничегно не возвращает, но тип
voidуказывать нужно! - Вызываем строго функцию без параметров
- Вызов будет осуществлен вне зависимости от того, нормально или аварийно завершится вызывающий метод
ByteWeaver позволяет заменять одни вызовы другими.
Везде-везде заменить вызовы NotificationManager.notify на вызовы NotificationsLogger.logNotify:
class * {
* *(***) {
void NotificationManager.notify(int, Notification) {
replace void NotificationsLogger.logNotify(self, 0, 1);
}
}
}
При этом класс NotificationsLogger должен выглядеть как-то так:
public class NotificationsLogger {
public static void logNotify(NotificationManager manager, String tag, int id, Notification notification) {
manager.notify(tag, id, notification);
}
}Как ByteWeaver заменяет вызовы методов:
- На замену всегда вставляется вызов статического метода, при этом модификатор
staticне указывается - Если заменяемый метод не статический, то первый параметр заменяющего метода должен быть всегда
self - Параметр
selfсодержит ссылку на объект, на котором был бы вызван заменяемый метод (не путать сthis, это ссылка на вызывающий объект) - Заменяемый метод может быть статическим, при этом нужно указывать модификатор
staticобязательно! - Если заменяемый метод статический, то первый параметр заменяющего метода не! должен быть
self - Остальные параметры заменяемого метода становятся позиционными параметрами заменяемого и должны быть перечисллены цифрами начиная с 0
