Skip to content

Commit

Permalink
Rollback booster-transform-r-inline
Browse files Browse the repository at this point in the history
  • Loading branch information
johnsonlee committed Mar 1, 2024
1 parent 3568f4f commit 18a6018
Show file tree
Hide file tree
Showing 10 changed files with 779 additions and 0 deletions.
24 changes: 24 additions & 0 deletions booster-transform-r-inline/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# booster-transform-r-inline

This module is used for resource index inline, such as fields in `R$id`, `R$layout`, `R$string`, etc.

## Properties

The following table shows the properties that transformer supports:

| Property | Description | Example |
| -------------------------------- | ------------------------------------------------------------ | ---------------------------------- |
| `booster.transform.r.inline.ignores` | comma separated wildcard patterns to ignore | android/\*,androidx/\* |

The properties can be passthrough the command line as following:

```bash
./gradlew assembleDebug -Pbooster.transform.r.inline.ignores=android/*,androidx/*
```

or configured in the `gradle.properties`:

```properties
booster.transform.r.inline.ignores=android/*,androidx/*
```

8 changes: 8 additions & 0 deletions booster-transform-r-inline/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apply from: "$rootDir/gradle/booster.gradle"

dependencies {
kapt "com.google.auto.service:auto-service:1.0"
implementation project(':booster-aapt2')
implementation project(':booster-api')
implementation project(':booster-transform-asm')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.didiglobal.booster.transform.r.inline

import java.lang.Exception

class MalformedSymbolListException(msg: String?) : Exception(msg)
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package com.didiglobal.booster.transform.r.inline

import com.didiglobal.booster.kotlinx.Wildcard
import com.didiglobal.booster.kotlinx.asIterable
import com.didiglobal.booster.kotlinx.execute
import com.didiglobal.booster.kotlinx.ifNotEmpty
import com.didiglobal.booster.kotlinx.touch
import com.didiglobal.booster.transform.ArtifactManager.Companion.MERGED_RES
import com.didiglobal.booster.transform.ArtifactManager.Companion.SYMBOL_LIST
import com.didiglobal.booster.transform.TransformContext
import com.didiglobal.booster.transform.asm.ClassTransformer
import com.didiglobal.booster.transform.r.inline.Build.*
import com.google.auto.service.AutoService
import org.gradle.api.logging.Logging
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.ClassWriter.COMPUTE_MAXS
import org.objectweb.asm.Opcodes.GETSTATIC
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.FieldInsnNode
import org.objectweb.asm.tree.LdcInsnNode
import java.io.PrintWriter
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern

internal const val R_STYLEABLE = "R\$styleable"
internal const val ANDROID_R = "android/R$"
internal const val COM_ANDROID_INTERNAL_R = "com/android/internal/R$"
internal const val R_REGEX = ".*/R\\\$.*|.*/R\\.*"

/**
* Represents a class node transformer for constants shrinking
*
* @author johnsonlee
*/
@AutoService(ClassTransformer::class)
class RInlineTransformer : ClassTransformer {

private lateinit var appPackage: String
private lateinit var appRStyleable: String
private lateinit var symbols: SymbolList
private lateinit var ignores: Set<Wildcard>
private lateinit var logger: PrintWriter
private val removedR by lazy {
ConcurrentHashMap<String, Int>()
}

override val name: String = ARTIFACT

override fun onPreTransform(context: TransformContext) {
this.appPackage = context.originalApplicationId.replace('.', '/')
this.logger = getReport(context, "report.txt").touch().printWriter()
this.symbols = SymbolList.from(context.artifacts.get(SYMBOL_LIST).single())
this.appRStyleable = "$appPackage/$R_STYLEABLE"
this.ignores = context.getProperty(PROPERTY_IGNORES, "").trim().split(',')
.filter(String::isNotEmpty)
.map(Wildcard.Companion::valueOf).toSet()

if (this.symbols.isEmpty()) {
logger_.error("Inline R symbols failed: R.txt doesn't exist or blank")
this.logger.println("Inlining R symbols failed: R.txt doesn't exist or blank")
return
}

val retainedSymbols: Set<String>
val deps = context.dependencies
if (deps.any { it.contains(SUPPORT_CONSTRAINT_LAYOUT) || it.contains(JETPACK_CONSTRAINT_LAYOUT) }) {
// Find symbols that should be retained
retainedSymbols = context.findRetainedSymbols()
if (retainedSymbols.isNotEmpty()) {
this.ignores += setOf(Wildcard.valueOf("android/support/constraint/R\$id"))
this.ignores += setOf(Wildcard.valueOf("androidx/constraintlayout/R\$id"))
this.ignores += setOf(Wildcard.valueOf("androidx/constraintlayout/widget/R\$id"))
}
} else {
retainedSymbols = emptySet()
}

logger.println(deps.joinToString("\n - ", "dependencies:\n - ", "\n"))
logger.println("$PROPERTY_IGNORES=$ignores\n")

retainedSymbols.ifNotEmpty { symbols ->
logger.println("Retained symbols:")
symbols.forEach {
logger.println(" - R.id.$it")
}
logger.println()
}

}

override fun transform(context: TransformContext, klass: ClassNode): ClassNode {
if (this.symbols.isEmpty()) {
return klass
}
if (this.ignores.any { it.matches(klass.name) }) {
logger.println("Ignore `${klass.name}`")
} else if (Pattern.matches(R_REGEX, klass.name) && klass.name != appRStyleable) {
klass.fields.clear()
removedR[klass.name] = klass.bytes()
} else {
klass.replaceSymbolReferenceWithConstant()
}

return klass
}

private fun ClassNode.bytes() = ClassWriter(COMPUTE_MAXS).also {
accept(it)
}.toByteArray().size

override fun onPostTransform(context: TransformContext) {
val totalSize = removedR.map { it.value }.sum()
val maxWidth = removedR.map { it.key.length }.maxOrNull()?.plus(10) ?: 10
this.logger.run {
println("Delete files:")
removedR.toSortedMap().forEach {
println(" - `${it.key}`")
}
println("-".repeat(maxWidth))
println("Total: $totalSize bytes")
println()
close()
}
}

private fun ClassNode.replaceSymbolReferenceWithConstant() {
methods.forEach { method ->
val insns = method.instructions.iterator().asIterable().filter {
it.opcode == GETSTATIC
}.map {
it as FieldInsnNode
}.filter {
("I" == it.desc || "[I" == it.desc)
&& it.owner.substring(it.owner.lastIndexOf('/') + 1).startsWith("R$")
&& !(it.owner.startsWith(COM_ANDROID_INTERNAL_R) || it.owner.startsWith(ANDROID_R))
}

val intFields = insns.filter { "I" == it.desc }
val intArrayFields = insns.filter { "[I" == it.desc }

// Replace int field with constant
intFields.forEach { field ->
val type = field.owner.substring(field.owner.lastIndexOf("/R$") + 3)
try {
method.instructions.insertBefore(field, LdcInsnNode(symbols.getInt(type, field.name)))
method.instructions.remove(field)
logger.println(" * ${field.owner}.${field.name} => ${symbols.getInt(type, field.name)}: $name.${method.name}${method.desc}")
} catch (e: NullPointerException) {
logger.println(" ! Unresolvable symbol `${field.owner}.${field.name}`: $name.${method.name}${method.desc}")
}
}

// Replace library's R fields with application's R fields
intArrayFields.forEach { field ->
field.owner = "$appPackage/${field.owner.substring(field.owner.lastIndexOf('/') + 1)}"
}
}
}

}

/**
* Find symbols that should be retained, such as:
*
* - attribute `constraint_referenced_ids` in `ConstraintLayout`
*/
private fun TransformContext.findRetainedSymbols(): Set<String> {
return artifacts.get(MERGED_RES).map {
RetainedSymbolCollector(it).execute()
}.flatten().toSet()
}

private val PROPERTY_PREFIX = ARTIFACT.replace('-', '.')

private val PROPERTY_IGNORES = "$PROPERTY_PREFIX.ignores"

private const val SUPPORT_CONSTRAINT_LAYOUT = "com.android.support.constraint:constraint-layout:"
private const val JETPACK_CONSTRAINT_LAYOUT = "androidx.constraintlayout:constraintlayout:"

private val logger_ = Logging.getLogger(RInlineTransformer::class.java)
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package com.didiglobal.booster.transform.r.inline

import com.didiglobal.booster.aapt2.BinaryParser
import com.didiglobal.booster.aapt2.MAGIC
import com.didiglobal.booster.aapt2.RES_FILE
import com.didiglobal.booster.aapt2.Resources
import com.didiglobal.booster.aapt2.ResourcesInternal
import com.didiglobal.booster.kotlinx.isValidJavaIdentifier
import com.didiglobal.booster.kotlinx.stackTraceAsString
import org.gradle.api.logging.Logging
import java.io.File
import java.util.Stack
import java.util.concurrent.RecursiveTask
import java.util.regex.Pattern

/**
* Represents a collector for retained symbols collecting
*
* @author johnsonlee
*/
internal class RetainedSymbolCollector(private val root: File) : RecursiveTask<Collection<String>>() {

override fun compute(): Collection<String> {
val tasks = mutableListOf<RecursiveTask<Collection<String>>>()
val result = mutableSetOf<String>()

root.listFiles()?.forEach { file ->
if (file.isDirectory) {
RetainedSymbolCollector(file).also { task ->
tasks.add(task)
}.fork()
} else if ((file.name.startsWith("layout_") || file.name.startsWith("layout-")) && file.name.endsWith(".xml.flat")) {
result.addAll(file.parseLayoutXml())
}
}

return result + tasks.flatMap { it.join() }
}

}

internal fun File.parseLayoutXml(): Collection<String> {
try {
BinaryParser(this).use { parser ->
val magic = parser.readInt()
if (MAGIC != magic) {
logger.error("Invalid AAPT2 container file: $canonicalPath")
return emptySet()
}

val version = parser.readInt()
if (version <= 0) {
logger.error("Invalid AAPT2 container version: $canonicalPath")
return emptySet()
}

val count = parser.readInt()
if (count <= 0) {
logger.warn("Empty AAPT2 container: $canonicalPath")
return emptySet()
}

return (1..count).map {
parser.parseResEntry()
}.flatten().toSet()
}
} catch (e: Throwable) {
logger.error(e.stackTraceAsString)
}

return emptySet()
}

internal fun BinaryParser.parseResEntry(): Collection<String> {
val p = tell()
val type = readInt()
val length = readLong()

if (type == RES_FILE) {
val headerSize = readInt()
val dataSize = readLong()
val header = parse {
ResourcesInternal.CompiledFile.parseFrom(readBytes(headerSize))
}

skip((4 - (tell() % 4)) % 4) // skip padding

if (header.type == Resources.FileReference.Type.PROTO_XML) {
return parse {
Resources.XmlNode.parseFrom(readBytes(dataSize.toInt()))
}.findAllRetainedSymbols()
}
}

seek(p + length.toInt())

return emptySet()
}

internal fun Resources.XmlNode.findAllRetainedSymbols(): Collection<String> {
val stack = Stack<Resources.XmlElement>().apply {
push(element)
}

return mutableSetOf<String>().apply {
while (stack.isNotEmpty()) {
val next = stack.pop()
next.childList.filter(Resources.XmlNode::hasElement)
.map(Resources.XmlNode::getElement)
.let(stack::addAll)
next.findRetainedSymbols()?.let(this::addAll)
}
}
}

internal fun Resources.XmlElement.findRetainedSymbols(): Collection<String>? = attributeList?.filter {
it.name !in IGNORED_CONSTRAINT_LAYOUT_ATTRS
}?.map { attr ->
when {
attr.name == "constraint_referenced_ids" -> attr.value.split(PATTERN_COMMA)
attr.name.startsWith("layout_constraint") -> attr.value.split(PATTERN_COMMA).filter {
"parent" != it
}.map {
it.substringAfter('/')
}.filter(String::isValidJavaIdentifier)
else -> emptyList()
}
}?.flatten()

private val logger = Logging.getLogger(RetainedSymbolCollector::class.java)

/**
* ref: https://developer.android.com/reference/androidx/constraintlayout/widget/ConstraintLayout
*/
private val IGNORED_CONSTRAINT_LAYOUT_ATTRS = setOf(
"layout_constraintHorizontal_bias",
"layout_constraintVertical_bias",
"layout_constraintCircleRadius",
"layout_constraintCircleAngle",
"layout_constraintDimensionRatio",
"layout_constraintHeight_default",
"layout_constraintHeight_min",
"layout_constraintHeight_max",
"layout_constraintHeight_percent",
"layout_constraintWidth_default",
"layout_constraintWidth_min",
"layout_constraintWidth_max",
"layout_constraintWidth_percent",
"layout_constraintHorizontal_chainStyle",
"layout_constraintVertical_chainStyle",
"layout_constraintHorizontal_weight",
"layout_constraintVertical_weight"
)

private val PATTERN_COMMA = Pattern.compile("\\s*,\\s*")
Loading

0 comments on commit 18a6018

Please sign in to comment.