mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2024-11-10 09:08:04 +01:00
perf: decode resources only when necessary
This commit is contained in:
parent
98ce0abfa9
commit
3ba4be240b
3 changed files with 192 additions and 149 deletions
|
@ -1,7 +1,6 @@
|
||||||
package app.revanced.patcher
|
package app.revanced.patcher
|
||||||
|
|
||||||
import app.revanced.patcher.data.Data
|
import app.revanced.patcher.data.Data
|
||||||
import app.revanced.patcher.data.PackageMetadata
|
|
||||||
import app.revanced.patcher.data.impl.findIndexed
|
import app.revanced.patcher.data.impl.findIndexed
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.dependencies
|
import app.revanced.patcher.extensions.PatchExtensions.dependencies
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.deprecated
|
import app.revanced.patcher.extensions.PatchExtensions.deprecated
|
||||||
|
@ -47,84 +46,30 @@ private val NAMER = BasicDexFileNamer()
|
||||||
class Patcher(private val options: PatcherOptions) {
|
class Patcher(private val options: PatcherOptions) {
|
||||||
private val logger = options.logger
|
private val logger = options.logger
|
||||||
private val opcodes: Opcodes
|
private val opcodes: Opcodes
|
||||||
|
private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY
|
||||||
val data: PatcherData
|
val data: PatcherData
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val version = VersionReader.read()
|
val version = VersionReader.read()
|
||||||
|
private fun BuildOptions.setBuildOptions(options: PatcherOptions) {
|
||||||
|
this.aaptPath = options.aaptPath
|
||||||
|
this.useAapt2 = true
|
||||||
|
this.frameworkFolderLocation = options.frameworkFolderLocation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val extInputFile = ExtFile(options.inputFile)
|
|
||||||
try {
|
|
||||||
val outDir = File(options.resourceCacheDirectory)
|
|
||||||
if (outDir.exists()) {
|
|
||||||
logger.info("Deleting existing resource cache directory")
|
|
||||||
outDir.deleteRecursively()
|
|
||||||
}
|
|
||||||
outDir.mkdirs()
|
|
||||||
|
|
||||||
val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) })
|
|
||||||
val resourceTable = androlib.getResTable(extInputFile, true)
|
|
||||||
|
|
||||||
val packageMetadata = PackageMetadata()
|
|
||||||
|
|
||||||
if (options.patchResources) {
|
|
||||||
logger.info("Decoding resources")
|
|
||||||
|
|
||||||
// decode resources to cache directory
|
|
||||||
androlib.decodeManifestWithResources(extInputFile, outDir, resourceTable)
|
|
||||||
androlib.decodeResourcesFull(extInputFile, outDir, resourceTable)
|
|
||||||
|
|
||||||
// read additional metadata from the resource table
|
|
||||||
packageMetadata.metaInfo.usesFramework = UsesFramework().also { framework ->
|
|
||||||
framework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
|
|
||||||
}
|
|
||||||
|
|
||||||
packageMetadata.metaInfo.doNotCompress = buildList {
|
|
||||||
androlib.recordUncompressedFiles(extInputFile, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
logger.info("Only decoding AndroidManifest.xml because resource patching is disabled")
|
|
||||||
|
|
||||||
// create decoder for the resource table
|
|
||||||
val decoder = ResAttrDecoder()
|
|
||||||
decoder.currentPackage = ResPackage(resourceTable, 0, null)
|
|
||||||
|
|
||||||
// create xml parser with the decoder
|
|
||||||
val axmlParser = AXmlResourceParser()
|
|
||||||
axmlParser.attrDecoder = decoder
|
|
||||||
|
|
||||||
// parse package information with the decoder and parser which will set required values in the resource table
|
|
||||||
// instead of decodeManifest another more low level solution can be created to make it faster/better
|
|
||||||
XmlPullStreamDecoder(
|
|
||||||
axmlParser, AndrolibResources().resXmlSerializer
|
|
||||||
).decodeManifest(
|
|
||||||
extInputFile.directory.getFileInput("AndroidManifest.xml"), nullOutputStream
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
packageMetadata.packageName = resourceTable.currentResPackage.name
|
|
||||||
packageMetadata.packageVersion = resourceTable.versionInfo.versionName
|
|
||||||
packageMetadata.metaInfo.versionInfo = resourceTable.versionInfo
|
|
||||||
packageMetadata.metaInfo.sdkInfo = resourceTable.sdkInfo
|
|
||||||
|
|
||||||
logger.info("Reading dex files")
|
logger.info("Reading dex files")
|
||||||
|
|
||||||
// read dex files
|
// read dex files
|
||||||
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
|
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
|
||||||
// get the opcodes
|
// get the opcodes
|
||||||
opcodes = dexFile.opcodes
|
opcodes = dexFile.opcodes
|
||||||
|
|
||||||
// finally create patcher data
|
// finally create patcher data
|
||||||
data = PatcherData(
|
data = PatcherData(dexFile.classes.toMutableList(), options.resourceCacheDirectory)
|
||||||
dexFile.classes.toMutableList(), options.resourceCacheDirectory, packageMetadata
|
|
||||||
)
|
// decode manifest file
|
||||||
} finally {
|
decodeResources(ResourceDecodingMode.MANIFEST_ONLY)
|
||||||
extInputFile.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -175,7 +120,8 @@ class Patcher(private val options: PatcherOptions) {
|
||||||
val metaInfo = packageMetadata.metaInfo
|
val metaInfo = packageMetadata.metaInfo
|
||||||
var resourceFile: File? = null
|
var resourceFile: File? = null
|
||||||
|
|
||||||
if (options.patchResources) {
|
when (resourceDecodingMode) {
|
||||||
|
ResourceDecodingMode.FULL -> {
|
||||||
val cacheDirectory = ExtFile(options.resourceCacheDirectory)
|
val cacheDirectory = ExtFile(options.resourceCacheDirectory)
|
||||||
try {
|
try {
|
||||||
val androlibResources = AndrolibResources().also { resources ->
|
val androlibResources = AndrolibResources().also { resources ->
|
||||||
|
@ -218,6 +164,8 @@ class Patcher(private val options: PatcherOptions) {
|
||||||
cacheDirectory.close()
|
cacheDirectory.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else -> logger.info("Not compiling resources because resource patching is not required")
|
||||||
|
}
|
||||||
|
|
||||||
logger.trace("Creating new dex file")
|
logger.trace("Creating new dex file")
|
||||||
val newDexFile = object : DexFile {
|
val newDexFile = object : DexFile {
|
||||||
|
@ -242,7 +190,9 @@ class Patcher(private val options: PatcherOptions) {
|
||||||
return PatcherResult(
|
return PatcherResult(
|
||||||
dexFiles.map {
|
dexFiles.map {
|
||||||
app.revanced.patcher.util.dex.DexFile(it.key, it.value.readAt(0))
|
app.revanced.patcher.util.dex.DexFile(it.key, it.value.readAt(0))
|
||||||
}, metaInfo.doNotCompress?.toList(), resourceFile
|
},
|
||||||
|
metaInfo.doNotCompress?.toList(),
|
||||||
|
resourceFile
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,16 +201,29 @@ class Patcher(private val options: PatcherOptions) {
|
||||||
* @param patches [Patch]es The patches to add.
|
* @param patches [Patch]es The patches to add.
|
||||||
*/
|
*/
|
||||||
fun addPatches(patches: Iterable<Class<out Patch<Data>>>) {
|
fun addPatches(patches: Iterable<Class<out Patch<Data>>>) {
|
||||||
for (patch in patches) {
|
/**
|
||||||
|
* Fill the cache with the instances of the [Patch]es for later use.
|
||||||
|
* Note: Dependencies of the [Patch] will be cached as well.
|
||||||
|
*/
|
||||||
|
fun Class<out Patch<Data>>.isResource() {
|
||||||
|
this.also {
|
||||||
|
if (!ResourcePatch::class.java.isAssignableFrom(it)) return
|
||||||
|
// set the mode to decode all resources before running the patches
|
||||||
|
resourceDecodingMode = ResourceDecodingMode.FULL
|
||||||
|
}.dependencies?.forEach { it.java.isResource() }
|
||||||
|
}
|
||||||
|
|
||||||
|
data.patches.addAll(
|
||||||
|
patches.onEach(Class<out Patch<Data>>::isResource).onEach { patch ->
|
||||||
val needsVersion = patch.sincePatcherVersion
|
val needsVersion = patch.sincePatcherVersion
|
||||||
if (needsVersion != null && needsVersion > version) {
|
if (needsVersion != null && needsVersion > version) {
|
||||||
logger.error("Patch '${patch.patchName}' requires Patcher version $needsVersion or higher")
|
logger.error("Patch '${patch.patchName}' requires Patcher version $needsVersion or higher")
|
||||||
logger.error("Current Patcher version is $version")
|
logger.error("Current Patcher version is $version")
|
||||||
logger.warn("Skipping '${patch.patchName}'!")
|
logger.warn("Skipping '${patch.patchName}'!")
|
||||||
continue // TODO: continue or halt/throw?
|
return@onEach // TODO: continue or halt/throw?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data.patches.addAll(patches)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -286,49 +249,119 @@ class Patcher(private val options: PatcherOptions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// recursively apply all dependency patches
|
// recursively apply all dependency patches
|
||||||
patch.dependencies?.forEach {
|
patch.dependencies?.forEach { dependency ->
|
||||||
val patchDependency = it.java
|
val result = applyPatch(dependency.java, appliedPatches)
|
||||||
|
|
||||||
val result = applyPatch(patchDependency, appliedPatches)
|
|
||||||
|
|
||||||
if (result.isSuccess()) return@forEach
|
if (result.isSuccess()) return@forEach
|
||||||
|
|
||||||
val error = result.error()!!
|
val error = result.error()!!
|
||||||
val errorMessage = error.cause ?: error.message
|
val errorMessage = error.cause ?: error.message
|
||||||
return PatchResultError("'$patchName' depends on '${patchDependency.patchName}' but the following error was raised: $errorMessage")
|
return PatchResultError("'$patchName' depends on '${patch.patchName}' but the following error was raised: $errorMessage")
|
||||||
|
}
|
||||||
|
|
||||||
|
patch.deprecated?.let { (reason, replacement) ->
|
||||||
|
logger.warn("'$patchName' is deprecated, reason: $reason")
|
||||||
|
if (replacement != null) logger.warn("Use '${replacement.java.patchName}' instead")
|
||||||
}
|
}
|
||||||
|
|
||||||
val patchInstance = patch.getDeclaredConstructor().newInstance()
|
val patchInstance = patch.getDeclaredConstructor().newInstance()
|
||||||
|
|
||||||
// if the current patch is a resource patch but resource patching is disabled, return an error
|
val isResourcePatch = ResourcePatch::class.java.isAssignableFrom(patch)
|
||||||
val isResourcePatch = patchInstance is ResourcePatch
|
// TODO: implement this in a more polymorphic way
|
||||||
if (!options.patchResources && isResourcePatch) {
|
|
||||||
return PatchResultError("'$patchName' is a resource patch, but resource patching is disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
patch.deprecated?.let { (reason, replacement) ->
|
|
||||||
logger.warn("'$patchName' is deprecated: $reason")
|
|
||||||
if (replacement != null) logger.warn("Use '${replacement.java.patchName}' instead")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: find a solution for this
|
|
||||||
val data = if (isResourcePatch) {
|
val data = if (isResourcePatch) {
|
||||||
data.resourceData
|
data.resourceData
|
||||||
} else {
|
} else {
|
||||||
val bytecodeData = data.bytecodeData
|
data.bytecodeData.also { data ->
|
||||||
(patchInstance as BytecodePatch).fingerprints?.resolve(bytecodeData, bytecodeData.classes.internalClasses)
|
(patchInstance as BytecodePatch).fingerprints?.resolve(
|
||||||
bytecodeData
|
data,
|
||||||
|
data.classes.internalClasses
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}")
|
logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}")
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val result = patchInstance.execute(data)
|
patchInstance.execute(data).also {
|
||||||
appliedPatches[patchName] = AppliedPatch(patchInstance, result.isSuccess())
|
appliedPatches[patchName] = AppliedPatch(patchInstance, it.isSuccess())
|
||||||
result
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
PatchResultError(e).also {
|
||||||
appliedPatches[patchName] = AppliedPatch(patchInstance, false)
|
appliedPatches[patchName] = AppliedPatch(patchInstance, false)
|
||||||
PatchResultError(e)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode resources for the patcher.
|
||||||
|
*
|
||||||
|
* @param mode The [ResourceDecodingMode] to use when decoding.
|
||||||
|
*/
|
||||||
|
private fun decodeResources(mode: ResourceDecodingMode) {
|
||||||
|
val extInputFile = ExtFile(options.inputFile)
|
||||||
|
try {
|
||||||
|
val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) })
|
||||||
|
val resourceTable = androlib.getResTable(extInputFile, true)
|
||||||
|
when (mode) {
|
||||||
|
ResourceDecodingMode.FULL -> {
|
||||||
|
val outDir = File(options.resourceCacheDirectory)
|
||||||
|
if (outDir.exists()) {
|
||||||
|
logger.info("Deleting existing resource cache directory")
|
||||||
|
if (!outDir.deleteRecursively()) {
|
||||||
|
logger.error("Failed to delete existing resource cache directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outDir.mkdirs()
|
||||||
|
|
||||||
|
logger.info("Decoding resources")
|
||||||
|
|
||||||
|
// decode resources to cache directory
|
||||||
|
androlib.decodeManifestWithResources(extInputFile, outDir, resourceTable)
|
||||||
|
androlib.decodeResourcesFull(extInputFile, outDir, resourceTable)
|
||||||
|
|
||||||
|
// read additional metadata from the resource table
|
||||||
|
data.packageMetadata.let { metadata ->
|
||||||
|
metadata.metaInfo.usesFramework = UsesFramework().also { framework ->
|
||||||
|
framework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
// read files to not compress
|
||||||
|
metadata.metaInfo.doNotCompress = buildList {
|
||||||
|
androlib.recordUncompressedFiles(extInputFile, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
ResourceDecodingMode.MANIFEST_ONLY -> {
|
||||||
|
logger.info("Decoding AndroidManifest.xml only, because resources are not needed")
|
||||||
|
|
||||||
|
// create decoder for the resource table
|
||||||
|
val decoder = ResAttrDecoder()
|
||||||
|
decoder.currentPackage = ResPackage(resourceTable, 0, null)
|
||||||
|
|
||||||
|
// create xml parser with the decoder
|
||||||
|
val axmlParser = AXmlResourceParser()
|
||||||
|
axmlParser.attrDecoder = decoder
|
||||||
|
|
||||||
|
// parse package information with the decoder and parser which will set required values in the resource table
|
||||||
|
// instead of decodeManifest another more low level solution can be created to make it faster/better
|
||||||
|
XmlPullStreamDecoder(
|
||||||
|
axmlParser, AndrolibResources().resXmlSerializer
|
||||||
|
).decodeManifest(
|
||||||
|
extInputFile.directory.getFileInput("AndroidManifest.xml"), nullOutputStream
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read of the resourceTable which is created by reading the manifest file
|
||||||
|
data.packageMetadata.let { metadata ->
|
||||||
|
metadata.packageName = resourceTable.currentResPackage.name
|
||||||
|
metadata.packageVersion = resourceTable.versionInfo.versionName
|
||||||
|
metadata.metaInfo.versionInfo = resourceTable.versionInfo
|
||||||
|
metadata.metaInfo.sdkInfo = resourceTable.sdkInfo
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
extInputFile.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,6 +371,9 @@ class Patcher(private val options: PatcherOptions) {
|
||||||
* @return A pair of the name of the [Patch] and its [PatchResult].
|
* @return A pair of the name of the [Patch] and its [PatchResult].
|
||||||
*/
|
*/
|
||||||
fun applyPatches(stopOnError: Boolean = false) = sequence {
|
fun applyPatches(stopOnError: Boolean = false) = sequence {
|
||||||
|
// prevent from decoding the manifest twice if it is not needed
|
||||||
|
if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL)
|
||||||
|
|
||||||
logger.trace("Applying all patches")
|
logger.trace("Applying all patches")
|
||||||
|
|
||||||
val appliedPatches = LinkedHashMap<String, AppliedPatch>() // first is name
|
val appliedPatches = LinkedHashMap<String, AppliedPatch>() // first is name
|
||||||
|
@ -364,6 +400,21 @@ class Patcher(private val options: PatcherOptions) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of decoding the resources.
|
||||||
|
*/
|
||||||
|
private enum class ResourceDecodingMode {
|
||||||
|
/**
|
||||||
|
* Decode all resources.
|
||||||
|
*/
|
||||||
|
FULL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the manifest file only.
|
||||||
|
*/
|
||||||
|
MANIFEST_ONLY,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -373,9 +424,3 @@ class Patcher(private val options: PatcherOptions) {
|
||||||
* @param success The result of the [Patch].
|
* @param success The result of the [Patch].
|
||||||
*/
|
*/
|
||||||
internal data class AppliedPatch(val patchInstance: Patch<Data>, val success: Boolean)
|
internal data class AppliedPatch(val patchInstance: Patch<Data>, val success: Boolean)
|
||||||
|
|
||||||
private fun BuildOptions.setBuildOptions(options: PatcherOptions) {
|
|
||||||
this.aaptPath = options.aaptPath
|
|
||||||
this.useAapt2 = true
|
|
||||||
this.frameworkFolderLocation = options.frameworkFolderLocation
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,10 +9,10 @@ import org.jf.dexlib2.iface.ClassDef
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
data class PatcherData(
|
data class PatcherData(
|
||||||
internal val internalClasses: MutableList<ClassDef>,
|
val internalClasses: MutableList<ClassDef>,
|
||||||
internal val resourceCacheDirectory: String,
|
val resourceCacheDirectory: String,
|
||||||
val packageMetadata: PackageMetadata
|
|
||||||
) {
|
) {
|
||||||
|
val packageMetadata = PackageMetadata()
|
||||||
internal val patches = mutableListOf<Class<out Patch<Data>>>()
|
internal val patches = mutableListOf<Class<out Patch<Data>>>()
|
||||||
internal val bytecodeData = BytecodeData(internalClasses)
|
internal val bytecodeData = BytecodeData(internalClasses)
|
||||||
internal val resourceData = ResourceData(File(resourceCacheDirectory))
|
internal val resourceData = ResourceData(File(resourceCacheDirectory))
|
||||||
|
|
|
@ -8,7 +8,6 @@ import java.io.File
|
||||||
* Options for the [Patcher].
|
* Options for the [Patcher].
|
||||||
* @param inputFile The input file (usually an apk file).
|
* @param inputFile The input file (usually an apk file).
|
||||||
* @param resourceCacheDirectory Directory to cache resources.
|
* @param resourceCacheDirectory Directory to cache resources.
|
||||||
* @param patchResources Weather to use the resource patcher. Resources will still need to be decoded.
|
|
||||||
* @param aaptPath Optional path to a custom aapt binary.
|
* @param aaptPath Optional path to a custom aapt binary.
|
||||||
* @param frameworkFolderLocation Optional path to a custom framework folder.
|
* @param frameworkFolderLocation Optional path to a custom framework folder.
|
||||||
* @param logger Custom logger implementation for the [Patcher].
|
* @param logger Custom logger implementation for the [Patcher].
|
||||||
|
@ -16,7 +15,6 @@ import java.io.File
|
||||||
data class PatcherOptions(
|
data class PatcherOptions(
|
||||||
internal val inputFile: File,
|
internal val inputFile: File,
|
||||||
internal val resourceCacheDirectory: String,
|
internal val resourceCacheDirectory: String,
|
||||||
internal val patchResources: Boolean = false,
|
|
||||||
internal val aaptPath: String = "",
|
internal val aaptPath: String = "",
|
||||||
internal val frameworkFolderLocation: String? = null,
|
internal val frameworkFolderLocation: String? = null,
|
||||||
internal val logger: Logger = NopLogger
|
internal val logger: Logger = NopLogger
|
||||||
|
|
Loading…
Reference in a new issue