2023-08-07 05:00:41 +03:00

8.9 KiB
Raw Blame History

🧩 Skeleton of a Patch

Patches are what make ReVanced, ReVanced. On the following page the basic structure of a patch will be explained.

Example patch

This page works with the following patch as an example:

package app.revanced.patches.ads.patch

// Imports

@Patch
@Name("Disable ads")
@Description("Disables ads.")
@DependsOn([DisableAdResourcePatch:class])
@Compatibility([Package("com.some.app", arrayOf("0.1.0"))])
@Version("0.0.1")
class DisableAdsPatch : BytecodePatch(
    listOf(LoadAdsFingerprint)
) {
    override fun execute(context: BytecodeContext): PatchResult {
        val result = LoadAdsFingerprint.result
            ?: return PatchResultError("LoadAdsFingerprint not found")

        result.mutableMethod.replaceInstructions(
                0,
                """
                    const/4 v0, 0x1
                    return v0
                """
        )

        return PatchResultSuccess()
    }
}

🔎 Dissecting the example patch

Lets start with understanding, how a patch is structured. A patch is mainly built out of three components:

  1. 📝 Patch annotations

    @Patch
    @Name("Disable Ads")
    @Description("Disables ads.")
    @DependsOn([DisableAdResourcePatch:class])
    @Compatibility([Package("com.some.app", arrayOf("0.1.0"))])
    @Version("0.0.1")
    

    To give context about the patch, annotations are used. They serve different but important purposes:

    • Every visible patch should be annotated with @Patch to be picked up by PatchBundle from the introduction. Patches which are not annotated with @Patch can be referenced by other patches. We refer to those as patch dependencies. Patch dependencies are useful to structure multiple patches.

      Example: To add settings switches to an app, first, a patch is required that can provide a basic framework for other patches to add their toggles to that app. Those patches refer to the dependency patch and use its framework to add their toggles to an app. ReVanced Patcher will execute the dependency and then the patch itself. The dependency can prepare a preference screen when executed and then initialize itself for further use by other patches.

    • Visible patches should be annotated with @Name. This annotation does not serve any functional purpose. Instead, it allows referring to the patch with a name. ReVanced Patches use Sentence casing by convention, but any name can be used for patches. Patches with no @Patch annotation do not require the @Name annotation, because they are only useable as dependencies for other patches, and therefore are not visible through PatchBundle.

    • Visible patches should be annotated with @Description. This annotation serves the same purpose as the annotation @Name. It is used to give the patch a short description.

    • Patches can be annotated with @DependsOn. If the current patch depends on other patches, it can declare them as dependencies.

      Example: The patch to remove ads needs to patch the bytecode. Additionally it makes use of a second patch, to get rid of resource files in the app which show ads in the app.

    • All patches should be annotated with @Compatibility. This annotation is the most complex, but most important one and serves the purpose of constraining a patch to a package. Every patch is compatible with usually one or more packages. Additionally, the constraint can optionally be extended to versions of the package to discourage the use of the patch with versions outside of the constraint.

      Example: The patch disables ads for an app. The app regularly updates and the code of the app mutates heavily. In that case the patch might not be compatible for future, untested versions of the app. To discourage the use of the app with other versions than the versions, this patch was confirmed to work on, it is constrained to those versions only.

    • Patches can be annotated with @Version.

      Currently, this annotation does not serve any purpose, but is added to patches by convention, in case a use case has been found.

    • Annotate a patch with @RequiresIntegrations if it depends on additional integrations to be merged by ReVanced Patcher.

      Integrations are precompiled classes which are useful to off-load and useful for developing complex patches. Details of integrations and what exactly integrations are will be introduced properly on another page.

  2. 🏗️ Patch class

    class DisableAdsPatch : BytecodePatch( /* Parameters */ ) {
      // ...
    }
    

    Usually, patches consist out of a single class. The class can be used to create methods and fields for the patch, or provide a framework for other patches, in case it is meant to be used as a dependency patch.

    ReVanced Patches follow a convention to name the class of patches:

    Example: The class for a patch which disables ads should be called DisableAdsPatch, for a patch which adds a new download feature it should be called DownloadsPatch.

    Each patch implicitly implements the Patch interface when extending off ResourcePatch or BytecodePatch. The current example extends off BytecodePatch:

    class DisableAdsPatch : BytecodePatch( /* Parameters */ ) {
      // ...
    }
    

    If the patch extends off ResourcePatch, it is able to patch resources such as XML, PNG or similar files. On the other hand, if the patche extends off BytecodePatch, it is able to patch the bytecode of an app. If a patch needs access to the resources and the bytecode at the same time. Either can use the other as a dependency. Circular dependencies are unhandled.

  3. 🏁 The execute method

    override fun execute(context: BytecodeContext): PatchResult {
      // ...
    }
    

    The execute method is declared in the Patch interface and therefore part of any patch:

    fun execute(context: /* Omitted */ T): PatchResult
    

    It is the first method executed when running the patch. The current example extends off BytecodePatch. Since patches that extend on it can interact with the bytecode, the signature for the execute method when implemented requires a BytecodeContext as a parameter:

    override fun execute(context: BytecodeContext): PatchResult {
      // ...
    }
    

    The BytecodeContext contains everything necessary related to bytecode for patches, including every class of the app on which the patch will be applied. Likewise, a ResourcePatch will require a ResourceContext parameter and provide the patch with everything necessary to patch resources.

    The execute method has to be returned with PatchResult. Patches may return early with PatchResultError if something went wrong. If this patch is used as a dependency for other patches, those patches will not execute subsequently. If a patch succeeds, PatchResultSuccess must be returned.

    In the current example the execute method runs the following code to replace instructions at the index 0 of the methods instruction list:

    val result = LoadAdsFingerprint.result
      ?: return PatchResultError("LoadAdsFingerprint not found")
    
    result.mutableMethod.replaceInstructions(
            0,
            """
                const/4 v0, 0x1
                return v0
            """
    )
    return PatchResultSuccess()
    

Note

: Details of this implementation and what exactly Fingerprints are will be introduced properly on another page.

🤏 Minimal template for a bytecode patch

package app.revanced.patches.examples.minimal.patch

// Imports

@Patch
@Name("Minimal Demonstration")
@Description("Demonstrates a minimal implementation of a patch.")
@Compatibility([Package("com.some.app")])
class MinimalExamplePatch : BytecodePatch() {
    override fun execute(context: BytecodeContext) {
      println("${MinimalExamplePatch::class.patchName} is being executed." )

      return PatchResultSuccess()
    }
}