Android Apk瘦身方案1——R.java文件常量內聯

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包名下主路徑

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章