8.9 KiB
🧩 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:
-
📝 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 byPatchBundle
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 throughPatchBundle
. -
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.
-
-
🏗️ 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 calledDownloadsPatch
.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 asXML
,PNG
or similar files. On the other hand, if the patche extends offBytecodePatch
, 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. -
🏁 The
execute
methodoverride fun execute(context: BytecodeContext): PatchResult { // ... }
The
execute
method is declared in thePatch
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, aResourcePatch
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 withPatchResultError
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 index0
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()
}
}