1
mirror of https://github.com/revanced/revanced-patcher synced 2025-09-10 05:30:49 +02:00

Compare commits

...

10 Commits

Author SHA1 Message Date
semantic-release-bot
348d0070e7 chore: Release v20.0.1-dev.3 [skip ci]
## [20.0.1-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v20.0.1-dev.2...v20.0.1-dev.3) (2024-10-03)

### Performance Improvements

* Free memory earlier and remove negligible lookup maps ([d53aacd](d53aacdad4))
2024-10-03 14:08:34 +00:00
oSumAtrIX
d53aacdad4 perf: Free memory earlier and remove negligible lookup maps
Negligible lookup maps used for matching fingerprints have been removed to reduce the likelihood of OOM when the maps are instantiated, commonly observed with 400M RAM. Additionally, lookup maps previously kept for the duration of the patcher instance are now cleared before the classes are compiled. This reduces the likelihood of OOM when compiling classes.
On a related note, a linear increase in memory usage is observed with every compiled class until all classes are compiled implying compiled classes not being freed by GC because they are still referenced. After compiling a class, the class is technically free-able though. The classes are assumed to be referenced in the `multidexlib2` library that takes the list of all classes to compile multiple DEX with and seems to hold the reference to all these classes in memory until all DEX are compiled. A clever fix would involve splitting the list of classes into chunks and getting rid of the list of all classes so that after every DEX compilation, the corresponding split of classes can be freed.
2024-10-03 16:06:42 +02:00
semantic-release-bot
f1615b7ab5 chore: Release v20.0.1-dev.2 [skip ci]
## [20.0.1-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v20.0.1-dev.1...v20.0.1-dev.2) (2024-10-01)
2024-10-01 15:31:49 +00:00
oSumAtrIX
ffb1d880d7 ci: Use permissions and regular GitHub token instead of PAT 2024-10-01 17:25:49 +02:00
oSumAtrIX
e95f13ae3e build(Needs bump): Update dependencies 2024-09-30 23:21:44 +02:00
oSumAtrIX
e1b984d601 ci: Adjust release commit message 2024-09-30 22:34:23 +02:00
semantic-release-bot
c2dc29e061 chore(release): 20.0.1-dev.1 [skip ci]
## [20.0.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v20.0.0...v20.0.1-dev.1) (2024-09-18)

### Bug Fixes

* Check for class type exactly instead of with contains ([#310](https://github.com/ReVanced/revanced-patcher/issues/310)) ([69f2f20](69f2f20fd9))
2024-09-18 12:38:03 +00:00
oSumAtrIX
69f2f20fd9 fix: Check for class type exactly instead of with contains (#310) 2024-09-18 14:36:15 +02:00
Pg
525beda18e docs: Fix code example (#306) 2024-08-29 08:59:33 +02:00
oSumAtrIX
73d3cbf4ff build: Bump Gradle Wrapper to 8.9 2024-08-06 17:46:39 +02:00
17 changed files with 1954 additions and 1247 deletions

View File

@@ -10,6 +10,9 @@ on:
jobs: jobs:
release: release:
name: Release name: Release
permissions:
contents: write
packages: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -46,5 +49,5 @@ jobs:
- name: Release - name: Release
env: env:
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm exec semantic-release run: npm exec semantic-release

View File

@@ -23,7 +23,8 @@
"assets": [ "assets": [
"CHANGELOG.md", "CHANGELOG.md",
"gradle.properties" "gradle.properties"
] ],
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
} }
], ],
[ [

View File

@@ -1,3 +1,19 @@
## [20.0.1-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v20.0.1-dev.2...v20.0.1-dev.3) (2024-10-03)
### Performance Improvements
* Free memory earlier and remove negligible lookup maps ([d53aacd](https://github.com/ReVanced/revanced-patcher/commit/d53aacdad4ed3750ddae526fb307577ea36e6171))
## [20.0.1-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v20.0.1-dev.1...v20.0.1-dev.2) (2024-10-01)
## [20.0.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v20.0.0...v20.0.1-dev.1) (2024-09-18)
### Bug Fixes
* Check for class type exactly instead of with contains ([#310](https://github.com/ReVanced/revanced-patcher/issues/310)) ([69f2f20](https://github.com/ReVanced/revanced-patcher/commit/69f2f20fd99162f91cd9c531dfe47d00d3152ead))
# [20.0.0](https://github.com/ReVanced/revanced-patcher/compare/v19.3.1...v20.0.0) (2024-08-06) # [20.0.0](https://github.com/ReVanced/revanced-patcher/compare/v19.3.1...v20.0.0) (2024-08-06)

View File

@@ -108,7 +108,7 @@ val rawResourcePatch = rawResourcePatch {
} }
@Surpress("unused") @Surpress("unused")
val resourcePatch = rawResourcePatch { val resourcePatch = resourcePatch {
execute { execute {
// TODO // TODO
} }

View File

@@ -1,3 +1,3 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
version = 20.0.0 version = 20.0.1-dev.3

Binary file not shown.

View File

@@ -1,6 +1,8 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dist zipStorePath=wrapper/dists

22
gradlew vendored
View File

@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,7 +85,9 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@@ -201,11 +205,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command:
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# shell script including quotes and variable substitutions, so put them in # and any embedded shellness will be escaped.
# double quotes to make sure that they get re-expanded; and # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# * put everything else in single quotes, so that it's not re-expanded. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \

22
gradlew.bat vendored
View File

@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

2972
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"@saithodev/semantic-release-backmerge": "^4.0.1", "@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"gradle-semantic-release-plugin": "^1.9.1", "gradle-semantic-release-plugin": "^1.10.1",
"semantic-release": "^23.0.2" "semantic-release": "^24.1.2"
} }
} }

View File

@@ -66,36 +66,15 @@ class Fingerprint internal constructor(
} }
// TODO: If only one string is necessary, why not use a single string for every fingerprint? // TODO: If only one string is necessary, why not use a single string for every fingerprint?
fun Fingerprint.lookupByStrings() = strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] } if (strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }?.let(::match) == true) {
if (lookupByStrings()?.let(::match) == true) {
return true return true
} }
// No strings declared or none matched (partial matches are allowed). context.classes.forEach { classDef ->
// Use signature matching. if (match(context, classDef)) return true
fun Fingerprint.lookupBySignature(): MethodClassPairs {
if (accessFlags == null) return lookupMaps.allMethods
var returnTypeValue = returnType
if (returnTypeValue == null) {
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) {
// Constructors always have void return type.
returnTypeValue = "V"
} else {
return lookupMaps.allMethods
}
}
val signature =
buildString {
append(accessFlags)
append(returnTypeValue.first())
appendParameters(parameters ?: return@buildString)
}
return lookupMaps.methodsBySignature[signature] ?: return MethodClassPairs()
} }
return match(lookupBySignature())
return false
} }
/** /**
@@ -128,7 +107,7 @@ class Fingerprint internal constructor(
fun match( fun match(
context: BytecodePatchContext, context: BytecodePatchContext,
method: Method, method: Method,
) = match(context, method, context.classByType(method.definingClass)!!.immutableClass) ) = match(context, method, context.classBy { method.definingClass == it.type }!!.immutableClass)
/** /**
* Match using a [Method]. * Match using a [Method].

View File

@@ -98,14 +98,12 @@ class Patcher(private val config: PatcherConfig) : Closeable {
logger.info("Merging extensions") logger.info("Merging extensions")
context.executablePatches.forEachRecursively { patch -> with(context.bytecodeContext) {
if (patch is BytecodePatch && patch.extension != null) { context.executablePatches.mergeExtensions()
context.bytecodeContext.merge(patch.extension)
}
}
// Initialize lookup maps. // Initialize lookup maps.
context.bytecodeContext.lookupMaps lookupMaps
}
logger.info("Executing patches") logger.info("Executing patches")

View File

@@ -20,6 +20,7 @@ class PatcherConfig(
private val temporaryFilesPath: File = File("revanced-temporary-files"), private val temporaryFilesPath: File = File("revanced-temporary-files"),
aaptBinaryPath: String? = null, aaptBinaryPath: String? = null,
frameworkFileDirectory: String? = null, frameworkFileDirectory: String? = null,
@Deprecated("This is going to be removed in the future because it is not needed anymore.")
internal val multithreadingDexFileWriter: Boolean = false, internal val multithreadingDexFileWriter: Boolean = false,
) { ) {
private val logger = Logger.getLogger(PatcherConfig::class.java.name) private val logger = Logger.getLogger(PatcherConfig::class.java.name)

View File

@@ -21,7 +21,6 @@ import lanchon.multidexlib2.MultiDexIO
import lanchon.multidexlib2.RawDexIO import lanchon.multidexlib2.RawDexIO
import java.io.Closeable import java.io.Closeable
import java.io.FileFilter import java.io.FileFilter
import java.io.InputStream
import java.util.* import java.util.*
import java.util.logging.Logger import java.util.logging.Logger
@@ -60,40 +59,41 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
internal val lookupMaps by lazy { LookupMaps(classes) } internal val lookupMaps by lazy { LookupMaps(classes) }
/** /**
* A map for lookup by [merge]. * Merge the extensions for this set of patches.
*/ */
internal val classesByType = mutableMapOf<String, ClassDef>().apply { internal fun Set<Patch<*>>.mergeExtensions() {
classes.forEach { classDef -> put(classDef.type, classDef) } // Lookup map for fast checking if a class exists by its type.
} val classesByType = mutableMapOf<String, ClassDef>().apply {
classes.forEach { classDef -> put(classDef.type, classDef) }
}
/** forEachRecursively { patch ->
* Merge an extension to [classes]. if (patch is BytecodePatch && patch.extension != null) {
*
* @param extensionInputStream The input stream of the extension to merge.
*/
internal fun merge(extensionInputStream: InputStream) {
val extension = extensionInputStream.readAllBytes()
RawDexIO.readRawDexFile(extension, 0, null).classes.forEach { classDef -> val extension = patch.extension.readAllBytes()
val existingClass = classesByType[classDef.type] ?: run {
logger.fine("Adding class \"$classDef\"")
classes += classDef RawDexIO.readRawDexFile(extension, 0, null).classes.forEach { classDef ->
classesByType[classDef.type] = classDef val existingClass = classesByType[classDef.type] ?: run {
logger.fine("Adding class \"$classDef\"")
return@forEach classes += classDef
} classesByType[classDef.type] = classDef
logger.fine("Class \"$classDef\" exists already. Adding missing methods and fields.") return@forEach
}
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass -> logger.fine("Class \"$classDef\" exists already. Adding missing methods and fields.")
// If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) { existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
return@let // If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) {
return@let
}
classes -= existingClass
classes += mergedClass
}
} }
classes -= existingClass
classes += mergedClass
} }
} }
} }
@@ -104,6 +104,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
* @param type The type of the class. * @param type The type of the class.
* @return A proxy for the first class that matches the type. * @return A proxy for the first class that matches the type.
*/ */
@Deprecated("Use classBy { type in it.type } instead.", ReplaceWith("classBy { type in it.type }"))
fun classByType(type: String) = classBy { type in it.type } fun classByType(type: String) = classBy { type in it.type }
/** /**
@@ -144,6 +145,9 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
override fun get(): Set<PatcherResult.PatchedDexFile> { override fun get(): Set<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files") logger.info("Compiling patched dex files")
// Free up memory before compiling the dex files.
lookupMaps.close()
val patchedDexFileResults = val patchedDexFileResults =
config.patchedFiles.resolve("dex").also { config.patchedFiles.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty. it.deleteRecursively() // Make sure the directory is empty.
@@ -177,21 +181,6 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
* @param classes The list of classes to create the lookup maps from. * @param classes The list of classes to create the lookup maps from.
*/ */
internal class LookupMaps internal constructor(classes: List<ClassDef>) : Closeable { internal class LookupMaps internal constructor(classes: List<ClassDef>) : Closeable {
/**
* Classes associated by their type.
*/
internal val classesByType = classes.associateBy { it.type }.toMutableMap()
/**
* All methods and the class they are a member of.
*/
internal val allMethods = MethodClassPairs()
/**
* Methods associated by its access flags, return type and parameter.
*/
internal val methodsBySignature = MethodClassPairsLookupMap()
/** /**
* Methods associated by strings referenced in it. * Methods associated by strings referenced in it.
*/ */
@@ -202,22 +191,6 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
classDef.methods.forEach { method -> classDef.methods.forEach { method ->
val methodClassPair: MethodClassPair = method to classDef val methodClassPair: MethodClassPair = method to classDef
// For fingerprints with no access or return type specified.
allMethods += methodClassPair
val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first()
// Add <access><returnType> as the key.
methodsBySignature[accessFlagsReturnKey] = methodClassPair
// Add <access><returnType>[parameters] as the key.
methodsBySignature[
buildString {
append(accessFlagsReturnKey)
appendParameters(method.parameterTypes)
},
] = methodClassPair
// Add strings contained in the method as the key. // Add strings contained in the method as the key.
method.instructionsOrNull?.forEach instructions@{ instruction -> method.instructionsOrNull?.forEach instructions@{ instruction ->
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) { if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) {
@@ -258,15 +231,12 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
} }
override fun close() { override fun close() {
allMethods.clear()
methodsBySignature.clear()
methodsByStrings.clear() methodsByStrings.clear()
} }
} }
override fun close() { override fun close() {
lookupMaps.close() lookupMaps.close()
classesByType.clear()
classes.clear() classes.clear()
} }
} }

View File

@@ -179,7 +179,9 @@ internal object ClassMerger {
callback: MutableClass.() -> Unit, callback: MutableClass.() -> Unit,
) { ) {
callback(targetClass) callback(targetClass)
this.classByType(targetClass.superclass ?: return)?.mutableClass?.let {
targetClass.superclass ?: return
this.classBy { targetClass.superclass == it.type }?.mutableClass?.let {
traverseClassHierarchy(it, callback) traverseClassHierarchy(it, callback)
} }
} }

View File

@@ -6,7 +6,9 @@ import app.revanced.patcher.util.ProxyClassList
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import io.mockk.every import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
@@ -193,6 +195,7 @@ internal object PatcherTest {
private operator fun Set<Patch<*>>.invoke(): List<PatchResult> { private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {
every { patcher.context.executablePatches } returns toMutableSet() every { patcher.context.executablePatches } returns toMutableSet()
every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes) every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes)
every { with(patcher.context.bytecodeContext) { any<Set<Patch<*>>>().mergeExtensions() } } just runs
return runBlocking { patcher().toList() } return runBlocking { patcher().toList() }
} }