40、资源优化:资源引用分析、无用资源清理、资源国际化、资源适配策略

说到资源优化,我脑子里第一个蹦出来的画面,是几年前接手的一个“祖传项目”。

那个APK解压出来,光图片资源就占了30多兆。里面甚至有十几张完全一样的按钮背景图,只是文件名不同。我当时就一个感觉——这哪是App,这分明是个图库。

资源优化这件事,说白了就是给APK“减脂”。你想想看,用户下载一个App,结果一半流量都花在下载那些根本用不上的图片和布局上,多冤啊。今天我们就聊聊怎么把这部分“赘肉”减掉。

资源引用分析:搞清楚谁在用

动手清理之前,得先知道哪些资源是“活”的,哪些是“死”的。我个人习惯用Android Studio自带的工具来扫一遍。

Lint检查是最直接的手段。在菜单栏点 Analyze → Inspect Code,然后选 Android → Lint → Performance → Unused resources。跑完之后,它会列出一份清单,告诉你哪些资源没有被任何代码引用。

核心思路:资源引用分析的本质,就是建立一张“资源-代码”的引用关系图。从代码入口(Activity、Fragment、布局文件、样式主题)出发,遍历所有R.id、R.drawable、R.string等引用,找出那些没有任何入边的孤立节点。

不过这里有个坑。Lint只能分析静态引用。如果你用 getIdentifier() 或者 Resources.getDrawable() 传字符串拼接的资源名,Lint是查不到的。我在项目中遇到过这种情况:一个动态换肤功能,通过反射加载图片,结果Lint报告说这些图片“未使用”。

注意:对于动态引用的资源,建议在资源文件名上加上统一前缀(比如 dynamic_),然后在Lint配置文件中添加白名单,避免误删。

无用资源清理:该删就删,别手软

分析完了,接下来就是动手删。但别急着全选删除,我建议分三步走:

  1. 确认引用链:有些资源虽然代码里没直接引用,但可能被主题、样式或者第三方库间接引用。比如 values/themes.xml 里引用的某个颜色值,删了可能导致整个UI变色。
  2. 使用Refactor功能:在Lint结果中右键点击未使用的资源,选择 Refactor → Remove Unused Resource。Android Studio会自动处理相关的R文件引用。
  3. 清理第三方库的冗余资源:很多第三方库会带一堆用不上的资源。比如你只用了某个图片加载库的缓存功能,但它却带了十几张示例图片。这时候可以用 shrinkResources true 配合 resConfigs 来裁剪。
// build.gradle 中的配置示例
android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true  // 开启资源压缩
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    
    defaultConfig {
        // 只保留中文和英文资源
        resConfigs "zh", "en"
    }
}

小技巧:开启 shrinkResources 后,Gradle会在打包时自动移除未引用的资源。但它依赖ProGuard的代码混淆结果——如果代码里某个资源引用被混淆规则误删了,资源也会被连带删除。所以一定要在Release包上做完整的回归测试。

我曾经在一个项目里,因为 shrinkResources 误删了某个启动页的图片,导致线上版本启动页一片空白。嗯,从那以后我养成了一个习惯:每次开启资源压缩后,都会手动对比一下APK里的资源列表,确保没有“误伤”。

资源国际化:别让翻译成为噩梦

国际化这件事,说白了就是让你的App能说“人话”。不同地区的人,说的语言不一样,用的数字格式、日期格式、货币符号也不一样。

Android的资源系统天生支持国际化。你只需要在 res 目录下创建对应的 values-语言代码 文件夹就行。比如:

  • values/strings.xml — 默认(通常是英文)
  • values-zh/strings.xml — 简体中文
  • values-ja/strings.xml — 日语

但这里有个容易忽略的点:字符串格式化。不同语言的语序不一样。比如中文说“打开文件 %s”,英文说“Open file %s”,看起来没问题。但有些语言会把文件名放在前面,动词放在后面。这时候如果你在代码里写死了字符串拼接,翻译就全乱了。

// 错误做法:字符串拼接
String message = "欢迎 " + userName + " 回来";

// 正确做法:使用占位符
// strings.xml 中定义:<string name="welcome_back">欢迎 %s 回来</string>
String message = getString(R.string.welcome_back, userName);

最佳实践:所有需要拼接的文本,都用 %s%d 等占位符写在strings.xml里。代码里只负责传参数。这样翻译人员只需要调整语序,不需要动代码。

另外,图片和布局也要考虑国际化。比如某些文化中,红色代表喜庆,但在另一些文化中可能代表警告。我建议把这类“文化敏感”的资源也放到对应的 drawable-语言代码 目录下。

资源适配策略:一套资源,多端适配

资源适配,核心就一句话:用最少的资源,覆盖最多的设备

Android设备屏幕尺寸、密度、方向千差万别。如果每个尺寸都放一套图,APK体积会爆炸。我的策略是这样的:

资源类型 适配策略 个人建议
图片(位图) 只提供xxhdpi一套,配合 android:anyDensity 缩放 如果对画质要求高,可以再加一套xxxhdpi
图标(矢量) 全部使用VectorDrawable,一套搞定所有密度 注意兼容性,API 21以下需要兼容库
布局 使用ConstraintLayout + 百分比布局,减少多布局文件 平板和横屏场景,单独提供 layout-sw600dplayout-land
主题/样式 使用 values-night 提供暗黑模式适配 颜色值用主题属性引用,不要硬编码

这里我想重点说说 VectorDrawable。这东西真的是资源优化的神器。一套矢量图,从手机到电视都能用,而且文件大小只有位图的十分之一甚至更少。我现在的项目里,所有图标都换成了矢量图,APK体积直接少了8兆。

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <path
        android:fillColor="#FF000000"
        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2V7h2v2z"/>
</vector>

不过矢量图也不是万能的。复杂的渐变、阴影、纹理效果,用矢量图描述起来很费劲,而且渲染性能反而不如位图。我的原则是:扁平化图标用矢量,写实风格用位图

适配的另一个维度:别忘了 res/rawres/font。字体文件也可以按语言适配,比如中文用思源黑体,英文用Roboto。这样既能保证显示效果,又能减少字体文件体积。

知识体系总览

说了这么多,我把资源优化的核心逻辑画了张图,方便你理解整体脉络:

Android资源优化 资源引用分析 Lint静态检查 动态引用白名单 引用关系图遍历 无用资源清理 shrinkResources resConfigs裁剪 第三方库冗余清理 资源国际化 values-语言代码 字符串占位符 资源适配策略 VectorDrawable矢量图 密度/方向/尺寸适配 目标:APK体积最小化 + 多端覆盖最大化

你看,资源优化其实就这四个维度。先分析清楚哪些资源在用,然后果断清理无用的,接着做好国际化支持,最后用一套聪明的适配策略覆盖所有设备。每一步都不复杂,但合在一起,效果非常明显。

我记得有一次,一个同事问我:“为什么我的App才几个功能,APK却有50兆?”我帮他跑了一遍Lint,发现他引用的一个图片加载库带了30多张示例图片。删掉之后,APK直接降到18兆。他当时那个表情,我到现在都记得。

资源优化就是这样——你花半天时间清理,用户省下的却是每次下载时的几十兆流量。这笔账,怎么算都划算。


公众号:蓝海资料掘金营,微信deep3321