Android瘦身最佳實(shí)踐

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


image.png
  • 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

image.png

image.png

這個(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è)好用的工具

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容