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 вашими зависимостями
mavenCentral()
}
}Если вы в вашем проекте уже используете Tracer, то этот шаг можно пропустить.
В вашем <project>/<app_module>/build.gradle.kts подключите плагин ByteWeaver актуальной версии:
plugins {
id("ru.ok.byteweaver").version("1.1.0")
}Инструкция для Groovy
Если ваши билд-скрипты написаны на Groovy, то инструкция по подключению в целом такая же с поправкой на синтаксис Groovy.
В вашем <project>/settings.gradle добавьте репозиторий с проектом ByteWeaver:
pluginManagement {
repositories {
// другие репозитории c вашими зависимостями
mavenCentral()
}
}В вашем <project>/<app_module>/build.gradle подключите плагин ByteWeaver актуальной версии:
plugins {
id 'ru.ok.byteweaver' version '1.1.0'
}Инструкция для Legacy Groovy
Если вы используете более старую версию Gradle и конструкция plugins вам недоступна, то инструкция по подключению плагина ByyeWeaver несколько отличается.
В вашем корневом <project>/build.gradle добавьте репозиторий и зависимость на модуль с проектом ByteWeaver актуальной версии:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'ru.ok.byteweaver:byteweaver-plugin:1.1.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 вставить вызов метода TraceCompat.beginSection с параметром trace (о нем ниже):
class * {
@ru.ok.android.commons.os.AutoTraceCompat
* *(***) {
before void TraceCompat.beginTraceSection(trace);
}
}
Это примерно эквивалентно, как если бы вы вручную переписали класс:
public class Main {
@AutoTraceCompat
public static void main(String[] args) {
System.out.println("Hello World");
}
}... получили бы:
public class Main {
public static void main(String[] args) {
TraceCompat.beginTraceSection("Main.main(String[])");
System.out.println("Hello World");
}
}Как ByteWeaver вставляет вызовы в начало методов:
- Вставляется всегда вызов статической функции, при этом модификатор
staticуказывать не нужно - Вставляется всегда вызов функции, которая ничего не возвращает, но тип
voidуказывать нужно! - Параметр
traceимеет типStringи содержит имя вызывающего класса и метода (и типы параметров вызывающего метода) - Значение параметра
traceгенерируется до обработки обфускатором - Параметр
this— этот объект (как в java) - Позиционные параметры
0,1,2и т.д. — соответствующие параметры метода, в который встраивается вызовbefore
ByteWeaver позволяет добавлять вызовы методов в конец тела ваших методов.
В конец любого метода аннотированного @AutoTraceCompat вставить вызов метода TraceCompat.endTraceSection
class * {
@ru.ok.android.commons.os.AutoTraceCompat
* *(***) {
after void TraceCompat.endTraceSection();
}
}
Это примерно эквивалентно, как если бы вы вручную переписали класс:
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 {
TraceCompat.endTraceSection();
}
}
}При необходимости в конец метода можно добавить код, использующий параметрtrace :
class * {
@ru.ok.android.commons.os.AutoTraceCompat
* *(***) {
after void SomeLogger.logAfter(trace);
}
}
Это примерно эквивалентно, как если бы вы вручную переписали класс:
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("Hello World");
} finally {
SomeLogger.logAfter("Main.main(String[])");
}
}
}При этом класс SomeLogger должен выглядеть как-то так:
public class SomeLogger {
private static final String AFTER_PREFIX = "AFTER: ";
private static void log(String tag, String msg) {
System.out.println(tag + " " + msg);
}
public static void logAfter(String msg) {
log(AFTER_PREFIX, msg);
}
}Как ByteWeaver вставляет вызовы в конец методов:
- Вставляется всегда вызов статической функции, при этом модификатор
staticуказывать не нужно - Вставляется всегда вызов функции, которая ничего не возвращает, но тип
voidуказывать нужно! - Вызываем строго функцию без параметров, либо с параметром
trace - Вызов будет осуществлен вне зависимости от того, нормально или аварийно завершится вызывающий метод
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
