神操作 之 「玲瓏寶塔」優化 Apk 包大小 [復制鏈接]

2019-11-11 16:32
靜心Study 閱讀:245 評論:0 贊:0
Tag:  優化Apk包大小

九分喜歡,一分尊嚴,放棄你,也放過自己,愿你安好,在多年以后不要記起深愛你的我。

圖片描述

絮絮叨

工作不長不短,之前未曾考慮過深處,只是停留寫出來了,便是完美。

而今的處境,不尷不尬,歲月剛好,背起行囊,繼續前行。

如今的 5 G 也在萬眾矚目矚目下翩翩起舞,而 Android 近些年也惹得不少爭議,所謂的謠言,不過爾爾。

每個人的追求不一樣,盡自己最大努力吧。

如何減少 Apk 大小,一直以來都是處于觀望狀態,懶得折騰,其實還是不會,Low 的一批。

Today,一起來搞一波~

歡迎各位指正~

現學現賣~

一腦圖,覽無余

圖片描述

玲瓏寶塔鎮萬物

首先附上一張現在 Apk 大小圖:
在這里插入圖片描述
未做任何處理原包大小為 10 MB,加固之后將近 11 MB。

以此為例,一起看看經過我們玲瓏寶塔升級完,最終還剩下多少精華?

圖片描述

一層鎮妖魔(減少 4.1 MB)

來到第一層,我們先來簡單分析下是什么造成 Apk 包如此“龐大”?在這里插入圖片描述
上圖可看到 lib 下兼容了全面的 CPU 架構,試想一下,假設未來的未來多了短視頻、直播、地圖導航等等(不接受杠精),這塊的大小會不會成倍數的增長。
在這里插入圖片描述
上圖可看到默認支持了 89 種語言類型,目前的應用暫時未國際化,這塊也可直接設置兼容中文即可,原諒我這個強迫癥。

占比排行榜依次為:源代碼、資源文件、lib。

我們先挑個軟柿子玩玩。

1.1 設置支持語言(減少 0.2 MB)

關于這塊,個人覺得雖然占比較小,但是用啥玩啥,用不到的直接干掉。

在 build.gradle 中設置僅支持中文:

    defaultConfig {
        ...
        // 僅支持 中文
        resConfigs "zh"
    }

這塊主要是根據現有項目需求來定,中心思想只有一個,兼容哪兒個就設置哪兒個國家語言,其他的直接忽略。

設置完之后打個包,看下有沒有什么變化。
在這里插入圖片描述
從上圖中可以很清晰的看到,經過設置僅支持的國家語言后,包大小減少了 0.2 MB。隨后我們看下資源映射文件中關于 string 中會有什么變化。
在這里插入圖片描述
默認語言中設置為中文,且應用也只支持了中文,少了好多東西,爽得很~

1.2 設置支持的 CPU 架構類型(減少 1.5 MB)

話說這里的 lib 為何兼容了這么多的 CPU 架構類型???

正好走到這里,關于這塊的小知識再次重溫下,瞅瞅 Google 為我們提供的解釋:

不同的 Android 手機使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 與指令集的每種組合都有專屬的應用二進制接口,即 ABI。ABI 可以非常精確地定義應用的機器代碼在運行時如何與系統交互。您必須為應用要使用的每個 CPU 架構指定 ABI。

貌似 Google 商店現在支持對應的架構模式分發對應的 Apk 包,這點爽的每個包只需要兼容一種就好了。But,ummm。

目前而言,項目中使用到真正用到 So 庫沒幾個,全部兼容太過于浪費,據說 arm 屬于通用,那么這里同語言設置一樣,僅支持 arm 即可。

    defaultConfig {
        ...
        ndk {
            // 設置支持的SO庫架構
            abiFilters "armeabi"
        }
    }

打包運行后,繼續查看現在包大小:
在這里插入圖片描述
這塊一直屬于個心病,之前的項目光是 So 庫就占了很大一部分空間,很濕蛋疼。

1.3 開啟壓縮、混淆(減少 2.4 MB)

根據 Google 官網解釋,當我們使用 Android Gradle 3.4.0 或者更高版本時,默認會啟用 R8 編譯器進行壓縮、混淆以及優化,主要項以及作用如下:

  • 代碼優化: 通過檢測并安全移除未使用的類、字段、方法和屬性;

  • 資源壓縮: 從應用中移除未使用的資源,此過程包含移除庫依賴項中未使用的資源文件。此項常常和代碼壓縮配合使用;

  • 混淆: 縮短類和成員的名稱,從而減小 Dex 文件大小;

  • 優化: 檢查并重寫代碼,進一步減小 Dex 文件大小。例如,如果 R8 檢測到從未采用過給定 if/else 語句的 else {} 分支,R8 便會移除 else {} 分支的代碼。

這里需要注意一下:

  • 默認情況下并不啟用壓縮、混淆和代碼優化功能。 因為開啟后會造成 Debug 模式下編譯時間較久。

關于混淆文件,這里需要正好學習一下。

混淆的意義在于什么?(引入官方解釋)

  • 混淆處理的目的是通過縮短應用的類、方法和字段的名稱來減小應用的大小

混淆效果(摘自官方):

androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
    androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
        android.content.Context mContext -> a
        int mListItemLayout -> O
        int mViewSpacingRight -> l
        android.widget.Button mButtonNeutral -> w
        int mMultiChoiceItemLayout -> M
        boolean mShowTitle -> P
        int mViewSpacingLeft -> j
        int mButtonPanelSideLayout -> K

混淆需注意:

  • Android 四大組件不能混淆;

  • 反射、注解、枚舉不能混淆;

  • JS、Native 調用的方法不能混淆;

  • 基礎 Bean 類以及序列化實體類不能混淆;

  • 自定義控件不能混淆;

  • 資源文件不能混淆(當然也有騷操作);

隨后列舉常用混淆規則(語法):

  • 保留某個類

    -keep public class com.hlq.Love
  • 保留某包下的所有類及其內部類

    -keep class com.hlq.** {*;}
  • 不顯示指定類警告
    dontwarn com.hlq.**

具體規則可文末查看官方手冊。

接下來跟著官網一起實踐一波~

    buildTypes {
        release {
            // 打開資源壓縮
            shrinkResources true
            // 開啟混淆操作
            minifyEnabled true 
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.config
        }
        debug {
            // 關閉資源壓縮以及混淆操作
            shrinkResources false
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.config
        }
    }

這里需要注意,在 Debug 模式下需要關閉資源壓縮以及混淆操作,否則會增加編譯時間,一般在發布正式包時打開即可。

這里附上現在項目使用的混淆文件,基于大芬兒提供混淆文件做了部分修改:

#############################################
#
# 混淆基本指令
#
#############################################
# 代碼混淆壓縮比,在0~7之間,默認為5,一般不做修改
-optimizationpasses 5

# 混合時不使用大小寫混合,混合后的類名為小寫
-dontusemixedcaseclassnames

# 指定不去忽略非公共庫的類
-dontskipnonpubliclibraryclasses

# 這句話能夠使我們的項目混淆后產生映射文件
# 包含有類名->混淆后類名的映射關系
-verbose

# 指定不去忽略非公共庫的類成員
-dontskipnonpubliclibraryclassmembers

# 不做預校驗,preverify是proguard的四個步驟之一,Android不需要preverify,去掉這一步能夠加快混淆速度。
-dontpreverify

# 保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses

# 避免混淆泛型
-keepattributes Signature

# 拋出異常時保留代碼行號
-keepattributes SourceFile,LineNumberTable

# 指定混淆是采用的算法,后面的參數是一個過濾器
# 這個過濾器是谷歌推薦的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*

# 忽略警告
-ignorewarnings

#############################################
#
# 需要保留的公共部分
#
#############################################

# 保留我們使用的四大組件,自定義的 Application 等等這些類不被混淆
# 因為這些子類都有可能被外部調用
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService

# 保留support下的所有類及其內部類
-keep class android.support.** {*;}

# 保留繼承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**

# 保留R下面的資源
-keep class **.R$* {*;}

# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留在Activity中的方法參數是view的方法,保障layout中寫的onClick不會被影響
-keepclassmembers class * extends android.app.Activity{
    public void *(android.view.View);
}

# 保留枚舉類不被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保留我們自定義控件(繼承自View)不被混淆
-keep public class * extends android.view.View{
    *** get*();
    void set*(***);
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

# 保留Parcelable序列化類不被混淆
-keep class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
}

# 保留Serializable序列化的類不被混淆
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    !static !transient <fields>;
    !private <fields>;
    !private <methods>;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

# 對于帶有回調函數的onXXEvent、**On*Listener的,不能被混淆
-keepclassmembers class * {
    void *(**On*Event);
    void *(**On*Listener);
}

# webView處理,項目中沒有使用到webView忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
    public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.webView, jav.lang.String);
}

# 移除Log類打印各個等級日志的代碼,打正式包的時候可以做為禁log使用,這里可以作為禁止log打印的功能使用
# 記得proguard-android.txt中一定不要加-dontoptimize才起作用
# 另外的一種實現方案是通過BuildConfig.DEBUG的變量來控制
-assumenosideeffects class android.util.Log {
    public static int v(...);
    public static int i(...);
    public static int w(...);
    public static int d(...);
    public static int e(...);
}

#############################################
#
# 處理項目中我們的部分
#
#############################################

#-----------處理實體類---------------
# 在開發的時候我們可以將所有的實體類放在一個包內,這樣我們寫一次混淆就行了
-keep public class 實體類.**{*;}

# Js 交互
-keepclassmembers class JS 交互類地址{
  public *;
}

-keepattributes *JavascriptInterface*

#############################################
#
# 處理第三方依賴庫部分
#
#############################################

# 此處按照實際項目中使用去官方查找對應的混淆代碼塊

隨后我們繼續打包,查看混淆、資源壓縮后 Apk 大小以及部分變化:
在這里插入圖片描述
dex 從 3 個降低到 2 個。未 Keep 的文件均已混淆,而 Keep 的文件依舊傲嬌挺立,如下圖:
在這里插入圖片描述
混淆操作,在一定程度增大了破解的難度。當然,也沒有絕對的安全。

R8 每次運行時都會創建一個 mapping.txt 文件,其中列出了混淆過的類、方法和字段名稱與原始名稱的映射關系。此映射文件還包含用于將行號映射回原始源文件行號的信息。R8 將此文件保存在 <module- name>/build/outputs/mapping/<build-type>/ 目錄中。
在這里插入圖片描述
線上版本肯定要進行混淆,那么針對線上版本報出的異常,我們又該如何處理呢?畢竟關鍵內容都變成無意義字符,鑒名其意不存在了。
在這里插入圖片描述
iTerm 2 打開:
在這里插入圖片描述
點擊 ReTrace:
在這里插入圖片描述
這塊步驟如下:

  • 導入 Mapping 文件

  • 將混淆后錯誤日志拷貝黏貼到 Obfuscated stack trace 中

  • 點擊右下角的 ReTrace!

1.4 開啟 Zipalign 優化

這塊我看的很濕懵逼,估計唯有雞大行云流水了。簡單摘自官方解釋:

zipalign 是一種歸檔對齊工具,可對 Android 應用文件進行重要的優化。其目的是要確保所有未壓縮數據的開頭均相對于文件開頭部分執行特定的對齊。具體來說,它會使 APK 中的所有未壓縮數據(例如圖片或原始文件)在 4 字節邊界上對齊。這樣一來,即可使用 mmap() 直接訪問所有部分,即使其中包含具有對齊限制的二進制數據也沒關系。這樣做的好處是可以減少運行應用時消耗的 RAM 容量。

如何使用?很是 easy~

    buildTypes {
        release {
            // 開啟Zipalign 優化
            zipAlignEnabled true
        }
        debug {
            zipAlignEnabled false
        }
    }

看一下結果:
在這里插入圖片描述
貌似沒啥用哦,能加還是加上吧。

二層鎮仙神(減小 1.5 MB)

來到第二層,我們再來開下資源映射文件中關于圖片這塊:
在這里插入圖片描述
其實對于圖片而言,真的是個很蛋疼的操作,不過還好,一些簡單的小背景、小效果,現在大部分都直接采用 shape、selector 等實現,多少也避免引入了一些圖片。

對于圖片優化,主要分為以下幾點:

  • 套圖的優化 -SVG

  • 套圖的優化 - Thit 著色器的應用

  • webp 的使用

2.1 套圖的優化 - SVG

什么是套圖呢?

好比應用中的某個 Icon,一般來講,UI 都會為我們提供 n 套圖,以便于我們適配不同分辨率記性,大概的目錄如下:
在這里插入圖片描述
例如下面的這些大大小小的 Icon,一個個拷貝、改名也是比較痛苦的:
在這里插入圖片描述
這個時候,SVG 便派上了用場。

可縮放矢量圖形(英語:Scalable Vector Graphics,SVG)是一種基于可擴展標記語言(XML),用于描述二維矢量圖形的圖形格式。 SVG由W3C制定,是一個開放標準。

SVG 優勢:

  • 節約空間、內存

SVG 劣勢:

  • 不支持透明度以及漸變

ummm,需要注明一點,Android 6.0 + 支持,6.0 以下需要做兼容處理。不過現在應該也沒必要兼容那么低的版本了吧?

如何在 Android Studio 中創建一張 SVG 圖片呢?如下所示:
在這里插入圖片描述
彈出如下界面,在此頁面可以選擇直接導入 Android 內置 Icon 庫圖,還是手動加載 SVG or PSD 格式,看需選擇。
在這里插入圖片描述
放個操作圖省事兒點:
在這里插入圖片描述
使用也很 easy:

    <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/iv_toolbar_back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="fitXY"
            app:srcCompat="@drawable/ic_arrow_back" />

別忘記在 build 里面對此設置:

    defaultConfig {
        ...
        // 強制 Gradle 在編譯時不自動生成兼容低版本的位圖資源
        vectorDrawables.useSupportLibrary = true
        // 生成指定類型的圖片資源
        vectorDrawables.generatedDensities = ['xhdpi', 'xxhdpi', 'xxxhdpi'] 
    }

建議直接下載 svg,導入 svg,這個真心比較爽。

推薦下良心企業,阿里的 iconfont:

經過一通折騰,阿里下載 SVG,之后 Android Studio 導入 SVG,巴拉巴拉,好歹改完了,哈哈哈

2.2 套圖的優化 - Tint 著色器

先來舉個天馬行空的例子,例如同一張圖片在不同的狀態下顯示不同,比如成功綠色,失敗紅色等等。按照以往的習慣,那肯定要求至少提供每種狀態對應的圖片,不然我怎么搞?

但是有個實實在在的問題,那就是,圖片一樣,只是顏色發生了變化,假設五種狀態,我們就需要引入至少五張圖片,那么,可否只需要一張圖片,針對不同的狀態,我們渲染不同的顏色呢?

當然可以,這就是今天要說到的 Tint 著色器。有了它,最簡潔明了一點,至少能幫我剩下很多可謂是“無用”圖片,大大節省了很多空間,我們的 Apk 更加“干練”。

再舉一個我們項目中常見的例子,首頁 Tab 欄,如下圖:
在這里插入圖片描述
Tab 切換,字體變色、圖片變色,這個見怪不怪了吧。上來至少給我提供八張圖,四張默認,四張選中,然后通過 selector 文件設置,不給圖沒法做。對吧,這就是之前最實際的想法,嗯,還感覺自己可 dei 了。

一起先來回顧下以前的 low 寫法:

Step 1:至少提供八張圖后,設置 icon 引用 selector 文件:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/nav_home"
        android:icon="@drawable/selector_menu_home"
        android:title="@string/nav_home" />
    ...
</menu>

Step 2:定義 Selector:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true" android:drawable="@drawable/ic_tab_home_sel"/>
    <item android:drawable="@drawable/ic_tab_home"/>
</selector>

這樣寫固然木有問題,但是平白無故多了很多圖片,在今天的我來看,必須不能忍,今天只用四張圖,干他~

這里吐槽下,由于之前底部導航采用 BottomNavigationView 方式,折騰好半天踩折騰出來,中間無數次想放棄了。但回頭一想,我好歹也是跟隨我雞大的,況且抽煙的時候還要和文哥交流呢。艾瑪,不容易,容小弟我抽根煙,ummm,沒煙了


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

龙江福彩p62开奖 欢乐捕鱼人直播 卖拌面赚钱么 天天捕鱼电玩版赢手机 现金流赚钱 捕鱼王游戏下载 pinterest 赚钱 真人麻将直播平台 货币 靠手续费赚钱 单机麻将瀛抱枕单机版 欢聚龙江麻将 bt网站赚钱吗 怎么坐在家里赚钱 视频编辑自媒体赚钱 制作游戏能不能赚钱 海龙王捕鱼手游 天天快递赚钱吗