要想进行apk瘦身,首先我们得知道我们的apk是哪些部分体积比较大,我们可以借助android studio来分析下我们的app,将apk文件直接拖入android studio中,就会帮我们分析显示apk各模块的占用的体积
- lib:静态库so文件,包括自己和第三方库里面的静态库,当前我们使用的是armeabi-v7a和x86,我们的需求是要支持模拟器,所以存在x86架构的so
- res:资源文件,包括图片、布局、音频以及各种xml文件,是apk体积大的最主要原因
- assets:包括字体、图片(有时设计给的字体库的字体包含的文字很多,而我们可能只需要某个类型的数字,这个时候我们就可以提取我们需要的字体出来,可以缩减字体库的体积。比如设计给我们otf文件包括中文和数字,而我们只需要数字的,这个时候就可以使用我后面的提取字体网站进行字体提取)
- classes.dex:项目代码,包括第三方库的代码
从上面的图我们就可以看出,apk的体积变大的原因排序依次是lib-->res-->classes.dex-->assets。所以下面我们就从这几个方面对我们的app进行瘦身
常用的优化策略
1.清理无用资源
在android打包过程中,如果代码有涉及资源和代码的引用,那么就会打包到App中,为了防止将这些废弃的代码和资源打包到App中,我们需要及时地清理这些无用的代码和资源来减小App的体积。清理的方法是,依次点击android studio的【Analyze】--->【Run Inspection by Name】然后输入unused res,然后选择unused resources
这个时候搜索完之后,会在底部Inspection Results中找出未被使用的资源文件。为什么未被使用被强调了呢,是因为这些未被使用的资源只是未被R文件引用的资源,因为有少数资源是被resource.getIdentifier()引用,所以删除这些无用的资源时,一定要通过资源文件的名字全局搜索这个资源名字的字符串是否被引用,确定为被使用才去删除。
2.使用Lint工具
Lint工具还是很有用的,它给我们需要优化的点:
- 检测没有用的布局并且删除
- 把未使用到的资源删除
- 建议string.xml有一些没有用到的字符也删除掉
3.删除无用的语言文件
defaultConfig {
applicationId "com.lexin.gamebox"
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
resConfigs "zh"
}
resConfigs "zh":只保留中文。因为我们的app只支持中文
4.开启shrinkResources去除无用资源
在build.gradle 里面配置shrinkResources true,在打包的时候会自动清除掉无用的资源,但经过实验发现打出的包并不会,而是会把部分无用资源用更小的东西代替掉。注意,这里的“无用”是指调用图片的所有父级函数最终是废弃代码,而shrinkResources true 只能去除没有任何父函数调用的情况。
android {
buildTypes {
release {
shrinkResources true
}
}
}
5.开启minifyEnabled进行代码混淆
android {
buildTypes {
release {
minifyEnabled true
}
}
}
开启minifyEnabled为true后进行代码混淆,全量的类名、方法名、变量名都会被混淆成一个单独的字母,这样就缩小了引用路径,缩减了包体积。这里需要注意的一点就是,开启混淆后,发生崩溃的堆栈信息也是混淆的,通常混淆后的堆栈不容易看出崩溃信息,这是可以导入mapping.txt文件,这个文件就是混淆后的单字母路径与全量路径的映射文件。
资源压缩
在android开发中,内置的图片是很多的,这些图片占用了大量的体积,因此为了缩小包的体积,我们可以对资源进行压缩。常用的方法有:
- 1.使用压缩过的图片:使用压缩过的图片,可以有效降低App的体积。
- 2.只用一套图片:对于绝大对数APP来说,只需要取一套设计图就足够了。
- 3.使用不带alpha值的jpg图片:对于非透明的大图,jpg将会比png的大小有显著的优势,虽然不是绝对的,但是通常会减小到一半都不止。
- 4.使用tinypng有损压缩:支持上传PNG图片到官网上压缩,然后下载保存,在保持alpha通道的情况下对PNG的压缩可以达到1/3之内,而且用肉眼基本上分辨不出压缩的损失。
- 5.使用webp格式:webp支持透明度,压缩比比,占用的体积比JPG图片更小。从Android 4.0+开始原生支持,但是不支持包含透明度,直到Android 4.2.1+才支持显示含透明度的webp,使用的时候要特别注意。
- 6.使用svg:矢量图是由点与线组成,和位图不一样,它再放大也能保持清晰度,而且使用矢量图比位图设计方案能节约30~40%的空间。
-
7.对打包后的图片进行压缩:使用7zip压缩方式对图片路径进行压缩,可以直接使用微信开源的AndResGuard压缩方案。
对于第7点,有点类似于代码混淆,AndResGuard实际上做的是对资源文件路径的混淆
资源路径全都被混淆成了无意义的字母。如下就是我们项目自己的配置文件
apply plugin: 'AndResGuard'
andResGuard {
// mappingFile = file("./resource_mapping.txt")
mappingFile = null
use7zip = true
useSign = true
// 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
keepRoot = false
// 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小
fixedResName = "arg"
// 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
mergeDuplicatedRes = true
whiteList = [
//橙子游戏
"R.drawable.gt_one_login_bg",
"R.drawable.umcsdk_load_dot_white",
"R.layout.gt_activity_one_login",
"R.layout.gt_activity_one_login_web",
"R.layout.gt_one_login_nav",
"R.style.GtOneLoginTheme",
"R.id.gt_one_login_*",
"R.drawable.bg_onelogin_btn",
"R.drawable.icon_onelogin_back",
"R.drawable.icon_unchecked",
"R.drawable.icon_agreement_checked",
"R.drawable.icon_ads",
"R.drawable.rec_b3ffe2cc",
// 友盟
"R.anim.umeng*",
"R.string.umeng*",
"R.string.UM*",
"R.string.tb_*",
"R.layout.umeng*",
"R.layout.socialize_*",
"R.layout.*messager*",
"R.layout.tb_*",
"R.color.umeng*",
"R.color.tb_*",
"R.style.*UM*",
"R.style.umeng*",
"R.drawable.umeng*",
"R.drawable.tb_*",
"R.drawable.sina*",
"R.drawable.qq_*",
"R.drawable.tb_*",
"R.id.umeng*",
"R.id.*messager*",
"R.id.progress_bar_parent",
"R.id.socialize_*",
"R.id.webView",
//极光推送
"R.drawable.jpush_notification_icon",
//华为push
"R.string.hms_*",
"R.string.connect_server_fail_prompt_toast",
"R.string.getting_message_fail_prompt_toast",
"R.string.no_available_network_prompt_toast",
"R.string.third_app_*",
"R.string.upsdk_*",
"R.style.upsdkDlDialog",
"R.style.AppTheme",
"R.style.AppBaseTheme",
"R.dimen.upsdk_dialog_*",
"R.color.upsdk_*",
"R.layout.upsdk_*",
"R.drawable.upsdk_*",
"R.drawable.hms_*",
"R.layout.hms_*",
"R.id.hms_*",
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
"*.webp"
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.21'
//path = "/usr/local/bin/7za"
}
/**
* 可选: 如果不设置则会默认覆盖assemble输出的apk
**/
// finalApkBackupPath = "${project.rootDir}/final.apk"
/**
* 可选: 指定v1签名时生成jar文件的摘要算法
* 默认值为“SHA-1”
**/
// digestalg = "SHA-256"
}
资源动态加载
在客户端开发中,动态加载资源可以有效减小apk的体积。除此之外,只提供对主流架构的支持,比如arm,对于mips和x86架构可以考虑不支持,这样可以大大减小APK的体积。比如一些体积很大的资源可以通过不打包在本地apk,动态的通过服务端下发的形式使用时才去下载可以有效减少apk包的体积。比如图片、字体文件以及架构so文件。下面我们就来看下加载架构so文件。因为我们的app是需要支持模拟器上运行的,所以不得不将x86的架构打包进apk文件中,原始的x86的架构so占用5M多,这个时候我们就看一通过服务端动态下发x86的so:
核心逻辑代码如下:
package com.lexin.gamebox.soloader;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Description:动态加载so文件的核心,注入so路径到nativeLibraryDirectories数组第一个位置,会优先从这个位置查找so
*
*/
public class LoadLibraryUtil {
private static final String TAG = LoadLibraryUtil.class.getSimpleName() + "-duqian";
private static File lastSoDir = null;
/**
* 清除so路径,实际上是设置一个无效的path,用户测试无so库的情况
*
* @param classLoader
*/
public static void clearSoPath(ClassLoader classLoader) {
try {
final String testDirNoSo = Environment.getExternalStorageDirectory().getAbsolutePath() + "/duqian/";
new File(testDirNoSo).mkdirs();
installNativeLibraryPath(classLoader, testDirNoSo);
} catch (Throwable throwable) {
Log.e(TAG, "dq clear path error" + throwable.toString());
throwable.printStackTrace();
}
}
public static synchronized boolean installNativeLibraryPath(ClassLoader classLoader, String folderPath) throws Throwable {
return installNativeLibraryPath(classLoader, new File(folderPath));
}
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 libDirs = (List) nativeLibraryDirectories.get(dexPathList);
//去重
if (libDirs == null) {
libDirs = new ArrayList<>(2);
}
final Iterator 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 systemLibDirs = (List) 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 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 libDirs = (List) nativeLibraryDirectories.get(dexPathList);
//去重
if (libDirs == null) {
libDirs = new ArrayList<>(2);
}
final Iterator 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 systemLibDirs = (List) 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;
}
}
推荐两个好用的工具
- 图片压缩网站:https://tinypng.com/
- 字体提取网站:https://www.fontke.com/tool/subfont/