perf: decode resources only when necessary

This commit is contained in:
oSumAtrIX 2022-09-26 08:57:39 +02:00
parent 98ce0abfa9
commit 3ba4be240b
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
3 changed files with 192 additions and 149 deletions

View file

@ -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) logger.info("Reading dex files")
try { // read dex files
val outDir = File(options.resourceCacheDirectory) val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
if (outDir.exists()) { // get the opcodes
logger.info("Deleting existing resource cache directory") opcodes = dexFile.opcodes
outDir.deleteRecursively() // finally create patcher data
} data = PatcherData(dexFile.classes.toMutableList(), options.resourceCacheDirectory)
outDir.mkdirs()
val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) }) // decode manifest file
val resourceTable = androlib.getResTable(extInputFile, true) decodeResources(ResourceDecodingMode.MANIFEST_ONLY)
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")
// read dex files
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
// get the opcodes
opcodes = dexFile.opcodes
// finally create patcher data
data = PatcherData(
dexFile.classes.toMutableList(), options.resourceCacheDirectory, packageMetadata
)
} finally {
extInputFile.close()
}
} }
/** /**
@ -175,48 +120,51 @@ 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) {
val cacheDirectory = ExtFile(options.resourceCacheDirectory) ResourceDecodingMode.FULL -> {
try { val cacheDirectory = ExtFile(options.resourceCacheDirectory)
val androlibResources = AndrolibResources().also { resources -> try {
resources.buildOptions = BuildOptions().also { buildOptions -> val androlibResources = AndrolibResources().also { resources ->
buildOptions.setBuildOptions(options) resources.buildOptions = BuildOptions().also { buildOptions ->
buildOptions.isFramework = metaInfo.isFrameworkApk buildOptions.setBuildOptions(options)
buildOptions.resourcesAreCompressed = metaInfo.compressionType buildOptions.isFramework = metaInfo.isFrameworkApk
buildOptions.doNotCompress = metaInfo.doNotCompress buildOptions.resourcesAreCompressed = metaInfo.compressionType
buildOptions.doNotCompress = metaInfo.doNotCompress
}
resources.setSdkInfo(metaInfo.sdkInfo)
resources.setVersionInfo(metaInfo.versionInfo)
resources.setSharedLibrary(metaInfo.sharedLibrary)
resources.setSparseResources(metaInfo.sparseResources)
} }
resources.setSdkInfo(metaInfo.sdkInfo) val manifestFile = cacheDirectory.resolve("AndroidManifest.xml")
resources.setVersionInfo(metaInfo.versionInfo)
resources.setSharedLibrary(metaInfo.sharedLibrary)
resources.setSparseResources(metaInfo.sparseResources)
}
val manifestFile = cacheDirectory.resolve("AndroidManifest.xml") ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile)
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile) val aaptFile = cacheDirectory.resolve("aapt_temp_file")
val aaptFile = cacheDirectory.resolve("aapt_temp_file") // delete if it exists
Files.deleteIfExists(aaptFile.toPath())
// delete if it exists val resDirectory = cacheDirectory.resolve("res")
Files.deleteIfExists(aaptFile.toPath()) val includedFiles = metaInfo.usesFramework.ids.map { id ->
androlibResources.getFrameworkApk(
id, metaInfo.usesFramework.tag
)
}.toTypedArray()
val resDirectory = cacheDirectory.resolve("res") logger.info("Compiling resources")
val includedFiles = metaInfo.usesFramework.ids.map { id -> androlibResources.aaptPackage(
androlibResources.getFrameworkApk( aaptFile, manifestFile, resDirectory, null, null, includedFiles
id, metaInfo.usesFramework.tag
) )
}.toTypedArray()
logger.info("Compiling resources") resourceFile = aaptFile
androlibResources.aaptPackage( } finally {
aaptFile, manifestFile, resDirectory, null, null, includedFiles cacheDirectory.close()
) }
resourceFile = aaptFile
} finally {
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")
@ -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) { /**
val needsVersion = patch.sincePatcherVersion * Fill the cache with the instances of the [Patch]es for later use.
if (needsVersion != null && needsVersion > version) { * Note: Dependencies of the [Patch] will be cached as well.
logger.error("Patch '${patch.patchName}' requires Patcher version $needsVersion or higher") */
logger.error("Current Patcher version is $version") fun Class<out Patch<Data>>.isResource() {
logger.warn("Skipping '${patch.patchName}'!") this.also {
continue // TODO: continue or halt/throw? 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)
data.patches.addAll(
patches.onEach(Class<out Patch<Data>>::isResource).onEach { patch ->
val needsVersion = patch.sincePatcherVersion
if (needsVersion != null && needsVersion > version) {
logger.error("Patch '${patch.patchName}' requires Patcher version $needsVersion or higher")
logger.error("Current Patcher version is $version")
logger.warn("Skipping '${patch.patchName}'!")
return@onEach // TODO: continue or halt/throw?
}
}
)
} }
/** /**
@ -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) {
appliedPatches[patchName] = AppliedPatch(patchInstance, false) PatchResultError(e).also {
PatchResultError(e) appliedPatches[patchName] = AppliedPatch(patchInstance, false)
}
}
}
/**
* 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
}

View file

@ -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))

View file

@ -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