前言
老话常谈,我们每次引入新的优化手段,都需要详细调研,明确优缺点,以及引入这项技术或者功能,能给现有的项目带来什么收益以及带来哪些不便。
首先我们要搞明白为什么要优化包体积?普遍认为的减少包体积有以下几个好处:
- 下载转换率,体积越小下载率越高
- 如果需要和厂商合作进行预装,由于预装空间是有限的,体积越小,成本越低
- 推广一般按照流量收费,同理,安装包体积越小,成本越低
- 减少应用占用手机的储存空间,一般100M的安装包,安装到手机上后一般会占用200M,相应的对于中低端手机就不友好
接下来就详细介绍如何对图片进行优化。
背景
随着项目的不断迭代和开发人员的增多,多业务线同时进行开发,就会造成一些问题:
- 由于同时进行业务的迭代,可能会造成相同的图片不同的命名放入项目中,造成图片重复增加了包体积。
- 由于导入图片是手动行为,很可能造成导入过大的图片。
- 存在第三方的module,第三方的aar中可能存在一些图片过于大。
以上的问题都是日积月累形成的,而且难以察觉,但是积少成多,这部分的文件在包体积中,占了不小的比重,而且这个行为是一直存在的,这个时候就需要有一个自动的过程,来检查整个过程或者是优化图片占用的大小,避免包体积以一种少为人知的方式慢慢增加。
目标
我们的总目标是减少包体积,以及在以后的开发中避免类似的问题造成包体积的增加,为了达到这个总目标,我们把目标更加具体化:
- 进一步压缩图片(png,jpeg,webp)
- 找出项目中内容一样,但是命名和位置不一样的图片
- 自动化整个过程,并且对所有的开发同学来说透明
- 除了减少包体积外,不会出现crash以及显示效果
压缩图片
我们在app中的图片一般包含 .jpg
、.jpeg
、png
、webp
、.9.png
。对于图片的选择:没有透明度的需求则选择jpg和jpeg,相对png的文件体积更小。
PNG
PNG是包含透明度的图片,顾名思义就是可以设置图片的透明度,每一个色素点包含ARGB
四个通道,由于png是无损压缩的图片,对png进行压缩没有太好的收益,一般使用的工具有TinyPng这个网站,他们的算法是比较好的,可以将PNG压缩到很小(有损压缩,但是不影响观看),大部分设计师应该都知道这个网站,这个网站也有api来统一压缩图片,但是缺点是收费,每天只要超过500次就需要收费,对于商用的app来说不太友好。开源的库有pngquant,它的压缩率近似TinyPng,但是压缩速度非常的缓慢,好处是不收费。
JPEG和JPG
JPEG是没有透明度的,每个色素点包含RGB
,没有alpha通道并且是采用了标准的有损压缩算法,可以进一步的压缩图片,现在开源比较好的压缩工具有Guetzli,基本上在高质量视觉的情况下可以减小20~30%的文件大小
WebP
WebP是谷歌提供的一种支持有损压缩和无损压缩的图片文件格式,而且可以提供比JPEG或PNG更好的压缩。在Android 4.0(API level 14)中支持有损的WebP图片,在Android 4.3(API level 18)和更高的版本中支持无损和透明的WebP图像(我们的app minSdkVersion=21,所以不用担心兼容问题),更加详细的介绍可以参考官网介绍
从上面的比对中不难看出,在我们平常人的视觉中,基本是没有区别的,我们也不会放大去看,所以在满足用户视觉需求的情况下,我们可以依照自己的需求对图片进行压缩或者转换。
获取项目中所有的图片
获取所有的图片资源一般有两种方案:
- 遍历工程目录中的所有文件,找出后缀为图片格式的文件,然后进行处理,这种方案有以下几个缺陷
- 如果修改本地文件,必定会产生diff,需要生成新的commit,不合理
- 如果是第三方的aar,则遍历不出图片资源,不合理
- 在打包过程中,hook一个阶段,拿到所有的资源文件,这个阶段不会产生commit,而且可以拿到第三方aar的图片资源,更加合理。
这里需要一些额外的知识,需要了解一下apk打包过程
我们先打印出所有的Task
║ :app:checkDebugClasspath [153]
║ :app:preBuild [0]
║ :app:preDebugBuild [14]
║ :app:compileDebugAidl [1]
║ :app:compileDebugRenderscript [11]
║ :app:checkDebugManifest [1]
║ :app:generateDebugBuildConfig [13]
║ :app:mainApkListPersistenceDebug [9]
║ :app:generateDebugResValues [2]
║ :app:generateDebugResources [0]
║ :app:mergeDebugResources [2099]
║ :app:createDebugCompatibleScreenManifests [4]
║ :app:processDebugManifest [94]
║ :app:splitsDiscoveryTaskDebug [5]
║ :app:processDebugResources [420]
║ :app:compileDebugKotlin [4510]
║ :app:prepareLintJar [0]
║ :app:generateDebugSources [0]
║ :app:javaPreCompileDebug [14]
║ :app:compileDebugJavaWithJavac [714]
║ :app:compileDebugNdk [1]
║ :app:compileDebugSources [0]
║ :app:mergeDebugShaders [7]
║ :app:compileDebugShaders [5]
║ :app:generateDebugAssets [0]
║ :app:mergeDebugAssets [6]
║ :app:transformClassesWithDexBuilderForDebug [754]
║ :app:transformDexArchiveWithExternalLibsDexMergerForDebug [681]
║ :app:transformDexArchiveWithDexMergerForDebug [423]
║ :app:mergeDebugJniLibFolders [4]
║ :app:transformNativeLibsWithMergeJniLibsForDebug [294]
║ :app:checkDebugLibraries [1]
║ :app:processDebugJavaRes [1]
║ :app:transformResourcesWithMergeJavaResForDebug [415]
║ :app:validateSigningDebug [2]
║ :app:packageDebug [416]
║ :app:revertDebug [185]
║ :app:assembleDebug [0]
从上面的task中可以看到有一个generateDebugResources
的task,这个task就是生成所有的资源文件,然后需要通过反射调用MergeResources.computeResourceSetList
方法,即可获取所有资源文件的路径。
寻找内容相同图片和大图
在打包过程中能获取所有资源文件的前提下,其实这个过程想起来很简单,一般找出两个相同的资源文件有两个方案:
- 最初的方案是需要将所有的图片(png,jpeg,jpg,webp)汇集到一个集合中,每个图片跟剩下所有的图片进行比对,而且是对比两张图片的所有像素点,这样的话整个算法是非常复杂而且运算量非常的大,会极大的增加打包流程,直接pass。
- 将所有图片大小相同的文件放入一个集合中,然后使用文件摘要算法计算出一个字符串,然后直接对字符串直接对比即可找出相同内容的图片文件,然后将这两个文件的路径打印到一个文件中,就可以查看并且删除相同的资源文件了。这样就大大缩减了整个task的过程。
从上面的demo中可以看到,找出了两张大的图片,和相同的图片,这样我们就可以在项目中,将这些问题进行优化。
自动化
关于自动化,最好的实践方式就是自定义Plugin,这个过程在打包阶段,对于开发阶段来说是透明的,怎样实现自定义的Plugin,可以参考Gradle-自定义plugin,这里的难点在于获取所有资源的文件,上面已经说了大致的流程。
进一步压缩图片
从现有的资料中,在大体不失真的情况对图片压缩最多也就是减少75%,但是如果想进一步较少图片,那么就会影响用户的视觉体验,这种情况是不允许的,所以我们可以将所有的图片转换为webp
,在不影响图片质量的情况下极大的减少图片,可以看下网上的一个对比图片
将所有的图片转换为webp的一个重要原因是,在现有所有开源的压缩库中,如果对png进行压缩至25%,一张30kb的图片将耗时6-8s,如果在工程中批量压缩图片,那么增加的时长将不可想象,而将png或者jpeg转换为webp则仅仅需要不到100ms。而且在不影响性能和用户体验的前提下我们选择将图片转换为webp。
最佳实践
以上描述中需要优化的点是:图片压缩, 虽然每种类型的图片都可以压缩,但是压缩率以及压缩时间在png上表现都不是很好,一个30kb的png图片,使用pngquant压缩,需要2s左右,如果是批量压缩,那么会非常的慢,所以这里建议是如果app的 minSdkVersion >= 18
那么完全可以将所有类型的图片都转换为webp
,这样不近能极大的缩短每张图片处理时间而且图片的压缩率也非常的可观,总结下整个流程。
- 自定义plugin,hook住
generateDebugResources
Task。 - 创建自己的Task,通过反射拿到所有资源的文件。
- 找出所有的相同内容的图片,打印出来
- 如果
minSdkVersion >= 18
,则将所有的图片都转化为webp
- 生成apk
实验对比
以上整个流程达到的效果是:
- 不会引起bug或者crash
- 对业务开发的人员来说完全透明,不会影响到任何的开发效率
- 对于整个打包过程不会有太大的影响,不会过分的增加整个打包时长
channel | process | optimize |
---|---|---|
Debug | 3M -> 2.3M | 0.7M |