“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情”
概述
包大小优化是一个老生常谈的技术,已经有很多非常好的文章了,这里主要记录一下笔记
APK组成
apk一般由以下几个部分组成
| APK组成 | 描述 |
|---|---|
| res | 主要图片资源和xml文件 |
| assets | 主要放置不需要编译的文件,比如MP4文件 |
| lib | 主要放置各种so文件 |
| classes.dex | 编译后的字节码文件,因为方法数量限制,可能有多个dex文件 |
| AndroidManifest.xml | 配置文件 |
| META-INF | 存放签名和证书的地方 |
| resources.arsc | 编译后的二进制文件主要记录了资源和Id的对应关系 |
其实要优化包大小,就可以从上面几个方面优化
- res大小优化
- assets大小优化
- lib大小优化
- dex大小优化
- resources.arsc大小优化
这几个文件占了包大小的99%,下面依次说一下,从这几个方面怎么优化
res大小优化
这里我写了一个gradle插件来做这件事 和# Android Gradle插件工具实战:检测三方库权限so适配及压缩图片
这个里面主要是图片和xml,所以有俩个思路
压缩图片
第一个就是压缩图片,这里使用的tinify进行压缩,针对这个压缩,我专门写了一个gradle插件来做
执行命令 ./gradlew checkres
...
[mmm] /app/src/main/res/drawable-xxhdpi/pay_success_bg.png size = 262KB
[mmm] 压缩前 size = 262
[mmm] 压缩后 size = 56
[mmm] copy 完成
[mmm] /app/src/main/res/drawable-xxhdpi/bg_qr_share.png size = 582KB
[mmm] 压缩前 size = 582
[mmm] 压缩后 size = 155
[mmm] copy 完成
[mmm] /app/src/main/res/drawable-xxhdpi/bg_every_day_pick.png size = 119KB
[mmm] 压缩前 size = 119
[mmm] 压缩后 size = 32
[mmm] copy 完成
[mmm] /app/src/main/res/drawable-xxhdpi/bg_secretary_dialog.png size = 216KB
[mmm] 压缩前 size = 216
[mmm] 压缩后 size = 43
[mmm] copy 完成
[mmm] 此次共压缩 {7706}
可以看到压缩了将近8MB的大小
还可以继续把png图片转为WebP图片,进行继续压缩
android studio 自带功能
点击图片右键->convert to webp
不能压缩的超大图片
这个种不能压缩的超大图片可以放到cdn上,用url进行加载
删除内容重复的xml
我们在正常开发的时候,有可能要写一个shape.xml,但是有可能这个shape已经被实现了,只不过我不知道,我友写了一遍,这样就会有命名不同,但内容相同的文件,这就是重复文件
针对这个问题,我也写了一个gradle插件,可以检测重复文件
...
[mmm] 重复文件
[mmm] /app/src/main/res/drawable/bg_10dp_chat_white.xml
[mmm] /app/src/main/res/drawable/bg_luck_telephone_part2.xml
[mmm] 重复文件
[mmm] /app/src/main/res/drawable-xhdpi/random_match_avatar_12.png
[mmm] /app/src/main/res/drawable-xhdpi/random_match_avatar_15.png
...
[mmm] 此次共找到重复文件 {15}
删除无用的资源和xml
android studio 的自带查询无用资源和代码
Analyze > Run Inspection by Name > Unused resources
Analyze > Inspect code
gradle 添加配置shrinkResources
buildTypes {
release {
minifyEnabled true
shrinkResources true
}
首先 minifyEnabled打开混淆,混淆的作用
- 可以删除无用的类,变量,方法
- 优化字节码并删除无用指令
- 通过将类名方法名变为无意义的字符来实现混淆结果
- 最后校验处理后的代码
可以看到minifyEnabled可以删除无用代码变量
配合shrinkResources可以删除无用资源,需要配合minifyEnabled使用
移除备用资源
比如国际化做了很多语言适配,第三方库含有的一些语言,我们可以统一设置语言
defaultConfig {
resConfigs("en","zh","zh-rCN")
}
资源图片也是可以指定用那些文件夹
defaultConfig {
resConfigs("xxhdpi","xxxhdpi")
}
lib压缩
为了更方便的查找那个依赖有so,以及so有没有适配32/64位,专门开发了一个插件,可以方便的找出so,大小,已经是否适配32/64
[mmm] ---------------[依赖产物开始] group=com.immomo.momomedia:x264encoder----------------
[mmm] so文件 = jni/arm64-v8a/libx264encoder.so size = 424KB
[mmm] so文件 = jni/armeabi/libx264encoder.so size = 477KB
[mmm] ---------------[依赖产物结束] group=com.immomo.momomedia:x264encoder----------------
[mmm] ---------------[依赖产物开始] group=com.immomo.momomedia:voaac----------------
[mmm] so文件 = jni/arm64-v8a/libVoAACEncoder.so size = 74KB
[mmm] so文件 = jni/armeabi/libVoAACEncoder.so size = 78KB
[mmm] so文件 = jni/armeabi-v7a/libVoAACEncoder.so size = 78KB
[mmm] ---------------[依赖产物结束] group=com.immomo.momomedia:voaac----------------
...
[mmm] v8a不包含 = libbsdiff.so group=com.immomo.android.mklibrary:mk size = 39KB
[mmm] v8a不包含 = libmkjni.so group=com.immomo.android.mklibrary:mk size = 7KB
[mmm] v8a不包含 = libsevenz.so group=com.immomo.android.mklibrary:mk size = 33KB
[mmm] v8a不包含 = libMOMOPitchShift.so group=com.immomo.momomedia:mmaudio size = 76KB
[mmm] v8a不包含 = libMOMOPitchShift.so group=com.immomo.momomedia:mmaudio size = 76KB
[mmm] v8a不包含 = libmjni.so group=MatchMakerAndroid:momsecurity size = 62KB
动态加载so
针对so过多的问题,我们可以选择动态加载so,哪一些初始化不需要的so,可以进行动态加载
动态加载so方案
- 下载到sdcard
- copy文件到app的缓存
- 反射修改so文件映射列表(关键:把自定义的native库path插入native数组最前面,即使安装包libs目录里面有同名的so,也优先加载指定路径的外部so。),这个其实和热修复方案原理一样
- 然后就可以正常使用了
/**
* com.google.android.apps.photolab.storyboard.soloader.LoadLibraryUtil
* Description:动态加载so文件的核心,注入so路径到nativeLibraryDirectories数组第一个位置,会优先从这个位置查找so
* 更多姿势,请参考开源库动态更新so的黑科技,仅供学习交流
*
*/
public class LoadLibraryUtil {
private static final String TAG = LoadLibraryUtil.class.getSimpleName() + "-duqian";
private static File lastSoDir = null;
public static synchronized boolean installNativeLibraryPath(ClassLoader classLoader, File folder)
throws Throwable {
if (classLoader == null || folder == null || !folder.exists()) {
Log.e(TAG, "classLoader or folder is illegal " + folder);
return false;
}
final int sdkInt = Build.VERSION.SDK_INT;
final boolean aboveM = (sdkInt == 25 && getPreviousSdkInt() != 0) || sdkInt > 25;
if (aboveM) {
try {
V25.install(classLoader, folder);
} catch (Throwable throwable) {
try {
V23.install(classLoader, folder);
} catch (Throwable throwable1) {
V14.install(classLoader, folder);
}
}
} else if (sdkInt >= 23) {
try {
V23.install(classLoader, folder);
} catch (Throwable throwable) {
V14.install(classLoader, folder);
}
} else if (sdkInt >= 14) {
V14.install(classLoader, folder);
}
lastSoDir = folder;
return true;
}
private static final class V23 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
Field pathListField = ReflectUtil.findField(classLoader, "pathList");
Object dexPathList = pathListField.get(classLoader);
Field nativeLibraryDirectories = ReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
//去重
if (libDirs == null) {
libDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = libDirs.iterator();
while (libDirIt.hasNext()) {
final File libDir = libDirIt.next();
if (folder.equals(libDir) || folder.equals(lastSoDir)) {
libDirIt.remove();
Log.d(TAG, "dq libDirIt.remove() " + folder.getAbsolutePath());
break;
}
}
libDirs.add(0, folder);
Field systemNativeLibraryDirectories =
ReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
//判空
if (systemLibDirs == null) {
systemLibDirs = new ArrayList<>(2);
}
Log.d(TAG, "dq systemLibDirs,size=" + systemLibDirs.size());
Method makePathElements = ReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class, List.class);
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
libDirs.addAll(systemLibDirs);
Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs, null, suppressedExceptions);
Field nativeLibraryPathElements = ReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.setAccessible(true);
nativeLibraryPathElements.set(dexPathList, elements);
}
}
/**
* 把自定义的native库path插入nativeLibraryDirectories最前面,即使安装包libs目录里面有同名的so,也优先加载指定路径的外部so
*/
private static final class V25 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
Field pathListField = ReflectUtil.findField(classLoader, "pathList");
Object dexPathList = pathListField.get(classLoader);
Field nativeLibraryDirectories = ReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
//去重
if (libDirs == null) {
libDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = libDirs.iterator();
while (libDirIt.hasNext()) {
final File libDir = libDirIt.next();
if (folder.equals(libDir) || folder.equals(lastSoDir)) {
libDirIt.remove();
Log.d(TAG, "dq libDirIt.remove()" + folder.getAbsolutePath());
break;
}
}
libDirs.add(0, folder);
//system/lib
Field systemNativeLibraryDirectories = ReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
//判空
if (systemLibDirs == null) {
systemLibDirs = new ArrayList<>(2);
}
Log.d(TAG, "dq systemLibDirs,size=" + systemLibDirs.size());
Method makePathElements = ReflectUtil.findMethod(dexPathList, "makePathElements", List.class);
libDirs.addAll(systemLibDirs);
Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs);
Field nativeLibraryPathElements = ReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.setAccessible(true);
nativeLibraryPathElements.set(dexPathList, elements);
}
}
private static final class V14 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
Field pathListField = ReflectUtil.findField(classLoader, "pathList");
Object dexPathList = pathListField.get(classLoader);
ReflectUtil.expandFieldArray(dexPathList, "nativeLibraryDirectories", new File[]{folder});
}
}
/**
* fuck部分机型删了该成员属性,兼容
*
* @return 被厂家删了返回1,否则正常读取
*/
@TargetApi(Build.VERSION_CODES.M)
private static int getPreviousSdkInt() {
try {
return Build.VERSION.PREVIEW_SDK_INT;
} catch (Throwable ignore) {
}
return 1;
}
}
assets 优化
assets 一般储存一些MP4文件,svga动画文件等,这些都可以放到CND上,然后本地加载url就可以了
dex优化
移除无用三方库
检测三方库,使用有用,无用的三方库删除
对字节码进行优化
使用facebook的开源ReDex,对dex的字节码进行优化
抖音 Android 包体积优化探索:基于 ReDex 的 DEX 优化落地实践
抖音 Android 包体积优化探索:从 Class 字节码入手精简 DEX 体积
resources.arsc 优化
对resources.arsc进行混淆,可以把res/drawable/wechat变为r/d/a,开源工具AndResGuard
AndResGuard 插件的工作原理,就是创建了一个资源混淆打包任务,该任务会先调用默认的打包任务,在默认打包工作结束后,会解压打好的 apk 包,识别解析包里的 resources.arsc 资源表,然后再混淆 res 文件夹下面的所有资源文件,同时相对应的修改资源表,最后将修改后的资源重新打包签名,生成新的 apk 包。
资源混淆的核心,就是对 resources.arsc 资源表的解析修改,了解了 resources.arsc 文件的格式,我们再来使用微信的 AndResGuard 插件就很容易理解了。
R文件
- R文件常量内联,R文件瘦身
- 无用resource资源检查
- 无用assets检查
这个可以使用字节开源 shrink-r-plugin
抖音 Android 包体积优化探索:资源二进制格式的极致精简
其他优化
插件化
利用插件化进行包大小的压缩