十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

APK体积优化实战:从50MB到15MB的完整压缩指南

APK体积优化实战:从50MB到15MB的完整压缩指南 50MB的安装包功能还不是什么重型应用这种体验谁用谁知道。我当时负责的一个工具类App功能不复杂但发布前我随手看了眼release包体积差点以为看错了。用户反馈里开始频繁出现下载太慢安装失败手机提示存储空间不足再加上渠道后台显示弱网环境下下载转化率明显掉了一截APK体积优化这件事已经不是要不要做的问题而是再不做就影响业务了。这篇文章就是记录我把APK从50MB一路压到15MB的完整过程。里面没有玄学全是能直接上手用的分析思路、压缩手段和验证方法。不管你是刚入门Android、准备发版的独立开发者还是老项目里接到包体优化任务的团队开发这篇文章都能给你一套可以照着做的方案。我会把每个环节为什么这么做也讲清楚因为只有理解了底层逻辑你才知道在什么场景下该用什么手段。1. 先从数据下手把50MB的实际构成摸清楚任何优化动作的第一步不是动手改代码而是搞清楚这50MB到底装了什么。1.1 用APK Analyzer做第一时间定位Android Studio自带一个非常好用的工具Build Analyze APK...。选中你打出来的release包它会直接展开成文件树列出每个目录和文件的原始大小、下载大小、占整包比例。我第一次用这个工具时发现自己凭感觉猜的应该是代码写太多完全不对实际情况是资源文件占了近一半体积。一个典型的50MB APK结构可能长这样组成部分典型大小说明res/20MB图片、布局、着色器等资源lib/12MB各种so库包含多个ABI版本assets/10MB内置字体、协议文件、预置数据classes.dex6.5MB编译后的Java/Kotlin字节码其他1.5MBMETA-INF签名、manifest等有了这张表优先级就非常清楚了先动res再动lib然后是assets和dex。1.2 用Gradle构建报告建立体积基线Analyze APK是单个包的一次性分析但项目要长期维护更需要一个可以反复对比的基线。我推荐在Gradle里开这两个配置android { buildTypes { release { // 开启资源收缩配合R8把未使用的资源剔除 shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } // 输出构建时的依赖分析报告 // app/build/reports/ 下会有相关输出 lint { checkReleaseBuilds true abortOnError false } }另外腾讯的Matrix工具链里有一个APK Checker可以把包体分析集成到CI里每次构建后自动输出体积报告和上一次做对比。如果你团队还没有类似的自动化设施至少做到手动记录每次发版的体积把它当成和崩溃率一样的核心指标来跟踪。没有基线你后面做的每一步优化都无法评估真实效果。1.3 不能忽略的下载大小与安装大小的区别APK Analyzer里每个文件会显示两项数据Raw File Size原始大小和Download Size下载大小。两者差异来自压缩算法。APK本身是一个zip包安装时会解压。所以即使两个APK的原始大小相同因为内部文件格式不同比如里面放的是本来就是高压缩率的mp4还是低压缩率的png下载体验也会完全不同。优化时你要盯着Download Size看这才是用户实际消耗的流量。我当时就是盯着这两个数据发现assets里放了一个24MB的MP4引导动画原始文件和下载大小几乎一样占了整个包的一半。这个发现直接改变了后续决策。2. 图片资源是最大的失血点压缩、换格式、去重的组合打法很多APK体积问题的根源不在代码在图片。处理图片也是所有优化手段里见效最快、操作门槛最低的一步。2.1 先给图片分类再决定优化策略不是所有图片都适合无脑压缩。我把项目里的图片分成几类分别采取不同策略图标类Icon体积小但数量多适合转矢量图或WebP。插画/引导图Illustration体积中大适合WebP有损压缩。大尺寸背景图适合降采样 WebP。动画帧序列Frame Animation优先考虑用Lottie或视频替代。九宫格.9.png这类不要动它的格式保持png即可但可以压缩内容。重点提醒任何经过压缩的图片都要在真机上看一遍效果。尤其是带有渐变、噪点纹理或者暗色背景的图有损压缩后容易出现色块断裂这种问题在设计师的显示器上不一定看得出来。2.2 WebP转换实操与收益估算WebP是目前Android端性价比最高的图片格式相同视觉效果下体积通常比png小30%-70%。Android Studio自带了一个转换入口右键点击drawable目录下的png文件选择Convert to WebP。不过我实际用下来批量化处理时更习惯用命令行工具cwebp# 转换为有损WebPquality 80 在视觉和体积之间比较均衡 cwebp -q 80 input.png -o output.webp # 带alpha通道的图推荐无损WebP # 无损模式的WebP对带透明区域的图标压缩率依然很可观 cwebp -lossless input.png -o output.webp对一张单张2MB的png背景图转成q75的WebP后通常能压到300-500KB接近以前1/5的体积。当然WebP也不是完全没缺点API 18以下的老机型需要额外适配但现在minSdk基本都在21往上可以放心用。2.3 用矢量图替换大面积纯色图标如果项目里有一批纯色、形状规则的图标直接换成VectorDrawable体积可以从几KB变成几百字节。例如一个关闭按钮图标vector xmlns:androidhttp://schemas.android.com/apk/res/android android:width24dp android:height24dp android:viewportWidth24 android:viewportHeight24 path android:fillColor#FF000000 android:pathDataM19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z/ /vector这段XML描述的关闭图标比任何一张位图都小。但要记住VectorDrawable对复杂插画是无能为力的强行用pathData去描复杂图形不仅体积大渲染性能还会下降。矢量图只适合简单图标。2.4 资源去重与大图降采样lint工具里有一个检查项叫UnusedResources能扫描出没有被引用的资源。你可以在菜单栏Run Inspect Code里触发也可以在Gradle里配置shrinkResources true让构建过程自动剔除未使用资源。但lint只能发现完全没有引用的资源发现不了引用了但根本不需要那么大的图。比如一张4096x4096、6MB的背景图实际使用时只占屏幕一小块这时候就应该做降采样。我踩过这个坑某页面背景图加载后App内存直接涨了80MB缩小到2048x2048之后包体和内存同时降下来了。同一个操作解决两个问题这种优化最划算。经过这一轮处理我的项目里res目录整体从20MB降到了9MB左右。图片这关过了包体已经轻了20%。3. 代码层减负R8压缩、资源混淆与依赖整理图片处理完后下一个大头就是代码和第三方库。这里要做的不是少写代码而是让构建工具把没用到的代码和重复的库全部干掉。3.1 正确开启R8并定制keep规则从AGP 3.4.0开始ProGuard被R8取代压缩、优化、混淆都在R8里完成。开启方式android { buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } }R8会把未被引用的类、方法、字段都删掉同时把类名混淆成短名。这是最有效、也最容易出问题的步骤。最常见的问题是混淆后运行时ClassNotFoundException、方法找不到所以keep规则写全非常重要。我的经验是反射调用的类要keepJNI方法要keep被注解标注的类要keepGson/Java序列化涉及的Bean类要keep# 示例需要keep的规则 -keep class com.example.data.model.** { *; } -keep class * implements java.io.Serializable { *; } -keepclasseswithmembernames class * { native methods; }开启R8后我项目的classes.dex从6.5MB降到了4.2MB左右。如果项目里没有大量keep规则这一步收益会更明显。3.2 用AndResGuard做资源混淆进一步压榨路径和资源名资源和代码处理完之后还能从路径命名上再做一次优化。AndResGuard是一个开源工具它可以把res/drawable-hdpi/ic_launcher_xxx.png这样的长路径缩短成r/d/xxx.png资源文件名也变成a、b、c这样的短命名。这个操作对APK体积的直观影响不大但能减少zip条目数量对下载和解压有一定帮助。配置只需要在根build.gradle里加插件然后在app模块配置apply plugin: AndResGuard andResGuard { mappingFile file(./resource_mapping.txt) use7zip true useSign true keepRoot false // 保留指定资源比如部分第三方SDK要求资源名不能被改 keepResName [ R.string.app_name, R.drawable.ic_launcher ] compressFilePattern [ *.png, *.jpg, *.webp, *.so ] }这里有几个关键点。use7zip true会使用7zip对APK做二次压缩部分场景下能额外减少几个百分点。keepResName很重要如果有的地方用getIdentifier()动态获取资源ResGuard混淆后运行时就会拿不到资源导致崩溃。遇到这类情况要么把动态获取改成静态引用要么把这些资源加入keep列表。3.3 第三方库的称重干掉重复和无用的依赖R8能压缩代码但压缩不了你引入了一个很重但只用了一个方法的库这种情况。用Gradle的依赖报告先摸个底./gradlew :app:dependencies --configuration releaseRuntimeClasspath我在这份报告里发现过不少问题多个库都依赖了Gson但版本不一致Gradle虽然会解析到高版本但同一个类在低版本库里的打包逻辑还是可能重复。某个图片加载库只用了加载GIF的功能但整个库的重量比项目里其他模块加起来都大。几个工具库的功能自己几行代码就能实现比如某个字符串处理库实际只用了它的isEmpty方法。这类问题的解法是能删就删能替换就替换能自己写的就自己写。我做了一个表格把每个第三方库的体积、功能、使用频次列出来逐一过堂。这一步做完dex又小了一圈同时减少了多种类在运行时的方法数压力。4. so库不是越多越好ABI筛选与动态化思路如果你的项目里有本地代码比如集成了OpenCV、FFmpeg、或者某个商业SDKlib目录通常就是体积的隐形炸弹。4.1 理解ABI别再为用不上的CPU架构打包Android设备常见的CPU架构有armeabi-v7a、arm64-v8a、x86、x86_64。每个架构都要一套对应的so文件所以同一个库支持四种架构的体积和支持一种架构的体积能差出3倍以上。现在主流设备清一色arm64-v8aarmeabi-v7a还在部分老设备上存活x86和x86_64基本只出现在模拟器上。所以我的做法是在abiFilters里只保留arm64-v8a和armeabi-v7aandroid { defaultConfig { ndk { abiFilters arm64-v8a, armeabi-v7a } } }如果你连老设备都不想管只保留arm64-v8alib目录体积还能再砍掉近40%。但对用户量大的App我建议还是保留armeabi-v7a做兼容毕竟部分低端机和中低端平板还在用32位so库。这一轮优化我的lib目录从12MB降到了6MB左右。4.2 进阶思路把so库从主包里拆出去动态加载如果单一abi过滤还不够可以考虑把某个大so库从APK里完全移除在App第一次用到对应功能时再动态下载到本地用ReLinker这类库做加载。做法大致是把so文件上传到自己的CDN或服务端。App启动后检测本地是否存在该so不存在则异步下载。下载完用ReLinker.loadLibrary()替代System.loadLibrary()。这个方案的收益非常明显但代价也大首次进入功能需要等待下载弱网环境下体验会差。一定得有下载进度提示、断点续传、下载失败后的兜底方案。我自己只在非核心功能上用了这个方案核心路径的so库依然保留在主包里。优化是取舍不是蛮干。4.3 assets里的内置文件能外置就外置assets目录是另一个看不见的体积黑洞。字体文件、内置音效、预置的json配置、离线数据包都会堆在这里。我当时的处理思路是类似的主包assets里只保留App启动必需的文件其余低频使用的内置文件全部挪到服务器按需下载。比如内置字体从3MB降到只保留一两种字重。预置的离线词库则做成首次启动后后台静默下载这样既不阻塞用户使用又能把包体降下来。5. 把步骤串起来从50MB到15MB的完整执行顺序前面的手段都是孤立的技术点真正有效的落地方式是排好执行顺序每做完一步都验证一次避免改出问题后定位困难。5.1 分阶段执行表和收益预估以下是我实践下来的推荐顺序和大致收益数值基于当时的项目情况不同项目会有浮动阶段操作内容预计效果第一阶段分析体积构成建立基线明确优化方向第二阶段图片压缩、WebP转换、矢量图替换、降采样减少约8-10MB第三阶段开启R8和shrinkResources审查keep规则减少约1.5-2.5MB第四阶段AndResGuard资源混淆减少约0.5-1MB第五阶段第三方依赖去重和替换减少约1-2MB第六阶段ABI filter只保留主流架构减少约3-6MB第七阶段assets和so动态化减少约5-10MB按这个顺序每一步的收益都能单独验证不会出现改完不知道是哪一步引起的崩溃这种情况。5.2 每阶段的验证清单每完成一个阶段至少要过这几道验证功能回归主要业务流程走一遍特别是涉及图片、资源的页面。老机型兼容在低端Android 8.0设备上跑一次完整流程。弱网模拟用Charles或Network Link Conditioner模拟慢速网络确认首次下载、动态加载的交互是正常的。崩溃监控灰度发布到外部测试群观察Crash率是否有异常上升。我在做图片压缩时曾经因为一张WebP图在低端机上解码失败导致Crash还好灰度阶段就发现了。这类问题不进灰度直接上线的话影响面会非常大。5.3 最终结果复盘50MB到15MB按上面这套流程走完我当时的APK各模块结构变成了这样组成部分优化前优化后res/20MB5MBlib/12MB3MBassets/10MB4MBclasses.dex6.5MB4MB其他1.5MB1MB整包从50MB降到17MB后来又做了一轮resources和dex的细节调优最终稳定在15MB左右。发布后那个版本在下载转化率和安装成功率上都有明显回升用户关于下载慢的反馈基本消失。6. 让包体不再反弹体积监控与踩坑备忘瘦身容易保持难。没有监控机制三个版本之后包体体积可能就悄悄涨回30MB了。6.1 在CI里加一道体积门禁我建议把包体大小纳入CI检查项。用apkanalyzer命令行工具可以很方便地获取当前APK核心体积数据# 获取APK各部分的原始大小 $ANDROID_HOME/cmdline-tools/latest/bin/apkanalyzer apk summary app-release.apk # 获取指定内容大小 apkanalyzer files list app-release.apk我当时的做法是写了一个简单的Gradle Task解析出APK总大小、dex大小、res大小、lib大小输出到构建日志然后拿它和上次发布版本的基线做对比。如果超出设定阈值就让CI报警。这样团队每次发版都能看到包体变化有意识地防止体积回归。6.2 我踩过的坑每个坑背后都是一个线上事故的种子这里列几个我亲身经历过的坑都是常规文档里不会写的。R8 keep规则不足导致运行时崩溃某个反射调用的类没加到keep里上线后一打开设置页就崩。后来我把反射、注解、序列化相关的类都系统性地加进了keep规则并在每次发版前跑了一遍反射相关的冒烟测试。AndResGuard混淆后getIdentifier拿不到资源SDK里有代码用字符串拼资源名动态获取id资源混淆后全部失效。解决方法是把相关资源名加进keepResName或者修改调用方式用静态的R.id引用替代。WebP把有渐变背景的插画压出明显色带这个对视觉影响很大但不容易被开发发现。后来我要求所有设计师给的插画原图先检查颜色复杂度再做WebP转换转换后必须在深色模式下看一眼。只保留arm64-v8a后模拟器和个别32位真机报错如果你测试时习惯用模拟器会发现x86架构的模拟器直接装不了只有arm64的包。建议测试用arm版本的镜像或者保留armeabi-v7a做保底。动态so下载失败时的处理我最初的方案是下载失败就弹Toast提示后来发现用户根本不看Toast。改成在功能页展示明确的正在加载组件状态同时提供重试按钮数据好了很多。6.3 还可以继续下探的空间如果15MB还想继续瘦可以尝试的方向有App BundleAAB按条件分发Google Play支持按语言、屏幕密度、ABI分别打包用户下载到的只是适合自己设备的那一部分。做过之后用户实际下载体积通常能再降20%左右。资源语言过滤如果App只支持中英文resConfigs zh-rCN, en可以裁掉其他语言的字符串资源。动态功能模块Dynamic Feature Module把低频功能模块化按需下载适合大型项目。代码混淆时压缩字符串R8里也可以设置optimize后的字符串精简但需谨慎处理常量引用。不过说句实话体积降到一定程度后继续优化带来的用户感知提升已经非常有限。这时候更有价值的思路是审视业务本身这个功能真的需要这个库吗这段逻辑能不能精简我在实际项目里最大的体会是包体优化不是一次性任务而是一种工程习惯。建立数据分析的习惯、建立每次改动的保留基线、建立自动化门禁比任何一次性的压包动作都更持久有效。最后再分享一个很实用的小技巧在每次合并请求描述里贴一行构建后的APK体积数据当整个团队都关注这个数字时它就会一直保持健康。
返回列表