转载请注明出处: http://www.cnblogs.com/soaringEveryday/p/5254520.html
随着Android移动开发的需求越来越复杂,我们不可避免的遇到发布出去的apk体积越来越大的问题,目前超过10MB、20MB的apk已经是很常见的事情了,但是依然能够看到一些apk的体积控制的很小。apk体积增大源于:
- 新需求不断的提出
- 需要支持高分辨率的屏幕而加入了高分图片
- 依赖了更多的第三方库
本文将从我自己的经历项目中探讨如何有效减小apk的体积。减小Apk体积是一件很有用处的android优化手段,降低了用户需要下载的比特数,同时也降低了分发安装时失败的概率。
将apk解压后发现,体积占大头的分辨是lib文件夹、res文件夹和dex文件。所以我们的降低apk体积的策略也应当从如何缩减so文件、资源图片、控制代码质量上来入手。
使用Progruard
Proguard是Android很早就使用的代码混淆工具,除了用于混淆代码提高安全性以外,他在代码编译的时候也会通过遍历代码的方式来发现没有被调用的代码,从而将其在打包成apk时剔除,最终一定程度上降低了apk的大小。
但是Proguard使用时候是要注意的,因为代码中利用反射机制的地方会被Proguard工具破坏,所以要慎重的编写混淆例外文件,同时对于混淆后打包出来的apk要重新充分回归测试下。
使用Android Lint
Proguard提供了代码的缩减方式,而Lint对于res下面的资源进行了充分的优化,他会提供一份报告给你,从而通知你哪些资源没有被用到,显然剔除这些资源是可以减少apk体积的。这些资源包括res文件夹下所有的内容,比如图片、字串、尺寸等等。现在Android Lint已经集成到了Android Studio中,用法很简单。
进入Android Studio的菜单中选择Analyze->Inspecting Code即可
分析完毕后在Inspection选项卡中会有一份详细的报告,找到Android Lint项目
拉到下面Unused resource这一栏打开,即是未被使用的资源列表,用户可以参照来手动删除资源
清理Assert文件夹
Assert文件夹经常会放置一些不被编译的资源,时间久了,里面可能一些文件或者资源已经不用了,然而这个文件夹也是会被打包到apk里面的。所以定期清理这个里面的内容也是减小apk体积的重要一步。
用代码代替图片
开发的时候有些地方能用代码做出来的就尽量不用图片来渲染,这样子可以减少图片资源的数量从而减少体积,这里举几个例子。
- 用shape代替背景图
- 用RotateDrawable代替仅仅是方向不同的“内容相同”的图片
- 用layer-list来制作多层图片从而达到复用
- 使用属性动画而不是多图片连续播放的帧动画
用shape代替背景图
很多背景图比如按钮的背景、纯色背景都是可以用shape来制作的,这样子仅用xml代码就代替了png资源。比如这么要给按钮背景图(纯色背景、带边框、圆角)可以用shape而不是Png图片来制作
xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item> <shape> <stroke android:width="0.5dp" android:color="@color/white"/> <gradient android:startColor="#ffffff" android:endColor="#ffffff" android:angle="0.0" /> <corners android:topLeftRadius="4dp" android:topRightRadius="0dp" android:bottomLeftRadius="4dp" android:bottomRightRadius="0dp" /> shape> item> selector>
用RotateDrawable代替仅仅是方向不同的“内容相同”的图片
这里两个图片是两个按钮箭头,但是仅仅方向不同而已,其实可以只用其中一个图片即可,而另一个用RotateDrawable来让其“调转”180度
xml version="1.0" encoding="utf-8"?> <rotate xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/ic_arrow_left" android:fromDegrees="180" android:pivotX="50%" android:pivotY="50%" android:toDegrees="180" />
用layer-list来制作多层图片从而达到复用
有些需求中需要一种图片,但是明显这个图片是其他几个图片简单叠加而已,那么可以使用layer-list来达到目的
xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item> <rotate android:pivotX="0" android:pivotY="0" android:fromDegrees="-10" android:toDegrees="-10"> <bitmap android:src="@drawable/chatting_bg_default_thumb"/> rotate> item> <item> <rotate android:pivotX="0" android:pivotY="0" android:fromDegrees="15" android:toDegrees="15"> <bitmap android:src="@drawable/chatting_bg_purecolor_thumb"/> rotate> item> <item> <rotate android:pivotX="0" android:pivotY="0" android:fromDegrees="35" android:toDegrees="55"> <bitmap android:src="@drawable/mark"/> rotate> item> layer-list>
使用属性动画而不是多图片连续播放的帧动画
这里就不过多解释了,比如你做一个图片的旋转,通过属性动画即可用代码搞定。如果你要用多张图片“连续播放”的话,那要用很多张预备图片才行,无疑加大了apk的体积
放弃一些图片资源
我们知道Android是支持多分辨率的,提供了多套图片适配的机制,我们根据不同尺寸的屏幕来放入多套的图片,比如ldpi、mdpi、hdpi、xhdpi等。但是实际上有些图片你可能是不需要放置的。
- 对于ldpi,系统会自动的将hdpi的图片缩放到达到目的,所以你就不需要把ldpi的图片拷贝到你的res文件夹下了
- 有些分辨率的图片你可能是用不到的,比如ldpi和xxxhdpi。前者的手机目前很少了,而后者是针对2k屏幕的,目前还没有普及开。所以对于这点,你可以考虑下是否需要放入这两套图片资源
此外,我们所引入的第三方包中可能也引用了资源图片,但是其中的某些我们可能是不想要的,比如ldpi和xxxhdpi,那么是否可以设置什么东西来让打包的时候剔除他们呢?是可以的,可以配置下build.gradle
defaultConfig {
// ...
resConfigs "en", "de", "fr", "it"
resConfigs "hdpi", "xhdpi", "xxhdpi"
}
defaultConfig提供了resConfig这个flavor来指定打包出只打包某些资源,比如字串、图片等等
压缩图片
图片从美工那边拿到的时候是比较漂亮的,但是代价是size通常也高居不下,我们可以用一些工具来“近乎无损”的缩小图片资源,同时不降低图片效果。这里推荐pngquant这个工具,可以参考我的上篇博客
http://www.cnblogs.com/soaringEveryday/p/5148881.html
so的优化
我们在接入百度地图的时候,发现需要引入很多很多so
这些so文件占了很多大体积,如果你不加控制,所有的so都会打包到你的apk了,最后发现这些so文件尽然占了我们apk的近乎三分之一的体积。然而考虑下我们的用户,基本都是跑在手机上的(没有人跑在模拟器上),所以明显x86和x86_64的so是不需要支持,那么我们可以通过配置gradle来制定只打包某些so,依然是在defualtConfig中:
defaultConfig {
... ...
ndk {
//设置支持的SO库架构
abiFilters 'arm64-v8a', 'armeabi' //, 'x86', , 'x86_64', 'arm64-v8a'
}
}
最后打包出来的apk真的是减少了好几个MB,这是太好了
当然如果有一些so是你们自己开发的,那么可以参考这个文章来参考如果用ndk开发的时候减少so本身的体积,这里就不过多介绍了
https://blog.algolia.com/android-ndk-how-to-reduce-libs-size/
对第三方库进行重新定制(重新打jar包)
开发中引入大量的第三方开发库也是一个增加apk体积的重要原因,因为你把人家的代码和资源全给包含进来了。但是想想人家的代码,并不一定全要的,是否可以只引入人家的一部分代码,而不是在build.gradle中仅仅添加一行“compile”来全部依赖呢?答案是可以得!这里举一个例子
我们开发中有一个需求是将数据通过图标的方式显示出来,这里我们站在巨人的肩膀上,使用了MPAndroidChart这个优秀的开源项目(https://github.com/PhilJay/MPAndroidChart),但是发现他们的东西太多了,我们仅仅需要使用其中一种chart。如果在build.gradle里面加一句:
dependencies {
compile 'com.github.PhilJay:MPAndroidChart:v2.2.3'
}
这要把他们的库全给引用过来了。想到他们是开源的,代码有,所以我们仅仅把他们的我们所用到的代码给剥离出来,单独打包了一个jar包引入到我们的项目里面,就OK了,减少了大量的无用依赖代码!
动态加载技术(插件化)
现在大型互联网移动App很多都采用了动态加载的技术,因为他们的业务需求太大,通过动态加载技术可以将一部分业务模块独立出来,以插件的方式分割出去,这样子主apk的体积就大大减小。当用户安装主apk后,静默的在后台下载插件apk,当用户点击使用到相关的子模块项目时候,动态的加载插件apk。
动态加载技术无疑从根本上减少了apk的体积,但是引入这个技术是有代价的,增加了项目的维护难度和开发难度。所以该技术适用于大型的移动应用,当你的业务大到不分开模块难以高效率开发维护的时候,再考虑动态加载技术吧,否则如果小规模应用,还是老老实实考虑传统的android官方推荐的开发方式。
这里推荐几个比较好的动态加载开源框架项目供大家研究
- dynamic-load-apk
- 360DroidPlugin