アノテーションから保持ルールを生成する

Jetpack Keep を使用すると、リフレクションと Java Native Interface(JNI)の使用を、個別の keep ルールファイルではなく、Kotlin または Java のソースコードにアノテーションとして直接宣言できます。Jetpack Keep Gradle プラグインは、ビルド時にこれらのアノテーションを読み取り、R8 が必要とする keep ルールを生成します。

Jetpack Keep は、コードベースが変更されても R8 の最適化を有効に保つために役立ちます。

  • Keep ルールは、必要なコードの近くに配置されます。リフレクションを使用するコードの名前を変更または削除する場合は、同じ場所でアノテーションを更新します。同期を維持するための個別のルールファイルはありません。

  • 生成されるルールは条件付きです。R8 は、リフレクション呼び出しのターゲットを、呼び出しを行うコードも保持する場合にのみ保持します。呼び出し元のコードが未使用で、R8 がそのコードを削除した場合(オプションのライブラリ機能でよくあるケース)、R8 はターゲットも削除できます。そのため、未使用の機能のルールによって肥大化することはありません。

  • Lint がリフレクションを検出します。このライブラリには、アノテーションが付けられていないリフレクション呼び出しにフラグを設定する lint チェックが含まれており、多くのアノテーションを自動的に追加できます。

リリースビルドをテストしてください。

Jetpack Keep の仕組み

名前でクラスをインスタンス化する関数について考えてみましょう。R8 がコードでクラスが使用されていることを認識できない場合、クラスのコンストラクタが削除され、実行時にアプリがクラッシュします。クラッシュを防ぐには、keep ルールファイルに次のようなルールを追加します。

# Unconditional: R8 keeps this constructor even if it removes the reflection that invokes it.
-keep class com.example.ResourceLocatorImpl {
    <init>();
}

Jetpack Keep では、代わりにリフレクションを実行する関数にアノテーションを付けます。

// ResourceLocatorImpl isn't on this module's compile classpath, so both the
// annotation and the code refer to it by name.
@UsesReflectionToConstruct(
    className = "com.example.ResourceLocatorImpl",
    // Omitting parameterTypes keeps every constructor. An empty array keeps
    // only the no-argument constructor, which the code calls.
    parameterTypes = [],
)
fun loadResourceLocator(): ResourceLocator {
    val implClass = Class.forName("com.example.ResourceLocatorImpl")
    return implClass.getDeclaredConstructor().newInstance() as ResourceLocator
}

ビルド時に、Gradle プラグインはコンストラクタの keep ルールを生成します。 手動で記述した上記のルールとは異なり、生成されたルールは条件付きです。R8 は、最適化されたアプリに loadResourceLocator が残っている場合にのみコンストラクタを保持します。

要件

Jetpack Keep を使用するには、プロジェクトに Android Gradle プラグイン(AGP)8.7 以降が必要です。

Gradle プラグインは、次のモジュール タイプをサポートしています。

モジュール タイプ モジュールが適用するプラグイン 追加の設定
Android アプリ com.android.application なし
Android ライブラリ com.android.library なし
Android ターゲットを含む Kotlin マルチプラットフォーム ライブラリ com.android.kotlin.multiplatform.library なし
JVM ライブラリ(Java または Kotlin) java-library または org.jetbrains.kotlin.jvm JAR 変換を登録する

Jetpack Keep を設定する

Jetpack Keep を使用するには、リフレクションまたは JNI を使用する各モジュールにライブラリと Gradle プラグインの両方を追加します。ライブラリはアノテーションと lint チェックを提供し、プラグインはアノテーションを keep ルールに変換します。

  1. libs.versions.toml ファイルに、ライブラリとプラグインを追加します。ライブラリとプラグインは同じバージョンを共有します。

    [versions]
    # The library and the plugin ship together. Use one version for both so
    # that they stay in sync when you upgrade.
    annotationKeep = "1.0.0-alpha01"
    
    [libraries]
    androidx-annotation-keep = { group = "androidx.annotation.keep", name = "keep", version.ref = "annotationKeep" }
    
    [plugins]
    androidx-annotation-keep = { id = "androidx.annotation.keep", version.ref = "annotationKeep" }
    

    Gradle は Google の Maven リポジトリからプラグインをダウンロードするため、settings.gradle.kts ファイルの pluginManagement リポジトリに google() が含まれていることを確認してください。

  2. ルート build.gradle.kts ファイルで、AGP とともにプラグインを宣言します。適用はしません。

    plugins {
        alias(libs.plugins.android.application) apply false
        // Other plugins.
    
        // The plugin relies on AGP classes that it doesn't bundle, so declare
        // it next to AGP. Declaring it here also gives every module the same
        // plugin version.
        alias(libs.plugins.androidx.annotation.keep) apply false
    }
    
  3. リフレクションまたは JNI を使用する各モジュールの build.gradle.kts ファイルで、プラグインを適用し、ライブラリの依存関係を追加します。

    plugins {
        alias(libs.plugins.android.application)
        // The plugin reads annotations only from this module's own code, so
        // apply it in every module that uses Jetpack Keep.
        alias(libs.plugins.androidx.annotation.keep)
    }
    
    dependencies {
        implementation(libs.androidx.annotation.keep)
    }
    
  4. プロジェクトを Gradle ファイルと同期します。

Android アプリ モジュールのビルド バリアントごとに、プラグインは <variant>ExtractKeepRules タスク(releaseExtractKeepRules など)を登録し、 生成されたルールをそのバリアントの R8 に渡します。ライブラリ モジュールの場合、プラグインは生成されたルールをライブラリの出力にコンシューマ keep ルールとしてパッケージ化します。 JVM ライブラリ モジュールには追加の設定が必要です。

lint でリフレクションを検出する

Jetpack Keep には、次の lint チェックが含まれています。

ReflectionWithoutKeepAnnotations
一致する keep アノテーションがないリフレクション呼び出し(Class.forName、getDeclaredConstructor、getDeclaredMethod、getDeclaredField など)にフラグを設定します。
MissingKeepAnnotationPlugin
androidx.annotation.keep:keep に依存しているが、 androidx.annotation.keepを適用していないモジュールにフラグを設定します。プラグインがないと、ビルドでアノテーションから keep ルールが生成されません。

lint を使用して Android モジュールにアノテーションを付ける手順は次のとおりです。

  1. モジュールで lint を実行して、アノテーションが必要なリフレクション呼び出しを見つけます。

    ./gradlew :app:lint
    
  2. lint が自動的に行える修正を適用します。

    ./gradlew :app:lintFix
    

    lint は、コードがリフレクションを行うクラスとメンバーを特定できる場合、アノテーションを自動的に追加できます。

  3. 追加されたアノテーションごとに、コードの動作と一致していることを確認します。たとえば、パラメータの型が、コードが呼び出すコンストラクタまたはメソッドと一致していることを確認します。

  4. Android Studio で残りの検出結果を解決します。lint がターゲット クラスを特定できない場合、クイック修正によって、完了するための TODO プレースホルダを含むアノテーションが挿入されます。アノテーションを手動で 追加することもできます。

    たとえば、次の関数はクラス名をパラメータとして受け取るため、lint はどのクラスを構築するかを特定できません。クイック修正により、プレースホルダを含むアノテーションが追加されます。

    @UsesReflectionToConstruct(
        className = TODO(),
        parameterTypes = []
    )
    fun createInstance(className: String): Any =
        Class.forName(className).getDeclaredConstructor().newInstance()
    

    プレースホルダを、コードが構築するクラスに置き換えます。クラスがコンパイル時に使用可能な場合は、className ではなく classConstant を使用します。Kotlin では、TODO() を置き換えるまでコードはコンパイルされません。

  5. 既存のルールを削除する前に、プラグインがアノテーションから生成する keep ルールを確認します。アプリ モジュールの場合、リリース バリアントの抽出タスクを実行します。

    ./gradlew :app:releaseExtractKeepRules
    

    プラグインは、ルールを app/build/generated/release/androidx.annotation.keep.rules.pro に書き込みます。ライブラリ モジュールについては、生成されたルールを確認するをご覧ください。

  6. 新しいアノテーションで置き換える keep ルールを keep ルールファイルから削除します。ルールは、lint がフラグを設定しなかったリフレクション(別のモジュールまたはライブラリのリフレクションなど)を対象にしている場合もあります。そのため、削除する前に各ルールを確認してください。

    • -keep class com.example.** { *; } などのパッケージ全体のルールについては、アノテーションまたは他のルールがパッケージのクラスのリフレクションをすべてカバーしていることを確認してください。
    • 特定のクラスまたはメンバーを保持するルールについては、生成されたルールのいずれかと厳密に一致しているか、他のリフレクションがそのルールに依存していないことを確認してください。
  7. リリースビルドをテストします。

コードにアノテーションを付ける

リフレクションを実行するコード(リフレクション呼び出しを含む関数、コンストラクタ、フィールド、クラス)に Jetpack Keep アノテーションを追加します。アノテーションはリフレクションのターゲットを記述します。生成されたルールは、R8 がアノテーション付きコードを保持する場合にのみ、そのターゲットを保持します。

コードの動作に基づいてアノテーションを選択します。

Annotation コードが次の場合に使用
@UsesReflectionToConstruct リフレクションを介してコンストラクタを呼び出します( getDeclaredConstructor().newInstance() など)。
@UsesReflectionToAccessMethod リフレクションを介してメソッドを呼び出します( getMethod や getDeclaredMethod など)。
@UsesReflectionToAccessField リフレクションを介してフィールドを読み取りまたは書き込みます( getField や getDeclaredField など)。
@UnconditionallyKeep アプリの Kotlin または Java コードの外部から呼び出されます( JNI を介してネイティブ コードから呼び出されるなど)。このアノテーションは、条件付きルールを生成する @UsesReflection* アノテーションを使用できない場合にのみ使用してください。

すべての @UsesReflection* アノテーションは、次の規則に従います。

  • `classConstant`classConstant または `className`className でターゲット クラスを特定します。クラスがコンパイル時に使用可能な場合は、classConstant を使用します。コンパイラは参照をチェックし、名前変更などの IDE リファクタリングで更新されます。className は、クラスがコンパイル時に使用できない場合(オプションの依存関係のクラスなど)にのみ使用します。これら 2 つのパラメータは相互に排他的です。
  • 最も具体的なクラスをターゲットにします。生成されたルールは、指定したクラスとそのサブクラスまたはインターフェース実装の一致するメンバーを保持します。クラスが一般的であるほど、R8 はより多くのコードを保持します。
  • パラメータの型を指定して、1 つのオーバーロードを保持します。parameterTypes または parameterTypeNames を設定しない場合、ルールはすべてのコンストラクタまたはメソッドのすべてのオーバーロードを保持します。引数なしのコンストラクタまたはメソッドのみを保持するには、空の配列を渡します。
  • 複数のターゲットに対してアノテーションを繰り返します。1 つの関数が複数のクラスまたはメンバーにリフレクションする場合は、ターゲットごとに 1 つのアノテーションを追加します。

リフレクションを使用してオブジェクトを構築する

コードがリフレクションを介してクラスをインスタンス化する場合は、@UsesReflectionToConstruct を使用します。次の例では、タスク ランナーが StartupTask インターフェースの実装を名前でインスタンス化します。アノテーションでインターフェースが指定されているため、R8 が execute を保持している限り、生成されたルールはすべての StartupTask 実装の引数なしのコンストラクタを保持します。

interface StartupTask {
    fun run()
}

object TaskRunner {
    // The task class is known only at runtime, so target the interface that
    // every task implements.
    @UsesReflectionToConstruct(classConstant = StartupTask::class, parameterTypes = [])
    fun execute(className: String) {
        val task = Class.forName(className)
            .getDeclaredConstructor()
            .newInstance() as StartupTask
        task.run()
    }
}

リフレクションを使用してメソッドを呼び出す

コードがリフレクションを介してメソッドを呼び出す場合は、@UsesReflectionToAccessMethod を使用します。methodName をメソッドの名前に設定します。次の例では、コードがオプションの依存関係の静的メソッドを呼び出します。クラスはコンパイル時に使用できないため、アノテーションでは classConstant ではなく className が使用されます。

object CrashReporting {
    @UsesReflectionToAccessMethod(
        className = "com.example.crashlib.CrashReporter",
        methodName = "log",
        // Omitting parameterTypes keeps every log overload. Keep only the one
        // that the code calls.
        parameterTypes = [String::class],
    )
    fun log(message: String) {
        val reporterClass = try {
            Class.forName("com.example.crashlib.CrashReporter")
        } catch (e: ClassNotFoundException) {
            // Crash reporting is optional, so skip it instead of failing.
            return
        }
        reporterClass.getMethod("log", String::class.java).invoke(null, message)
    }
}

リフレクションを使用してフィールドにアクセスする

コードがリフレクションを介してフィールドを読み取りまたは書き込みする場合は、@UsesReflectionToAccessField を使用します。fieldName をフィールドの名前に設定します。

class UserProfileFetcher {
    // User doesn't expose internalId, so reading it requires reflection.
    @UsesReflectionToAccessField(classConstant = User::class, fieldName = "internalId")
    fun readInternalId(user: User): Any? {
        val field = User::class.java.getDeclaredField("internalId")
        field.isAccessible = true
        return field.get(user)
    }
}

アプリの外部から呼び出されるコードを保持する

@UnconditionallyKeep は、アプリの Kotlin または Java コードの外部から呼び出されるコード(ネイティブ コードが JNI を介して呼び出すメソッドなど)に使用します。呼び出すコードではなく、呼び出される要素にアノテーションを付けます。

class NativeBridge {
    // Only native code calls this method, through a JNI lookup by name that
    // R8 can't see. Keep the method and its name unconditionally.
    @UnconditionallyKeep
    fun onNativeEvent(message: String) {
        Log.d("NativeBridge", "Received from native code: $message")
    }

    // The default proguard-android-optimize.txt file already keeps native
    // method names, so this method doesn't need an annotation.
    external fun startNativeProcess()
}

生成されたルールは、アノテーション付きメソッドとその包含クラスを元の名前で保持します。メソッドのシグネチャが参照する他のクラスは保持されません。ネイティブ コードがこれらのクラスに依存している場合は、それらのクラスにもアノテーションを付けるか、keep ルールを追加します。JNI と R8 の詳細については、JNI を介したネイティブ コードとの連携 をご覧ください。

デフォルトでは、@UnconditionallyKeep は要素の名前を保持します。外部の呼び出し元が名前に依存していない場合は、shouldPreserveName = false を設定して、R8 が要素の名前を変更できるようにします。

生成されたルールを確認する

このセクションでは、モジュールのタイプごとに生成されたルールを確認する方法について説明します。 アプリ モジュールの場合、プラグインはルールを R8 に直接渡します。ライブラリ モジュールの場合、プラグインはルールをライブラリの出力にコンシューマ keep ルールとしてパッケージ化します。ライブラリに依存するアプリは、Jetpack Keep を使用していない場合でも、R8 を実行するとこれらのルールを自動的に適用します。コンシューマ keep ルールの詳細については、ライブラリ作成者向けの最適化をご覧ください。

Android アプリ

プラグインがアプリ モジュール用に生成する keep ルールを確認するには、ビルド バリアントの抽出タスクを実行します。

./gradlew :app:releaseExtractKeepRules

プラグインは、各バリアントのルールを app/build/generated/<variant>/androidx.annotation.keep.rules.pro に書き込みます。各ルールの前の # context: コメントは、そのルールを生成したアノテーション付きコードを示します。

生成されたルールが他の keep ルールとともに最適化にどのように影響するかを確認するには、R8 構成アナライザを使用します。

Android ライブラリ

Android ライブラリ モジュールに追加の設定は必要ありません。AAR をビルドすると、プラグインは、AAR にすでに含まれているコンシューマ keep ルールの後に、生成されたルールを AAR の proguard.txt ファイルに追加します。プラグインは、この処理を行うために、ビルド バリアントごとに <variant>KeepRulesTransformAar タスクを登録します。

ルールを確認するには、AAR をビルドします(例: ./gradlew :mylibrary:assembleRelease)。生成された AAR で proguard.txt を開きます。生成されたルールは、次のヘッダーを含むセクションに表示されます。

#################################################################
#   Rules generated from androidx.annotation.keep annotations   #
#################################################################

macOS と Linux では、コマンドラインからルールを出力することもできます。

unzip -p mylibrary/build/intermediates/aar/release/releaseKeepRulesTransformAar/mylibrary-release.aar proguard.txt

Kotlin マルチプラットフォーム ライブラリ

Android ターゲットを含む Kotlin マルチプラットフォーム ライブラリ モジュールにも追加の設定は必要ありません。Android ターゲットの AAR をビルドすると、 プラグインは Android ライブラリの場合と同じ ように、生成されたルールを AAR の proguard.txt ファイルに追加します。プラグインは、この処理を行うために androidMainKeepRulesTransformAar タスクを登録します。

ルールを確認するには、そのタスクを実行し、作成された AAR で proguard.txt を開きます。macOS と Linux では、コマンドラインからルールを出力できます。

./gradlew :mylibrary:androidMainKeepRulesTransformAar
unzip -p mylibrary/build/intermediates/aar/androidMain/androidMainKeepRulesTransformAar/mylibrary.aar proguard.txt

JVM ライブラリ

JVM ライブラリ モジュールのルールを確認する前に、モジュールを 構成して、プラグインが生成されたルールを JAR のコピー に追加するようにします。次に、登録した変換タスクを実行し、作成された JAR のルールファイルを確認します。たとえば、タスクに keepRulesJar という名前を付けた場合は、macOS と Linux で次のコマンドを使用してルールを出力できます。

./gradlew :mylibrary:keepRulesJar
unzip -p mylibrary/build/intermediates/annotation-keep/keepRulesJar/mylibrary-keep-rules.jar \
    "META-INF/com.android.tools/*" "META-INF/proguard/*"

JVM ライブラリを構成する

JAR を生成する JVM ライブラリ モジュールの場合、プラグインは JAR を自動的に変更しません。代わりに、annotationKeep.registerJavaArchiveTransform 関数を使用して、生成されたルールを含む JAR のコピーを作成するタスクを登録し、その JAR を公開します。ライブラリ モジュールの build.gradle.kts ファイルに次の行を追加します。

plugins {
    id("java-library")
    alias(libs.plugins.jetbrains.kotlin.jvm)
    alias(libs.plugins.androidx.annotation.keep)
}

val jarTask = tasks.named<Jar>("jar")

// Apps run R8 on this library's code, so the generated rules must be inside
// the JAR. The plugin handles this automatically only for Android modules.
val keepRulesJarTask = annotationKeep.registerJavaArchiveTransform(
    taskName = "keepRulesJar",
    input = jarTask.flatMap { it.archiveFile },
    fileName = jarTask.flatMap { it.archiveFileName }.map { it.replace(".jar", "-keep-rules.jar") },
)

// Consumers of this library resolve apiElements and runtimeElements, so add
// the JAR that contains the generated rules to them.
configurations.configureEach {
    if (name == "apiElements" || name == "runtimeElements") {
        outgoing.artifact(keepRulesJarTask.flatMap { it.outputJar })
    }
}

dependencies {
    implementation(libs.androidx.annotation.keep)
}

変換により、生成されたルールが JAR のターゲット R8 ルールの場所 META-INF/com.android.tools/r8/proguard.txt に追加されます。JAR に META-INF/com.android.tools/ または META-INF/proguard/ にルールがすでに含まれている場合、変換では、生成されたルールが既存のルールファイルに追加されます。これらの場所の詳細については、さまざまな R8 バージョンをサポートするをご覧ください。

変換を登録したら、JAR のルールを確認します。

例: オプションのアドオン ライブラリを読み込む

ライブラリでは、リフレクションを使用してオプションのアドオンを読み込むことがよくあります。たとえば、アナリティクス SDK には、コア ライブラリと動画アドオンが同梱されている場合があります。アドオンはコア ライブラリに依存しているため、コア ライブラリはアドオンのクラスを直接参照できません。代わりに、コア ライブラリは実行時にアドオンのクラスを名前で検索し、クラスが見つかった場合にのみ動画分析を有効にします。

コア ライブラリはリフレクションを介してのみアドオンのクラスを使用するため、R8 はアプリのリリースビルドからクラスまたはそのコンストラクタを削除し、検索が失敗する可能性があります。このエラーは、アドオンを含むアプリのリリースビルドでのみ発生するため、テストで検出するのは困難です。

Jetpack Keep を使用しない場合、アドオンは、オプションの依存関係をサポートするためのリフレクションに示すように、クラスのコンシューマ keep ルールを同梱します。

# In the add-on's consumer keep rules file.
-keep class com.example.analytics.video.VideoEventTracker {
    <init>();
}

このルールは、必要な検索とは異なるライブラリにあるため、同期が取れなくなる可能性があります。Jetpack Keep では、代わりにコア ライブラリの検索にアノテーションを付けます。

// In the core library.
interface AnalyticsPlugin {
    fun start()
}

object AnalyticsManager {
    // The annotation and the lookup share this constant so that they can't
    // refer to different classes.
    private const val VIDEO_TRACKER_CLASS =
        "com.example.analytics.video.VideoEventTracker"

    @UsesReflectionToConstruct(
        className = VIDEO_TRACKER_CLASS,
        parameterTypes = [],
    )
    fun initialize() {
        val tracker = try {
            Class.forName(VIDEO_TRACKER_CLASS)
                .getDeclaredConstructor()
                .newInstance() as AnalyticsPlugin
        } catch (e: ClassNotFoundException) {
            // Apps that don't play video leave out the add-on, so a missing
            // class isn't an error.
            return
        }
        // This is an ordinary interface call, not a reflective one, so R8
        // keeps start() without an annotation.
        tracker.start()
    }
}

アノテーションは次のガイドラインに従います。

  • リフレクションを実行するコードにアノテーションを付ける。アノテーションは、アドオンのクラスではなく、Class.forName を呼び出す initialize にあります。
  • コードの動作に基づいてアノテーションを選択する。検索ではクラスのインスタンスが作成されるため、コードでは @UsesReflectionToConstruct が使用されます。
  • 参照できないクラスには className を使用する。アドオンのクラスはコア ライブラリのコンパイル クラスパスにないため、アノテーションで classConstant を使用できません。
  • コードが呼び出すコンストラクタのみを保持する。検索では引数なしのコンストラクタが呼び出されるため、parameterTypes は空の配列になります。

コア ライブラリの AAR をビルドすると、R8 が initialize を保持するたびに、プラグインは VideoEventTracker とその引数なしのコンストラクタを保持するルールを追加します。SDK を使用するアプリは、Jetpack Keep を使用していない場合でも、このルールを自動的に適用するため、アドオンに独自のルールは必要ありません。 アドオンのルールと比較して、このアプローチには次の利点があります。

  • ルールは検索の近くに配置されます。コア ライブラリは検索とルールの両方を所有しているため、2 つのライブラリ間で変更を調整するのではなく、まとめて更新します。
  • R8 は未使用のコードを削除できます。アプリにアドオンが含まれていても initialize を呼び出さない場合、生成されたルールは適用されず、R8 は VideoEventTracker を削除できます。アドオンのルールでは、アドオンを含むすべてのアプリで保持されます。
  • Lint はアノテーションのない検索を検出します。コア ライブラリで後からアノテーションなしで別のリフレクション検索が追加された場合、lint はそれにフラグを設定するため、ライブラリをリリースする前に修正できます。

結果を確認するには、Android ライブラリの説明に従って、生成されたルールが AAR's proguard.txt ファイルに表示されていることを確認します。次に、アドオンを含むアプリのリリースビルドをテストします。ルールがないと、動画分析が中断されるためです。

参考情報