| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.build.shrinker.usages |
| |
| import com.android.SdkConstants.DOT_DEX |
| import com.android.build.shrinker.ResourceShrinkerModel |
| import com.android.build.shrinker.obfuscation.ClassAndMethod |
| import com.android.build.shrinker.usages.AppCompat.isAppCompatClass |
| import com.android.ide.common.resources.usage.ResourceUsageModel |
| import com.android.resources.ResourceType |
| import com.android.tools.r8.references.MethodReference |
| import java.nio.file.Files |
| import java.nio.file.Path |
| |
| /** |
| * Records resource usages, detects usages of WebViews and {@code Resources#getIdentifier}, |
| * gathers string constants from compiled .dex files. |
| * |
| * @param root directory starting from which all .dex files are analyzed. |
| */ |
| class DexUsageRecorder(val root: Path) : ResourceUsageRecorder { |
| |
| override fun recordUsages(model: ResourceShrinkerModel) { |
| // Record resource usages from dex classes. The following cases are covered: |
| // 1. Integer constant which refers to resource id. |
| // 2. Reference to static field in R classes. |
| // 3. Usages of android.content.res.Resources.getIdentifier(...) and |
| // android.webkit.WebView.load... |
| // 4. All strings which might be used to reference resources by name via |
| // Resources.getIdentifier. |
| |
| Files.walk(root) |
| .filter { Files.isRegularFile(it) } |
| .filter { it.toString().endsWith(DOT_DEX, ignoreCase = true) } |
| .forEach { path -> |
| runResourceShrinkerAnalysis( |
| Files.readAllBytes(path), |
| path, |
| DexFileAnalysisCallback(path, model) |
| ) |
| } |
| } |
| } |
| |
| private class DexFileAnalysisCallback( |
| private val path: Path, |
| private val model: ResourceShrinkerModel |
| ) : AnalysisCallback { |
| companion object { |
| const val ANDROID_RES = "android_res/" |
| |
| private fun String.toSourceClassName(): String { |
| return this.replace('/', '.') |
| } |
| } |
| |
| // R class methods should only be processed for reachable resource IDs. R class fields that are |
| // not referenced should not be considered since there is no usage in the program. |
| // If the fields have been inlined, the values at the callsite will be recorded when visited. |
| var isRClass: Boolean = false |
| |
| // In cases where a value from a method is inlined into a constant, we should still mark the |
| // resource as used. |
| val visitingMethod = MethodVisitingStatus() |
| |
| override fun shouldProcess(internalName: String): Boolean { |
| isRClass = isResourceClass(internalName) |
| return true |
| } |
| |
| /** Returns whether the given class file name points to an aapt-generated compiled R class. */ |
| fun isResourceClass(internalName: String): Boolean { |
| val realClassName = |
| model.obfuscatedClasses.resolveOriginalClass(internalName.toSourceClassName()) |
| val lastPart = realClassName.substringAfterLast('.') |
| if (lastPart.startsWith("R$")) { |
| val typeName = lastPart.substring(2) |
| return ResourceType.fromClassName(typeName) != null |
| } |
| return false |
| } |
| |
| override fun referencedInt(value: Int) { |
| // Avoid marking R class fields as reachable. |
| if (shouldIgnoreField()) { |
| return |
| } |
| val resource = model.resourceStore.getResource(value) |
| if (ResourceUsageModel.markReachable(resource)) { |
| model.debugReporter.debug { |
| "Marking $resource reachable: referenced from $path" |
| } |
| } |
| } |
| |
| override fun referencedStaticField(internalName: String, fieldName: String) { |
| // Avoid marking R class fields as reachable. |
| if (shouldIgnoreField()) { |
| return |
| } |
| val realMethod = model.obfuscatedClasses.resolveOriginalMethod( |
| ClassAndMethod(internalName.toSourceClassName(), fieldName) |
| ) |
| |
| if (isValidResourceType(realMethod.className)) { |
| val typePart = realMethod.className.substringAfterLast('$') |
| ResourceType.fromClassName(typePart)?.let { type -> |
| model.resourceStore.getResources(type, realMethod.methodName) |
| .forEach { ResourceUsageModel.markReachable(it) } |
| } |
| } |
| } |
| |
| override fun referencedString(value: String) { |
| // Avoid marking R class fields as reachable. |
| if (shouldIgnoreField()) { |
| return |
| } |
| // See if the string is at all eligible; ignore strings that aren't identifiers (has java |
| // identifier chars and nothing but .:/), or are empty or too long. |
| // We also allow "%", used for formatting strings. |
| if (value.isEmpty() || value.length > 80) { |
| return |
| } |
| fun isSpecialCharacter(c: Char) = c == '.' || c == ':' || c == '/' || c == '%' |
| |
| if (value.all { Character.isJavaIdentifierPart(it) || isSpecialCharacter(it) } && |
| value.any { Character.isJavaIdentifierPart(it) }) { |
| model.addStringConstant(value) |
| model.isFoundWebContent = model.isFoundWebContent || value.contains(ANDROID_RES) |
| } |
| } |
| |
| override fun referencedMethod( |
| internalName: String, |
| methodName: String, |
| methodDescriptor: String |
| ) { |
| if (isRClass && visitingMethod.isVisiting && visitingMethod.methodName == "<clinit>") { |
| return |
| } |
| if (internalName == "android/content/res/Resources" && |
| methodName == "getIdentifier" && |
| methodDescriptor == "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I" |
| ) { |
| // "benign" usages: don't trigger reflection mode just because the user has included |
| // appcompat |
| if (isAppCompatClass(internalName.toSourceClassName(), model.obfuscatedClasses)) { |
| return |
| } |
| model.isFoundGetIdentifier = true |
| // TODO: Check previous instruction and see if we can find a literal String; if so, we |
| // can more accurately dispatch the resource here rather than having to check the whole |
| // string pool! |
| } |
| if (internalName == "android/webkit/WebView" && methodName.startsWith("load")) { |
| model.isFoundWebContent = true |
| } |
| } |
| |
| override fun startMethodVisit(methodReference: MethodReference) { |
| visitingMethod.isVisiting = true |
| visitingMethod.methodName = methodReference.methodName |
| } |
| |
| override fun endMethodVisit(methodReference: MethodReference) { |
| visitingMethod.isVisiting = false |
| visitingMethod.methodName = null |
| } |
| |
| private fun shouldIgnoreField(): Boolean { |
| val visitingFromStaticInitRClass = (isRClass |
| && visitingMethod.isVisiting |
| && (visitingMethod.methodName == "<clinit>")) |
| return visitingFromStaticInitRClass || |
| isRClass && !visitingMethod.isVisiting |
| } |
| |
| private fun isValidResourceType(className: String): Boolean = |
| className.substringAfterLast('.').startsWith("R$") |
| } |