forked from didi/booster
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3568f4f
commit 18a6018
Showing
10 changed files
with
779 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/* | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
} |
5 changes: 5 additions & 0 deletions
5
...src/main/kotlin/com/didiglobal/booster/transform/r/inline/MalformedSymbolListException.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
180 changes: 180 additions & 0 deletions
180
...-r-inline/src/main/kotlin/com/didiglobal/booster/transform/r/inline/RInlineTransformer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
155 changes: 155 additions & 0 deletions
155
...line/src/main/kotlin/com/didiglobal/booster/transform/r/inline/RetainedSymbolCollector.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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*") |
Oops, something went wrong.