R.java 文件結構
R.java 是自動生成的,它包含了應用內所有資源的名稱到數值的映射關係。先創建一個最簡單的工程,看看 R.java 文件的內容:
R文件生成的目錄爲app/build/generated/not_namespaced_r_class_sources/xxxxxDebug/processXXXXDebugResources/r/com/xxx/xxx/R.java
R.java 內部包含了很多內部類:如 layout、mipmap、drawable、string、id 等等
這些內部類裏面只有 2 種數據類型的字段:
public static final int
public static final int[]
只有 styleable 最爲特殊,只有它裏面有 public static final int[] 類型的字段定義,其它都只有 int 類型的字段。
此外,我們發現 R.java 類的代碼行數最少也1000行了,這還只是一個簡單的工程,壓根沒有任何業務邏輯。如果我們採用組件化開發或者在工程裏創建多個 module ,你會發現在每個模塊的包名下都會生成一個 R.java 文件。
爲什麼R文件可以刪除
所有的 R.java 裏定義的都是常量值,以 Activity 爲例:
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
R.layout.activity_main 實際上對應的是一個 int 型的常量值,那麼如果我們編譯打包時,將所有這些對 R 類的引用直接替換成常量值,效果也是一樣的,那麼 R.java 類在 apk 包裏就是冗餘的了。
前面說過 R.java 類裏有2種數據類型,一種是 static final int 類型的,這種常量在運行時是不會修改的,另一種是 static final int[] 類型的,雖然它也是常量,但它是一個數組類型,並不能直接刪除替換,所以打包進 apk 的 R 文件中,理論上除了 static final int[] 類型的字段,其他都可以全部刪除掉。以上面這個爲例:我們需要做的是編譯時將 setContentView(R.layout.activity_main) 替換成:
setContentView(213196283);
ProGuard對R文件的混淆
通常我們會採用 ProGuard 進行混淆,你會發現混淆也能刪除很多 R$*.class,但是混淆會造成一個問題:混淆後不能通過反射來獲取資源了。現在很多應用或者SDK裏都有通過反射調用來獲取資源,比如大家最常用的統計SDK友盟統計、友盟分享等,就要求 R 文件不能混淆掉,否則會報錯,所以我們常用的做法是開啓混淆,但 keep 住 R 文件,在 proguard 配置文件中增加如下配置:
-keep class **.R$* {
*;
}
-dontwarn **.R$*
-dontwarn **.R
ProGuard 本身會對 static final 的基本類型做內聯,也就是把代碼引用的地方全部替換成常量,全部內聯以後整個 R 文件就沒地方引用了,就會被刪掉。如果你的應用開啓了混淆,並且不需要keep住R文件,那麼app下的R文件會被刪掉,但是module下的並不會被刪掉,因爲module下R文件內容不是static final的,而是靜態變量。
如果你的應用需要keep住R文件,那麼接下來,我們學習如何刪除所有 R 文件裏的冗餘字段。
刪除不必要的 R
對於 Android 工程來說,通常,library 的 R 只是 application 的 R 的一個子集,所以,只要有了全集,子集是可以通通刪掉的,而且,application 的 R 中的常量字段,一旦參與編譯後,就再也沒有利用價值(反射除外)。在 R 的字段,styleable 字段是一個例外,它不是常量,它是 int[]。所以,刪除 R 之前,我們要弄清楚要確定哪些是能刪的,哪些是不能刪的,根據經驗來看,不能刪的索引有:
1.ConstraintLayout 中引用的字段,例如:
<android.support.constraint.Group
android:id="@+id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="button4,button9" />
其中,R.id.button4 和 R.id.button9 是必須要保留的,因爲 ContraintLayout 會調用 TypedArray.getResourceId(int, int) 來獲取 button4 和 button9 的 id 索引。
總結下來,在 ConstraintLayout 中引用其它 id 的屬性如下:
constraint_referenced_ids
layout_constraintLeft_toLeftOf
layout_constraintLeft_toRightOf
layout_constraintRight_toLeftOf
layout_constraintRight_toRightOf
layout_constraintTop_toTopOf
layout_constraintTop_toBottomOf
layout_constraintBottom_toTopOf
layout_constraintBottom_toBottomOf
layout_constraintBaseline_toBaselineOf
layout_constraintStart_toEndOf
layout_constraintStart_toStartOf
layout_constraintEnd_toStartOf
layout_constraintEnd_toEndOf
也就是說系統通過反射來獲取的,包含反射屬性的R是不能進行刪除的,不然就會獲取不到
因此,採用瞭解析 xml 的方式,從 xml 中提取以上屬性。
其它通過 TypedArray.getResourceId(int, int) 或 Resources.getIdentifier(String, String, String) 來獲取索引值的資源
針對這種情況,需要對字節碼進行全盤掃描才能確定哪些地方調用了 TypedArray.getResourceId(int, int) 或 Resources.getIdentifier(String, String, String),考慮到增加一次 Transform 帶來的性能損耗, 可以提供通過配置白名單的方式來保留這些資源索引
刪除不必要的 Field
由於 Android 的資源索引只有 32 位整型,格式爲:PP TT NNNN,其中:
PP 爲 Package ID,默認爲 0x7f;
TT 爲 Resource Type ID,從 1 開始依次遞增;
NNNN 爲 Name ID,從 1 開始依次遞增;
爲了節省空間,在構建 application 時,所有同類型的資源索引會重排,所以,library 工程在構建期間無法確定資源最終的索引值,這就是爲什麼 library 工程中的資源索引是變量而非常量,既然在 application 工程中可以確定每個資源最終的索引值了,爲什麼不將 library 中的資源索引也替換爲常量呢?這樣就可以刪掉多餘的 field 了,在一定程度上可以減少 dex 的數量,收益是相當的可觀。
我們可以看一下,這個是app module下的R.java
這個是module下的R.java
可以很明顯發現app下是常量,library下是靜態的變量
在編譯期間獲取索引常量值有很多種方法:
1)反射 R 類文件
2)解析 R 類文件
3)解析 Symbol List (R.txt)
經過 測試發現,解析 Symbol List 的方案性能最優,因此,在 Transform 之前拿到所有資源名稱與索引值的映射關係。
關於解析 Symbol List (R.txt)的思路來源,可以參考gradle源碼
TaskManager#createNonNamespacedResourceTasks
private void createNonNamespacedResourceTasks(
@NonNull VariantScope scope,
@NonNull File symbolDirectory,
InternalArtifactType packageOutputType,
@NonNull MergeType mergeType,
@NonNull String baseName,
boolean useAaptToGenerateLegacyMultidexMainDexProguardRules) {
File symbolTableWithPackageName =
FileUtils.join(
globalScope.getIntermediatesDir(),
FD_RES,
"symbol-table-with-package",
scope.getVariantConfiguration().getDirName(),
"package-aware-r.txt");
final TaskProvider<? extends ProcessAndroidResources> task;
File symbolFile = new File(symbolDirectory, FN_RESOURCE_TEXT);
BuildArtifactsHolder artifacts = scope.getArtifacts();
if (mergeType == MergeType.PACKAGE) {
// MergeType.PACKAGE means we will only merged the resources from our current module
// (little merge). This is used for finding what goes into the AAR (packaging), and also
// for parsing the local resources and merging them with the R.txt files from its
// dependencies to write the R.txt for this module and R.jar for this module and its
// dependencies.
// First collect symbols from this module.
taskFactory.register(new ParseLibraryResourcesTask.CreateAction(scope));
// Only generate the keep rules when we need them.
if (generatesProguardOutputFile(scope)) {
taskFactory.register(new GenerateLibraryProguardRulesTask.CreationAction(scope));
}
// Generate the R class for a library using both local symbols and symbols
// from dependencies.
task =
taskFactory.register(
new GenerateLibraryRFileTask.CreationAction(
scope, symbolFile, symbolTableWithPackageName));
} else {
// MergeType.MERGE means we merged the whole universe.
task =
taskFactory.register(
createProcessAndroidResourcesConfigAction(
scope,
() -> symbolDirectory,
symbolTableWithPackageName,
useAaptToGenerateLegacyMultidexMainDexProguardRules,
mergeType,
baseName));
if (packageOutputType != null) {
artifacts.createBuildableArtifact(
packageOutputType,
BuildArtifactsHolder.OperationType.INITIAL,
artifacts.getFinalArtifactFiles(InternalArtifactType.PROCESSED_RES));
}
// create the task that creates the aapt output for the bundle.
taskFactory.register(new LinkAndroidResForBundleTask.CreationAction(scope));
}
artifacts.appendArtifact(
InternalArtifactType.SYMBOL_LIST, ImmutableList.of(symbolFile), task.getName());
// Synthetic output for AARs (see SymbolTableWithPackageNameTransform), and created in
// process resources for local subprojects.
artifacts.appendArtifact(
InternalArtifactType.SYMBOL_LIST_WITH_PACKAGE_NAME,
ImmutableList.of(symbolTableWithPackageName),
task.getName());
}
就是會在以下路徑app/build/intermediates/symbols/debug/R.txt生成文件,我們打開這個文件查看
可以看到R.txt裏就有資源和索引的對應關係
代碼實現
通過編寫gradle插件,在
這裏代碼分析實現都是參考開源項目booster下代碼
如何解析Symbol List (R.txt)
fun from(file: File) = SymbolList.Builder().also { builder ->
if (file.exists()) {
file.forEachLine { line ->
val sp1 = line.nextColumnIndex(' ')
val dataType = line.substring(0, sp1)
when (dataType) {
"int" -> {
val sp2 = line.nextColumnIndex(' ', sp1 + 1)
val type = line.substring(sp1 + 1, sp2)
val sp3 = line.nextColumnIndex(' ', sp2 + 1)
val name = line.substring(sp2 + 1, sp3)
val value: Int = line.substring(sp3 + 1).toInt()
builder.addSymbol(IntSymbol(type, name, value))
}
"int[]" -> {
val sp2 = line.nextColumnIndex(' ', sp1 + 1)
val type = line.substring(sp1 + 1, sp2)
val sp3 = line.nextColumnIndex(' ', sp2 + 1)
val name = line.substring(sp2 + 1, sp3)
val leftBrace = line.nextColumnIndex('{', sp3)
val rightBrace = line.prevColumnIndex('}')
val vStart = line.skipWhitespaceForward(leftBrace + 1)
val vEnd = line.skipWhitespaceBackward(rightBrace - 1) + 1
val values = mutableListOf<Int>()
var i = vStart
while (i < vEnd) {
val comma = line.nextColumnIndex(',', i, true)
i = if (comma > -1) {
values.add(line.substring(line.skipWhitespaceForward(i), comma).toInt())
line.skipWhitespaceForward(comma + 1)
} else {
values.add(line.substring(i, vEnd).toInt())
vEnd
}
}
builder.addSymbol(IntArraySymbol(type, name, values.toIntArray()))
}
else -> throw MalformedSymbolListException(file.absolutePath)
}
}
}
}.build()
結合debug
int anim abc_slide_in_bottom 0x7f010006
其實就是解析第一個看是int還是int[]
然後解析出type=anim,name=abc_slide_in_bottom,value=0x7f010006.toInt,然後構建IntSymbol,然後添加到一個list中 val symbols = mutableListOf<Symbol<*>>()
如果是int[]
public static int[] MsgView = { 0x7f040204, 0x7f040205, 0x7f040206, 0x7f040207, 0x7f040208, 0x7f040209 };
同樣進行解析
對多餘的R進行刪除
尋找多餘的R
private fun TransformContext.findRedundantR(): List<Pair<File, String>> {
return artifacts.get(ALL_CLASSES).map { classes ->
val base = classes.toURI()
classes.search { r ->
r.name.startsWith("R") && r.name.endsWith(".class") && (r.name[1] == '$' || r.name.length == 7)
}.map { r ->
r to base.relativize(r.toURI()).path.substringBeforeLast(".class")
}
}.flatten().filter {
it.second != appRStyleable // keep application's R$styleable.class
}.filter { pair ->
!ignores.any { it.matches(pair.second) }
}
}
這裏過濾掉了application’s R$styleable.class,還有白名單的
可以從debug中看到多餘的R文件有哪些
對R常量內聯
通過ASM對所有的class文件進行掃描,並利用其進行修改
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)}"
}
}
}
對這段代碼進行debug
以androidx/appcompat/app/AlertController.java這個類爲例子
通過如下方法過濾出可以內聯的field
("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))
例如過濾出上面這個field
查看AlertController.java中確實有用到地方
private static boolean shouldCenterSingleButton(Context context) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.alertDialogCenterButtons, outValue, true);
return outValue.data != 0;
}
即可以對R.attr.alertDialogCenterButtons進行內聯替換
代碼如下:
method.instructions.insertBefore(field, LdcInsnNode(symbols.getInt(type, field.name)))
method.instructions.remove(field)
context.getTheme().resolveAttribute(R.attr.alertDialogCenterButtons, outValue, true);
1.通過symbols.getInt(type, field.name)獲取R.attr.alertDialogCenterButtons對應的常量值
2.通過ASM在R.attr.alertDialogCenterButtons前插入這個常量值即method.instructions.insertBefore(field, LdcInsnNode(symbols.getInt(type, field.name)))
3.刪除R.attr.alertDialogCenterButtons
對於int[]的修改就簡單多了
intArrayFields.forEach { field ->
field.owner = "$appPackage/${field.owner.substring(field.owner.lastIndexOf('/') + 1)}"
}
直接將field.owner修改,從module的包路徑改爲app包名下主路徑