写在前面
最近刚做了一波apk瘦身优化,瘦身后apk大小降低了19%左右。打铁要趁热,赶紧记录一下先。
APK Analyzer的简单使用
工欲善其事,必先利其器。就像分析卡顿问题要善用Systrace和TraceView,分析内存问题要善用Profiler和MAT一样。Apk瘦身优化,有工具来帮我们也会省心很多,而这个工具就是APK Analyzer了。
APK Analyzer是集成在Android Studio中的工具,直接将apk拖到Studio或者选择Build
-> Analyze APK...
都能够打开。
APK Size代表原始的安装包大小,Download Size代表经过Google Play压缩后。我们的apk不通过Google Play渠道的,所以关注的是APK Size。
图中的下半部分是Apk的结构,从上到下按大小排序,所以哪些地方最有优化空间也是一目了然。
- res:存放资源的文件;
- assets:包含应用程序的原生资源文件;
- lib:主要存放so文件;
- classes.dex:dex文件,所有的Java代码最终都是转化成dex文件。
- resources.arsc:经过编译的资源,将res下的资源映射成ID。
res/values/
目录下的资源也会被编译进来。 - AndroidManifest.xml:全局配置文件
- META-INF:存放签名校验的相关文件,用于保证APK的完整性和安全性;
点击各个类别,下方还会有一个窗口。比如说点击classes.dex:
可以清楚的看到包下的类,定义的方法和大小等。另外,如果发现被混淆了,还可以点击Load Proguard mappings
加载mapping文件。
删除无用的代码和资源
优化的第一步,我没有急着去看各种apk目录下各种资源占用的大小。而是删选择掉没有用的代码,我们一般都会在gradle文件中这样配置:
buildTypes {
release {
minifyEnabled true
shrinkResources true
}
}
这样在打包release包的时候,会帮我们把没有用的代码给删掉。但我们还是得手动删一下,这样做的原因是,这些代码虽然没有用到了,但是它们可能会引用到一些图片资源,这些资源在打包apk过程中是不会帮我们删掉的。具体做法是:右键
->Analyze
->Run Inspection by Name
->输入Unused declaration
,这时候会弹出一个框,根据自己的需求选择即可。它最厉害的地方就是能通过entry points
的调用链来判断一个类的可达性。举个例子,假设有两个类:
public class EmptyClassA {
public EmptyClassA() {
EmptyClassB b = new EmptyClassB(this);
}
}
public class EmptyClassB {
private EmptyClassA mA;
public EmptyClassB(EmptyClassA a) {
mA = a;
}
}
EmptyClassA在初始化时创建了EmptyClassB,如果我们单纯看EmptyClassB,就会发现这是有人使用的类,不能删掉。但其实EmptyClassA没有人会用到,也就不会用到EmptyClassB,所以EmptyClassB是可以删的。当代码越来越多的时候,这种调用关系可能会很复杂,好在Android Studio就能帮我们分析出来。
解决方法是根据提示,解除掉所有的依赖就能将类给删掉了。当然,如果某些类的依赖关系太过于复杂的话还是放弃吧,毕竟成本太高,成效太低。瘦身的过程中还要考虑一个性价比的问题。
不过我发现这个工具有个缺点就是分析不了Kotlin文件,希望以后Google能够加上吧。
优化的第二步是删掉无用的资源文件。同样也是利用Android Studio,不过这一步比较简单。具体方法是,在代码里右键
->Refactor
->Remove Unused Resources...
。如果你的项目已经经过了很多个迭代了,那这一步应该可以去掉很多没有用到的资源了。
压缩图片资源
当经过了前两步之后,再编一个apk。嗯,你就会发现,其实减的并不多。。这时候再看apk下的资源占比,最大头的一般还是res/
下的图片,但是这些图片都是项目中使用到的,不能删,所以只能选择压缩了。我用的是tinypng,这一步的效果是非常明显的,有一些几百K的图片直接被压缩成了几十K。项目里的大图片越多,效果就越明显。压缩后如果对一些大图片的大小还不是很满意,可以考虑一下WebP格式的图片,具体方法是选中图片->右键->Convert to WebP
。需要自己确认下转换后图片变小了没有,实际上有些图片的大小反而是会变大的。
这几步下来,尤其是删代码的时候耗费了很多时间。回头一看,发现才减了2.8M,10%还不到,这交不了差啊。。
动态加载so
回头再看看apk的资源占比,res/
和assets/
目录下能压缩的都压缩了。代码该删的也都删了。剩下的一座大山就是lib/
下的so库了。最后权衡后决定,将几个后台任务的so放到服务器上,然后下载后进行动态加载。因为这几个so本身就是执行后台任务时才需要用到的,所以对用户来说是感知不到的。
网上的很多文章都说先将so下载到sd卡,然后在复制到App的app_lib/
目录下面,然后再动态加载。最开始我也跟着这样做了,后来试了直接将so下载到app_lib/xxx
目录,然后动态加载也是可行的。这样就省去了一步复制操作。
public boolean loadSo() {
if (hasSoLoaded()) {
return true;
}
if (isSoExisted()) {
return loadLibrary();
} else if (downloadSo()) {
return loadLibrary();
}
return false;
}
大概流程就是这样的:先检查so有没有加载过,没有的话检查so是否已经存在了,是的话直接加载,没有的话下载后进行加载。
不得不说,最后这一步的效果是最明显的,大概减掉了4M。可以说是很满意了。