Skip to content

Commit 05c213b

Browse files
yanglwshifujun
authored andcommitted
feat(core.gradle-plugin): 支持AGP 8后开启shrinkResources
当在 AGP 8.9.0 版本中开启 shrinkResources 功能后,ap_ 文件中的 AndroidManifest.xml 为新的二进制格式,且资源文件引用也不会改为 ID 。新的方案使用合并后的 AndroidManifest.xml 和 R.txt 生成 PluginManifest 文件。 新的方案自动应用在 AGP 8 且开启shrinkResources时。
1 parent 474e6bf commit 05c213b

8 files changed

Lines changed: 409 additions & 19 deletions

File tree

projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/AGPCompat.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818

1919
package com.tencent.shadow.core.gradle
2020

21+
import com.android.build.gradle.AppExtension
2122
import com.android.build.gradle.BaseExtension
2223
import com.android.build.gradle.api.ApplicationVariant
2324
import com.android.build.gradle.api.BaseVariantOutput
2425
import com.android.build.gradle.internal.dsl.ProductFlavor
26+
import org.gradle.api.Project
2527
import org.gradle.api.Task
2628
import java.io.File
2729

@@ -36,4 +38,18 @@ internal interface AGPCompat {
3638
fun getAaptAdditionalParameters(processResourcesTask: Task): List<String>
3739
fun getMinSdkVersion(pluginVariant: ApplicationVariant): Int
3840
fun hasDeprecatedTransformApi(): Boolean
41+
fun isGeneratePluginManifestByMergedManifest(
42+
project: Project,
43+
appExtension: AppExtension,
44+
pluginVariant: ApplicationVariant
45+
): Boolean
46+
47+
fun getProcessManifestTask(output: BaseVariantOutput): Task
48+
fun getProcessManifestFile(
49+
project: Project,
50+
pluginVariant: ApplicationVariant,
51+
output: BaseVariantOutput
52+
): File
53+
54+
fun getRTxtFile(project: Project, processResourcesTask: Task?, variantName: String): File
3955
}

projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/AGPCompatImpl.kt

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package com.tencent.shadow.core.gradle
22

33
import com.android.SdkConstants
4+
import com.android.build.gradle.AppExtension
45
import com.android.build.gradle.BaseExtension
56
import com.android.build.gradle.api.ApplicationVariant
67
import com.android.build.gradle.api.BaseVariantOutput
78
import com.android.build.gradle.internal.dsl.ProductFlavor
89
import com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask
910
import com.android.build.gradle.internal.scope.InternalArtifactType
1011
import com.android.sdklib.AndroidVersion.VersionCodes
12+
import org.gradle.api.Project
1113
import org.gradle.api.Task
1214
import org.gradle.api.file.Directory
1315
import org.gradle.api.file.DirectoryProperty
@@ -144,6 +146,147 @@ internal class AGPCompatImpl : AGPCompat {
144146
return true
145147
}
146148

149+
override fun isGeneratePluginManifestByMergedManifest(
150+
project: Project,
151+
appExtension: AppExtension,
152+
pluginVariant: ApplicationVariant
153+
): Boolean {
154+
// 可以通过配置强制开启
155+
if ("true" == project.findProperty("shadow.generatePluginManifestUseMergedManifest")) {
156+
return true
157+
}
158+
// 没有开启无用资源删减,则不使用 merged manifest
159+
try {
160+
if (!pluginVariant.buildType.isMinifyEnabled) {
161+
return false
162+
}
163+
// AppExtension 获取的 BuildType 无法获取 isShrinkResources 属性,只能查找原始的 BuildType 实现。
164+
if (!appExtension.buildTypes.getByName(pluginVariant.buildType.name).isShrinkResources) {
165+
return false
166+
}
167+
} catch (ignored: Error) {
168+
}
169+
170+
// 开启无用资源删减功能,同时AGP 版本至少要为 8.9.0
171+
try {
172+
val version = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
173+
val majorVersion = version.substringBefore('.', "0").toInt()
174+
if (majorVersion > 8) {
175+
return true
176+
}
177+
if (majorVersion == 8) {
178+
val minorVersion =
179+
version.substringAfter('.').substringBefore('.').toIntOrNull() ?: 0
180+
return minorVersion >= 9
181+
}
182+
} catch (ignored: Error) {
183+
}
184+
// 默认不使用 merged manifest 。
185+
return false
186+
}
187+
188+
/**
189+
* 获取生成最终 AndroidManifest.xml 文件的任务。
190+
*/
191+
override fun getProcessManifestTask(output: BaseVariantOutput): Task {
192+
return try {
193+
output.processManifestProvider.get()
194+
} catch (_: Error) {
195+
output.processManifest
196+
}
197+
}
198+
199+
/**
200+
* 获取合并后的 AndroidManifest.xml 文件。
201+
*
202+
* 优先从 processManifest 任务输出获取,否则搜索 intermediates 目录。
203+
*/
204+
override fun getProcessManifestFile(
205+
project: Project,
206+
pluginVariant: ApplicationVariant,
207+
output: BaseVariantOutput
208+
): File {
209+
// 1. 优先从任务输出获取
210+
try {
211+
output.processManifestProvider.get().outputs.files.files.forEach {
212+
findFileByName(it, "AndroidManifest.xml")?.let { file -> return file }
213+
}
214+
} catch (_: Exception) {
215+
// 忽略
216+
}
217+
218+
val variantName = pluginVariant.name
219+
220+
// 2. 搜索中间产物目录
221+
return listOf(
222+
"intermediates/merged_manifests/$variantName", // AGP 4.x/7.x/8.x
223+
"intermediates/manifests/full/$variantName", // AGP 3.x
224+
)
225+
.map { File(project.buildDir, it) }
226+
.first {
227+
findFileByName(it, "AndroidManifest.xml") != null
228+
}
229+
}
230+
231+
/**
232+
* 获取 R.txt 文件。
233+
*
234+
* 优先从 processResources 任务的输出获取(最准确), 否则搜索 intermediates 目录。
235+
*/
236+
override fun getRTxtFile(
237+
project: Project,
238+
processResourcesTask: Task?,
239+
variantName: String
240+
): File {
241+
// 1. 优先尝试从任务输出中查找
242+
if (processResourcesTask != null) {
243+
try {
244+
processResourcesTask.outputs.files.files.forEach {
245+
findFileByName(it, "R.txt")?.let { file -> return file }
246+
}
247+
} catch (_: Exception) {
248+
// 忽略解析错误,继续走备选路径
249+
}
250+
}
251+
252+
// 2. 根据 AGP 版本已知的中间产物路径搜索
253+
return listOf(
254+
"intermediates/runtime_symbol_list/$variantName", // AGP 4.x/7.x/8.x
255+
"intermediates/symbols/$variantName",
256+
"intermediates/bundles/$variantName"
257+
)
258+
.map { File(project.buildDir, it) }
259+
.first {
260+
findFileByName(it, "R.txt") != null
261+
}
262+
}
263+
264+
/**
265+
* 搜索指定目录下指定文件名的文件。
266+
*
267+
* @return 文件对象,若找不到则返回 null 。
268+
*/
269+
private fun findFileByName(file: File, fileName: String): File? {
270+
if (!file.exists()) {
271+
return null
272+
}
273+
if (file.isFile && file.name == fileName) {
274+
return file
275+
}
276+
if (file.isDirectory) {
277+
val subFiles = file.listFiles()
278+
if (subFiles != null) {
279+
for (subFile in subFiles) {
280+
val resultFile = findFileByName(subFile, fileName)
281+
if (resultFile != null) {
282+
return resultFile
283+
}
284+
}
285+
}
286+
}
287+
return null
288+
}
289+
147290
companion object {
148291
fun getStringFromProperty(x: Any?): String {
149292
return when (x) {

projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/ShadowPlugin.kt

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.android.build.gradle.BaseExtension
2626
import com.android.build.gradle.api.ApplicationVariant
2727
import com.android.sdklib.AndroidVersion.VersionCodes
2828
import com.tencent.shadow.core.gradle.extensions.PackagePluginExtension
29+
import com.tencent.shadow.core.manifest_parser.createManifestValueParser
2930
import com.tencent.shadow.core.manifest_parser.generatePluginManifest
3031
import com.tencent.shadow.core.transform.DeprecatedTransformWrapper
3132
import com.tencent.shadow.core.transform.GradleTransformWrapper
@@ -111,7 +112,20 @@ class ShadowPlugin : Plugin<Project> {
111112

112113
val appExtension: AppExtension =
113114
project.extensions.getByType(AppExtension::class.java)
114-
createGeneratePluginManifestTasks(project, appExtension, pluginVariant)
115+
if (agpCompat.isGeneratePluginManifestByMergedManifest(
116+
project,
117+
appExtension,
118+
pluginVariant
119+
)
120+
) {
121+
createGeneratePluginManifestTasksByMergedManifest(
122+
project,
123+
appExtension,
124+
pluginVariant
125+
)
126+
} else {
127+
createGeneratePluginManifestTasks(project, appExtension, pluginVariant)
128+
}
115129
}
116130
}
117131

@@ -276,6 +290,56 @@ class ShadowPlugin : Plugin<Project> {
276290
(javacTask as JavaCompile).source(project.fileTree(relativePath))
277291
}
278292

293+
private fun createGeneratePluginManifestTasksByMergedManifest(
294+
project: Project,
295+
appExtension: AppExtension,
296+
pluginVariant: ApplicationVariant
297+
) {
298+
val output = pluginVariant.outputs.first()
299+
300+
val variantName = pluginVariant.name
301+
val capitalizeVariantName = variantName.capitalize()
302+
303+
// 添加生成PluginManifest.java任务
304+
val pluginManifestSourceDir =
305+
File(project.buildDir, "generated/source/pluginManifest/$variantName")
306+
307+
val javacTask = project.tasks.getByName("compile${capitalizeVariantName}JavaWithJavac")
308+
309+
val generatePluginManifestTask =
310+
project.tasks.register("generate${capitalizeVariantName}PluginManifest") {
311+
// 依赖 processManifest 任务以获取最终的 AndroidManifest.xml
312+
val processManifestTask = agpCompat.getProcessManifestTask(output)
313+
// 依赖 processResources 任务以获取 R.txt
314+
val processResourcesTask = agpCompat.getProcessResourcesTask(output)
315+
it.dependsOn(processManifestTask)
316+
it.dependsOn(processResourcesTask)
317+
318+
it.outputs.dir(pluginManifestSourceDir).withPropertyName("pluginManifestSourceDir")
319+
320+
it.doLast {
321+
// 解析合并后的 AndroidManifest.xml + R.txt
322+
// 这种方案直接解析 XML 格式的 AndroidManifest.xml ,不再依赖 aapt2 产生的二进制产物
323+
val mergedManifest =
324+
agpCompat.getProcessManifestFile(project, pluginVariant, output)
325+
val rTxt = agpCompat.getRTxtFile(project, processResourcesTask, variantName)
326+
val manifestValueParser = createManifestValueParser(rTxt)
327+
generatePluginManifest(
328+
mergedManifest,
329+
pluginManifestSourceDir,
330+
"com.tencent.shadow.core.manifest_parser",
331+
manifestValueParser
332+
)
333+
}
334+
}
335+
javacTask.dependsOn(generatePluginManifestTask)
336+
337+
// 把PluginManifest.java添加为源码
338+
val relativePath =
339+
project.projectDir.toPath().relativize(pluginManifestSourceDir.toPath()).toString()
340+
(javacTask as JavaCompile).source(project.fileTree(relativePath))
341+
}
342+
279343
/**
280344
* 反射apkanalyzer中的BinaryXmlParser类的decodeXml方法
281345
*/

projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestKeys.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ sealed class AndroidManifestKeys {
2424
typealias ComponentMapKey = String
2525
typealias ComponentMapValue = Any
2626
typealias ComponentMap = Map<ComponentMapKey, ComponentMapValue>
27-
typealias MutableComponentMap = MutableMap<ComponentMapKey, ComponentMapValue>
27+
typealias MutableComponentMap = MutableMap<ComponentMapKey, ComponentMapValue>
28+
typealias ManifestValueParser = (String) -> String
Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.tencent.shadow.core.manifest_parser
22

33
import java.io.File
4+
import java.util.Collections
45

56
/**
67
* manifest-parser的入口方法
@@ -9,13 +10,74 @@ import java.io.File
910
* 一般位于apk工程的build/intermediates/merged_manifest目录中。
1011
* @param outputDir 生成文件的输出目录
1112
* @param packageName 生成类的包名
13+
* @param manifestValueParser 资源解析器
1214
*/
1315
fun generatePluginManifest(
1416
xmlFile: File,
1517
outputDir: File,
16-
packageName: String
18+
packageName: String,
19+
manifestValueParser: ManifestValueParser? = null
1720
) {
1821
val androidManifest = AndroidManifestReader().read(xmlFile)
1922
val generator = PluginManifestGenerator()
20-
generator.generate(androidManifest, outputDir, packageName)
21-
}
23+
generator.generate(androidManifest, outputDir, packageName, manifestValueParser)
24+
}
25+
26+
/**
27+
* 创建资源解析器。
28+
*
29+
* @param rTxt R.txt文件
30+
* @return 资源解析器
31+
*/
32+
fun createManifestValueParser(rTxt: File): ManifestValueParser {
33+
val rTxtMap = parseRTxt(rTxt)
34+
35+
return { resName ->
36+
if (resName.startsWith("@android:")) {
37+
// @android:style/Theme.NoTitleBar -> android.R.style.Theme_NoTitleBar
38+
val parts = resName.substringAfter("@android:").split("/")
39+
val type = parts[0]
40+
val name = parts[1].replace(".", "_")
41+
"android.R.$type.$name"
42+
} else {
43+
// @[package:]type/name -> id 值
44+
var raw = resName.substringAfter("@")
45+
if (raw.contains(":")) {
46+
raw = raw.substringAfter(":")
47+
}
48+
val parts = raw.split("/")
49+
val type = parts[0]
50+
val name = parts[1].replace('.', '_')
51+
val key = "@$type/$name"
52+
rTxtMap[key]
53+
?: throw IllegalArgumentException("Resource not found in R.txt: $resName (normalized: $key)")
54+
}
55+
}
56+
}
57+
58+
/**
59+
* 解析 R.txt 文件并生成资源 ID 映射表。 R.txt 包含项目引用的所有资源 ID。
60+
*
61+
* @param rTxtFile R.txt 文件对象
62+
* @return 资源全称(如 @string/app_name)到 ID 的映射
63+
*/
64+
fun parseRTxt(rTxtFile: File): Map<String, String> {
65+
if (!rTxtFile.exists()) return Collections.emptyMap()
66+
67+
val map = mutableMapOf<String, String>()
68+
rTxtFile.useLines {
69+
it.forEach { line ->
70+
if (!(line.startsWith("int "))) {
71+
return@forEach
72+
}
73+
val parts = line.split(Regex("\\s+")).filter { it.isNotBlank() }
74+
if (parts.size == 4 && parts[0] == "int") {
75+
val type = parts[1]
76+
val name = parts[2]
77+
val idStr = parts[3]
78+
map["@$type/$name"] = idStr
79+
}
80+
}
81+
}
82+
return map
83+
}

0 commit comments

Comments
 (0)