要想進(jìn)行apk瘦身,首先我們得知道我們的apk是哪些部分體積比較大,我們可以借助android studio來分析下我們的app,將apk文件直接拖入android studio中,就會(huì)幫我們分析顯示apk各模塊的占用的體積

- lib:靜態(tài)庫so文件,包括自己和第三方庫里面的靜態(tài)庫,當(dāng)前我們使用的是armeabi-v7a和x86,我們的需求是要支持模擬器,所以存在x86架構(gòu)的so
- res:資源文件,包括圖片、布局、音頻以及各種xml文件,是apk體積大的最主要原因
- assets:包括字體、圖片(有時(shí)設(shè)計(jì)給的字體庫的字體包含的文字很多,而我們可能只需要某個(gè)類型的數(shù)字,這個(gè)時(shí)候我們就可以提取我們需要的字體出來,可以縮減字體庫的體積。比如設(shè)計(jì)給我們otf文件包括中文和數(shù)字,而我們只需要數(shù)字的,這個(gè)時(shí)候就可以使用我后面的提取字體網(wǎng)站進(jìn)行字體提取)
- classes.dex:項(xiàng)目代碼,包括第三方庫的代碼
從上面的圖我們就可以看出,apk的體積變大的原因排序依次是lib-->res-->classes.dex-->assets。所以下面我們就從這幾個(gè)方面對(duì)我們的app進(jìn)行瘦身
常用的優(yōu)化策略
1.清理無用資源
在android打包過程中,如果代碼有涉及資源和代碼的引用,那么就會(huì)打包到App中,為了防止將這些廢棄的代碼和資源打包到App中,我們需要及時(shí)地清理這些無用的代碼和資源來減小App的體積。清理的方法是,依次點(diǎn)擊android studio的【Analyze】--->【Run Inspection by Name】然后輸入unused res,然后選擇unused resources


這個(gè)時(shí)候搜索完之后,會(huì)在底部Inspection Results中找出未被使用的資源文件。為什么未被使用被強(qiáng)調(diào)了呢,是因?yàn)檫@些未被使用的資源只是未被R文件引用的資源,因?yàn)橛猩贁?shù)資源是被resource.getIdentifier()引用,所以刪除這些無用的資源時(shí),一定要通過資源文件的名字全局搜索這個(gè)資源名字的字符串是否被引用,確定為被使用才去刪除。
2.使用Lint工具
Lint工具還是很有用的,它給我們需要優(yōu)化的點(diǎn):
- 檢測(cè)沒有用的布局并且刪除
- 把未使用到的資源刪除
- 建議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":只保留中文。因?yàn)槲覀兊腶pp只支持中文
4.開啟shrinkResources去除無用資源
在build.gradle 里面配置shrinkResources true,在打包的時(shí)候會(huì)自動(dòng)清除掉無用的資源,但經(jīng)過實(shí)驗(yàn)發(fā)現(xiàn)打出的包并不會(huì),而是會(huì)把部分無用資源用更小的東西代替掉。注意,這里的“無用”是指調(diào)用圖片的所有父級(jí)函數(shù)最終是廢棄代碼,而shrinkResources true 只能去除沒有任何父函數(shù)調(diào)用的情況。
android {
buildTypes {
release {
shrinkResources true
}
}
}
5.開啟minifyEnabled進(jìn)行代碼混淆
android {
buildTypes {
release {
minifyEnabled true
}
}
}
開啟minifyEnabled為true后進(jìn)行代碼混淆,全量的類名、方法名、變量名都會(huì)被混淆成一個(gè)單獨(dú)的字母,這樣就縮小了引用路徑,縮減了包體積。這里需要注意的一點(diǎn)就是,開啟混淆后,發(fā)生崩潰的堆棧信息也是混淆的,通?;煜蟮亩褩2蝗菀卓闯霰罎⑿畔ⅲ@是可以導(dǎo)入mapping.txt文件,這個(gè)文件就是混淆后的單字母路徑與全量路徑的映射文件。
資源壓縮
在android開發(fā)中,內(nèi)置的圖片是很多的,這些圖片占用了大量的體積,因此為了縮小包的體積,我們可以對(duì)資源進(jìn)行壓縮。常用的方法有:
- 1.使用壓縮過的圖片:使用壓縮過的圖片,可以有效降低App的體積。
- 2.只用一套圖片:對(duì)于絕大對(duì)數(shù)APP來說,只需要取一套設(shè)計(jì)圖就足夠了。
- 3.使用不帶alpha值的jpg圖片:對(duì)于非透明的大圖,jpg將會(huì)比png的大小有顯著的優(yōu)勢(shì),雖然不是絕對(duì)的,但是通常會(huì)減小到一半都不止。
- 4.使用tinypng有損壓縮:支持上傳PNG圖片到官網(wǎng)上壓縮,然后下載保存,在保持alpha通道的情況下對(duì)PNG的壓縮可以達(dá)到1/3之內(nèi),而且用肉眼基本上分辨不出壓縮的損失。
- 5.使用webp格式:webp支持透明度,壓縮比比,占用的體積比JPG圖片更小。從Android 4.0+開始原生支持,但是不支持包含透明度,直到Android 4.2.1+才支持顯示含透明度的webp,使用的時(shí)候要特別注意。
- 6.使用svg:矢量圖是由點(diǎn)與線組成,和位圖不一樣,它再放大也能保持清晰度,而且使用矢量圖比位圖設(shè)計(jì)方案能節(jié)約30~40%的空間。
-
7.對(duì)打包后的圖片進(jìn)行壓縮:使用7zip壓縮方式對(duì)圖片路徑進(jìn)行壓縮,可以直接使用微信開源的AndResGuard壓縮方案。
對(duì)于第7點(diǎn),有點(diǎn)類似于代碼混淆,AndResGuard實(shí)際上做的是對(duì)資源文件路徑的混淆
image.png
資源路徑全都被混淆成了無意義的字母。如下就是我們項(xiàng)目自己的配置文件
apply plugin: 'AndResGuard'
andResGuard {
// mappingFile = file("./resource_mapping.txt")
mappingFile = null
use7zip = true
useSign = true
// 打開這個(gè)開關(guān),會(huì)keep住所有資源的原始路徑,只混淆資源的名字
keepRoot = false
// 設(shè)置這個(gè)值,會(huì)把a(bǔ)rsc name列混淆成相同的名字,減少string常量池的大小
fixedResName = "arg"
// 打開這個(gè)開關(guān)會(huì)合并所有哈希值相同的資源,但請(qǐng)不要過度依賴這個(gè)功能去除去冗余資源
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"
}
/**
* 可選: 如果不設(shè)置則會(huì)默認(rèn)覆蓋assemble輸出的apk
**/
// finalApkBackupPath = "${project.rootDir}/final.apk"
/**
* 可選: 指定v1簽名時(shí)生成jar文件的摘要算法
* 默認(rèn)值為“SHA-1”
**/
// digestalg = "SHA-256"
}
資源動(dòng)態(tài)加載
在客戶端開發(fā)中,動(dòng)態(tài)加載資源可以有效減小apk的體積。除此之外,只提供對(duì)主流架構(gòu)的支持,比如arm,對(duì)于mips和x86架構(gòu)可以考慮不支持,這樣可以大大減小APK的體積。比如一些體積很大的資源可以通過不打包在本地apk,動(dòng)態(tài)的通過服務(wù)端下發(fā)的形式使用時(shí)才去下載可以有效減少apk包的體積。比如圖片、字體文件以及架構(gòu)so文件。下面我們就來看下加載架構(gòu)so文件。因?yàn)槲覀兊腶pp是需要支持模擬器上運(yùn)行的,所以不得不將x86的架構(gòu)打包進(jìn)apk文件中,原始的x86的架構(gòu)so占用5M多,這個(gè)時(shí)候我們就看一通過服務(wù)端動(dòng)態(tài)下發(fā)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:動(dòng)態(tài)加載so文件的核心,注入so路徑到nativeLibraryDirectories數(shù)組第一個(gè)位置,會(huì)優(yōu)先從這個(gè)位置查找so
*
*/
public class LoadLibraryUtil {
private static final String TAG = LoadLibraryUtil.class.getSimpleName() + "-duqian";
private static File lastSoDir = null;
/**
* 清除so路徑,實(shí)際上是設(shè)置一個(gè)無效的path,用戶測(cè)試無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<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,也優(yōu)先加載指定路徑的外部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部分機(jī)型刪了該成員屬性,兼容
*
* @return 被廠家刪了返回1,否則正常讀取
*/
@TargetApi(Build.VERSION_CODES.M)
private static int getPreviousSdkInt() {
try {
return Build.VERSION.PREVIEW_SDK_INT;
} catch (Throwable ignore) {
}
return 1;
}
}
推薦兩個(gè)好用的工具
- 圖片壓縮網(wǎng)站:https://tinypng.com/
- 字體提取網(wǎng)站:https://www.fontke.com/tool/subfont/
- 瘦身實(shí)踐參考:https://juejin.cn/post/6844904103131234311?searchId=20231229154352C413439530FA9BE3FAC2
