revanced-patcher/docs/2_2_patch_anatomy.md
oSumAtrIX 0abf1c6c02
feat: Improve Fingerprint API (#316)
Fingerprints can now be matched easily without adding them to a patch first.

BREAKING CHANGE: Many APIs have been changed.
2024-10-27 16:04:30 +01:00

9.6 KiB
Raw Permalink Blame History


                       

Continuing the legacy of Vanced

🧩 Anatomy of a ReVanced patch

Learn the API to create patches using ReVanced Patcher.

Example patch

The following example patch disables ads in an app.
In the following sections, each part of the patch will be explained in detail.

package app.revanced.patches.ads

val disableAdsPatch = bytecodePatch(
    name = "Disable ads",
    description = "Disable ads in the app.",
) { 
    compatibleWith("com.some.app"("1.0.0"))
    
    // Patches can depend on other patches, executing them first.
    dependsOn(disableAdsResourcePatch)

    // Merge precompiled DEX files into the patched app, before the patch is executed.
    extendWith("disable-ads.rve")
    
    // Business logic of the patch to disable ads in the app.
    execute {
        // Fingerprint to find the method to patch.
        val showAdsMatch by showAdsFingerprint {
          // More about fingerprints on the next page of the documentation.
        }
      
        // In the method that shows ads,
        // call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file)
        // to enable or disable ads.
        showAdsMatch.method.addInstructions(
            0,
            """
                invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z
                move-result v0
                return v0
            """
        )
    }
}

Tip

To see real-world examples of patches, check out the repository for ReVanced Patches.

🧩 Patch API

⚙️ Patch options

Patches can have options to get and set before a patch is executed. Options are useful for making patches configurable. After loading the patches using PatchLoader, options can be set for a patch. Multiple types are already built into ReVanced Patcher and are supported by any application that uses ReVanced Patcher.

To define an option, use the available option functions:

val patch = bytecodePatch(name = "Patch") {
    // Add an inbuilt option and delegate it to a property.
    val value by stringOption(key = "option")

    // Add an option with a custom type and delegate it to a property.
    val string by option<String>(key = "string")
    
    execute {
        println(value)
        println(string)
    }
}

Options of a patch can be set after loading the patches with PatchLoader by obtaining the instance for the patch:

loadPatchesJar(patches).apply {
    // Type is checked at runtime.
    first { it.name == "Patch" }.options["option"] = "Value"
}

The type of an option can be obtained from the type property of the option:

option.type // The KType of the option. Captures the full type information of the option.

Options can be declared outside a patch and added to a patch manually:

val option = stringOption(key = "option")

bytecodePatch(name = "Patch") {
    val value by option()
}

This is useful when the same option is referenced in multiple patches.

🧩 Extensions

An extension is a precompiled DEX file merged into the patched app before a patch is executed. While patches are compile-time constructs, extensions are runtime constructs that extend the patched app with additional classes.

Assume you want to add a complex feature to an app that would need multiple classes and methods:

public class ComplexPatch {
    public static void doSomething() {
        // ...
    }
}

After compiling the above code as a DEX file, you can add the DEX file as a resource in the patches file and use it in a patch:

val patch = bytecodePatch(name = "Complex patch") {
    extendWith("complex-patch.rve")
  
    execute { 
        fingerprint.match!!.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
    }
}

ReVanced Patcher merges the classes from the extension into context.classes before executing the patch. When the patch is executed, it can reference the classes and methods from the extension.

Note

The ReVanced Patches template repository is a template project to create patches and extensions.

Tip

To see real-world examples of extensions, check out the repository for ReVanced Patches.

♻️ Finalization

Patches can have a finalization block called after all patches have been executed, in reverse order of patch execution. The finalization block is called after all patches that depend on the patch have been executed. This is useful for doing post-processing tasks. A simple real-world example would be a patch that opens a resource file of the app for writing. Other patches that depend on this patch can write to the file, and the finalization block can close the file.

val patch = bytecodePatch(name = "Patch") { 
    dependsOn(
        bytecodePatch(name = "Dependency") { 
            execute {
                print("1")
            }

            finalize {
                print("4")
            }
        }
    )

    execute {
        print("2")
    }

    finalize {
        print("3")
    }
}

Because Patch depends on Dependency, first Dependency is executed, then Patch. Finalization blocks are called in reverse order of patch execution, which means, first, the finalization block of Patch, then the finalization block of Dependency is called. The output after executing the patch above would be 1234. The same order is followed for multiple patches depending on the patch.

💡 Additional tips

  • When using PatchLoader to load patches, only patches with a name are loaded. Refer to the inline documentation of PatchLoader for detailed information.
  • Patches can depend on others. Dependencies are executed first. The dependent patch will not be executed if a dependency raises an exception while executing.
  • A patch can declare compatibility with specific packages and versions, but patches can still be executed on any package or version. It is recommended that compatibility is specified to present known compatible packages and versions.
    • If compatibleWith is not used, the patch is treated as compatible with any package
  • If a package is specified with no versions, the patch is compatible with any version of the package
  • If an empty array of versions is specified, the patch is not compatible with any version of the package. This is useful for declaring incompatibility with a specific package.
  • A patch can raise a PatchException at any time of execution to indicate that the patch failed to execute.

⏭️ What's next

The next page explains the concept of fingerprinting in ReVanced Patcher.

Continue: 🔎 Fingerprinting