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
вставить вызов метода 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("Hellow World");
}
}
... получили бы:
public class Main {
public static void main(String[] args) {
TraceCompat.beginTraceSection("Main.main(String[])");
System.out.println("Hellow World");
}
}
Как ByteWeaver вставляет вызовы в начало методов:
- Вставляется всегда вызов статической функции, при этом модификатор
static
указывать не нужно - Вставляется всегда вызов функции, которая ничего не возвращает, но тип
void
указывать нужно! - Вызываем либо функцию без параметров, либо с единственным параметром
trace
- Параметр
trace
имееи типString
и содержит имя вызывающего класса и метода (и типы параметров вызывающего метода) - Значение параметра
trace
генерируется до обработки обфускатором
ByteWeaver позволяет добавлять вызовы методов в конец тела ваших методов.
В конец любого метода аннотированный @AutoTraceCompat
вставить вызов метода TraceCompat.beginSection
с параметром trace
(о нем ниже):
class * {
@ru.ok.android.commons.os.AutoTraceCompat
* *(***) {
after void TraceCompat.beginTraceSection(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("Hellow World");
} finally {
TraceCompat.endTraceSection();
}
}
}
Как 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