摘要: 如何瘦身是 APK 的重要优化技能。APK 在装置和更新时都需求经过网络下载到设备,APK 越小,用户体会越好。本文作者经过对 APK 内涵机制的具体解析,给出了对 APK 各组成成分的优化办法及技能,并完成了一个根本 APK 的最小化进程。

正文:

高尔夫运动中,分数最小者胜出。

让咱们将这一准则运用到 Android App 开发中。咱们将玩转一个称为“ApkGolf”的 APK,意图是创立一个尽可能具有最少字节数的 App,并可装置在运转 Oreo 的设备上。

基线测定

一开始,咱们用 Android Studio 生成一个缺省的 App,创立密钥库(Keystore)

并对 App 签名,然后运用指令stat -f%z $filename测定生成 APK 文件的字节数巨细。

进一步,为确保该 APK 作业正常,咱们将在一台运转 Oreo 的 Nexus 5x 手机上装置它。

实现Android APK瘦身99.99%

看上去挺美丽。可是现在咱们的 APK 巨细近乎 1.5Mb。

APK Analyser

考虑到咱们 App 的功用十分简略,1.5Mb 的规划看上去过于臃肿了。因而,咱们要深化了解一下该项目,看看是否有一些能立竿见影地减少文件巨细的当地。Android Studio 生成了:

  • 扩展AppCompatActivity而得到的MainActivity
  • 运用根视图ConstraintLayout的布局文件;
  • Value 文件,其间包含三种色彩、一个字符串资源(Resource)和一个主题(Theme);
  • AppCompatConstraintLayout的支撑库;
  • 一个AndroidManifest.xml文件;
  • PNG 格式的启动图标,分别是正方形、圆形和前台的。

看上去首战之地的方针是启动图标文件,由于 APK 中共包含了 15 个图像文件,并且在mipmap-anydpi-v26下还有两个 XML 文件。下面,让咱们运用 Android Studio 的 APK Analyser

(developer.android.com/studio/buil…)

对该 APK 文件做一个定量剖析。

实现Android APK瘦身99.99%

给出的成果与咱们的开始假定截然不同,其间显现 Dex 文件是大头,而上述资源仅占 APK 巨细的 20%。

文件 巨细占比
classes.dex 74%
res 20%
resources.arsc 4%
META-INF 2%
AndroidManifest.xml <1%

下面让咱们逐一剖析每个文件的行为。

Dex 文件

看上去元凶巨恶是classes.dex文件,它占据了 73% 的空间,因而它成为咱们的首要减少方针。该文件为 Dex 格式

其间包含了咱们的全部编译后代码,以及对 Android 结构和支撑库中外部办法的引证。

然而android.support软件包中引证了超越 13000 种的办法,关于一个简略的“Hello World”App 而言,彻底没有必要。

资源

目录“res”中包含了大量的布局(Layout)文件、Drawable 和动画,它们并非在 Android Studio UI 中马上可见。相同,它们也是由支撑库推入其间的,约占 APK 规划的 20%。

实现Android APK瘦身99.99%

resources.arsc文件中,还包含了对每个资源的引证。

签名

目录“META-INF”中包含有CERT.SFMANIFEST.MFCERT.RSA文件,这些文件都需求 v1 APK 签名

(source.android.com/security/ap…) 。

假如有攻击者修改了咱们 APK 中的代码,签名就会不匹配。这一机制保证了用户能避免执行第三方歹意软件的风险。

MANIFEST.MF文件中列出了 APK 中的所有文件。其间,CERT.SF文件中包含了文件清单的摘要,以及每个文件的独立摘要。CERT.RSA文件中包含了一个公钥,用于验证CERT.SF文件的完整性。

实现Android APK瘦身99.99%

在签名文件中,没有方针明显可优化。

AndroidManifest 文件

看上去AndroidManifest文件十分类似于咱们的原始输入文件。唯一不同在于,文件中的字符串和 Drawable 等资源被整数资源 ID 所替代,这些 ID 以0x7F开始。

启用最小化功用(Minification)

咱们没有在 App 的build.gradle文件中设置允许最小化(Minification)和资源缩短(Resource Shrinking)。咱们现在做此设置:

android {
  buildTypes {
    release {
      minifyEnabled true
      shrinkResources true
      proguardFiles getDefaultProguardFile(
       'proguard-android.txt'), 'proguard-rules.pro'
     }
   }
}
-keep class com.fractalwrench.** { *; }

minifyEnabled特点设置为“true”值,这将启用 Proguard

(www.guardsquare.com/en/proguard) ,

该功用将从 App 中剥离出那些未运用的代码,并对符号的称号做含糊化处理,使得 App 难以被反向工程。

设置shrinkResources特点,将会在 APK 中移除任何并非直接引证的资源。这时假如咱们运用反射机制间接地拜访资源,就会导致问题,可是本文给出的 App 并不存在这样的问题。

优化为 786 Kb(减少 50%)

咱们现已完成了 APK 规划折半,并未对咱们的 APP 有任何可见的影响。

实现Android APK瘦身99.99%

关于那些没有在 App 中启用AndroidManifest.xmlshrinkResources的开发人员,这是本文给出的最需求重视的并应学会的技巧。他们仅花费数小时做装备和测验,就能轻松地减少数兆的规划。

咱们没有了解 AppCompat 的作业机制

现在classes.dex文件已减少到占用 APK 的 57%。在咱们的 Dex 文件中,大多数办法引证属于android.support软件包,因而咱们即将去除该支撑库。具体做法为:

  • build.gradle中彻底清除依赖块。 dependencies { implementation ‘com.android.support:appcompat-v7:26.1.0’ implementation ‘com.android.support.constraint:constraint-layout:1.0.2’ }
  • 更新MainActivity,以扩展android.app.Activity
public class MainActivity extends Activity
  • 更新布局,运用单一的TextView
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:gravity="center"
  android:text="Hello World!" />
  • 删去styles.xml文件,并从AndroidManifest文件的<application>元素中移除android:theme特点。
  • 删去colors.xml文件。
  • 在 gradle 同步时做 50 次上推(push-up)。

优化为 108 Kb(减少 87%)

天哪,咱们刚刚完成了近十倍的减少,即从 786Kb 减少到 108Kb。唯一可见的更改是工具条(Toolbar)的色彩,现在它运用了缺省的 OS 主题。

实现Android APK瘦身99.99%

目录“res”现在占用 APK 规划约 95%,原因是所有的加载图标。假如这些 PNG 图片是由咱们自己的设计师所给出的,那么咱们能够测验 将它们转换为 WebP 格式,该格式愈加高效,并被 API 15 及以上所支撑。

走运的是,Google 现已优化了咱们的 Drawable。即便没有这种优化,ImageOptim 也可优化 PNG 并从中剥离不必要的元数据。

让咱们当一次坏人,将咱们所有的加载图标替换为单一的单像素黑点,并置于未验证的res/drawable目录中。图片巨细约 67 个字节。

优化为 6808 字节(减少 94%)

咱们现已移除了几乎全部的资源,因而毫不古怪 APK 规划现已减少了约 95%。可是resources.arsc依然引证了如下项:

  • 一个布局文件;
  • 一个字符串资源;
  • 一个调用图标。

让咱们从第一项着手。

布局文件(优化为 6262 字节,减少 9%)

Android 结构会胀大咱们的 XML 文件

并主动创立一个TextView对象,用于Activity对象的contentView

咱们能够测验一些越过中心的进程,具体做法是移除 XML 文件,并运用程序设置contentView。这样会降低资源的规划,由于咱们减少了一个 XML 文件。可是 Dex 文件将会增大,由于咱们引证了额外的TextView办法。

TextView textView = new TextView(this);
textView.setText("Hello World!");
setContentView(textView);

让咱们检查一下这一权衡做法的作业情况,它减少了 5710 个字节。

App 称号(优化为 6034 字节,减少 4%)

下面咱们将删去strings.xml文件,并将AndroidManifest中的android:label特点值更改为“A”。这看上去是一个小更改,可是它从resources.arsc中删去了一项,减少了 Manifest 文件中的字符数,并从“res”目录中移除了一个文件。略有裨益,咱们减少了 228 个字节。

加载图标(优化为 5300 字节,减少 13%)

Android Platform 代码库中的resources.arsc的文档

告知咱们,APK 中的每个资源经过resources.arsc中的一个整数 ID 引证。这些 ID 具有两个命名空间(Namespace):

0x01: 体系资源(预装在 framework-res.apk 中);0x7f: 运用资源(捆绑在运用的.apk 文件中)。

那么假如在0x01命名空间中引证了一个资源,咱们的 APK 发生了什么?咱们应该能够在减少文件规划的一起,得到一个更美丽的图标。

android:icon="@android:drawable/btn_star"

实现Android APK瘦身99.99%

虽然文档是这样说的,可是在一个出产 App 中,咱们应该保持“永久不要信赖体系资源”这一准则。该步骤会导致 Google Play 验证失利,而且考虑到咱们知道某些制造商现已重界说了白色

因而在具体操作时需求稳重。

Manifest 文件(优化为 5252 字节,减少 1%)

目前为止,咱们没有对 Manifest 文件下手。

android:allowBackup="true"
android:supportsRtl="true"

移除这些特点将会减少 48 个字节。

避免破解(优化为 4984 字节,减少 5%)

看上去 Dex 文件中依然包括BuildConfigR

-keep class com.fractalwrench.MainActivity { *; }

假如咱们精粹 Proguard 规则,就会清除掉这些类。

命名混杂(优化为 4936 字节,减少 1%)

现在对咱们的Activity赋予一个混杂后的姓名。关于正常类,Proguard 可主动完成混杂功用,可是考虑到Activity类名会经过Intents唤醒,因而缺省情况下不要混杂Activity的姓名。

MainActivity -> c.java
com.fractalwrench.apkgolf -> c.c

META-INF(优化为 3307 字节,减少 33%)

当前在 App 签名中,咱们运用了 v1 和 v2 签名。看上去这彻底是浪费,尤其是 v2 会对整个 APK 做哈希,供给了更高档的保护才能和功能

(source.android.com/security/ap…)。

在 APK Analyser 中,v2 签名并不可见,由于它在 APK 文件自身中以二进制块的办法存在。v1 签名是可见的,它是以CERT.RSACERT.SF文件的办法给出。

Android Studio UI 中供给了 v1 签名的复选框,咱们需求去除该选择,并生成一个签名的 APK。咱们也需求做相反的进程。

签名 巨细(字节)
v1 3511
v2 3307

看上去从此以后咱们运用的是 v2。

下面的操作将无需 IDE 的支撑

现在咱们要手艺修改咱们的 APK 了。咱们将运用如下指令:

# 1. 创立一个未签名的 APK。
./gradlew assembleRelease
# 2. 解紧缩归档文件。
unzip app-release-unsigned.apk -d app
# 对文件进行修改。
# 3. 紧缩归档文件
zip -r app app.zip
# 4. 运转 zipalign。
zipalign -v -p 4 app-release-unsigned.apk app-release-aligned.apk
# 5. 运用 v2 签名运转 apksigner。
apksigner sign --v1-signing-enabled false --ks $HOME/fake.jks --out signed-release.apk app-release-unsigned.apk
# 6. 验证签名。
apksigner verify signed-release.apk

具体概述了 APK 签名进程。总而言之,gradle 生成了一个未签名的归档文件,zipalign 更改了未紧缩资源的字节对齐办法,用于改善加载 APK 时的 RAM 运用,最后 APK 将被加密签名。

未签名且未对齐的 APK 巨细为 1902 字节,这意味着签名和对齐进程增加了约 1 Kb。

文件巨细差异(优化为 2608 字节,减少 21%)

很古怪!咱们对未对齐的 APK 解紧缩并手艺签名,并手动移除了META-INF/MANIFEST.MF,这减少了 543 字节。假如有人知道原因,请告知我!

现在咱们的签名 APK 中只有三个文件,当然还能够去除resources.arsc,由于咱们并未界说任何资源!

这将使咱们仅保留 Manifest 和classes.dex文件,两个文件巨细适当。

紧缩破解(Compression Hack)(优化为 2599 个字节,减少 0.5%)

让咱们将剩余的字符串都更改为‘c’,更新版别为 26,然后生成一个签名的 APK。

compileSdkVersion 26
    buildToolsVersion "26.0.1"
    defaultConfig {
        applicationId "c.c"
        minSdkVersion 26
        targetSdkVersion 26
        versionCode 26
        versionName "26"
    }
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="c.c">
    <application
        android:icon="@android:drawable/btn_star"
        android:label="c"
        >
        <activity android:name="c.c.c">

这将减少 9 个字节。

虽然文件中的字符数并未改变,可是咱们更改了‘c’字符的频次。这使得紧缩算法能够进一步降低文件的巨细。

你好,ADB(优化到 2462 字节,减少 5%)

经过移除“`Activity““的 Launch Intent Filter,咱们能够进一步优化 Manifest。尔后,咱们将运用如下指令加载 App:

adb shell am start -a android.intent.action.MAIN -n c.c/.c

下面给出新的 Manifest 文件:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="c.c">
    <application>
        <activity
            android:name="c"
            android:exported="true" />
    </application>
</manifest>

咱们还移除了加载图标。

减少办法引证(优化为 2179 字节,减少 12%)

咱们开始需求是生成一个可装置在设备上的 APK。现在是运转“Hello World”的时候了。

咱们的 App 引证了TextViewBundleActivity中的办法。经过移除Activity,并替换为用户界说的Application类,咱们能够进一步减少 Dex 文件巨细。现在咱们的 Dex 文件应该仅引证了单一的办法,即Application的构造函数。

现在咱们的源文件如下:

package c.c;
import android.app.Application;
public class c extends Application {}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="c.c">
    <application android:name=".c" />
</manifest>

咱们能够运用 adb 验证该 APK 是能够成功装置的,也能够经过 Setting App 做验证。

实现Android APK瘦身99.99%

Dex 优化(优化为 1961 字节,减少 10%)

在此次优化中,我花费了多个小时研讨 Dex 文件格式

意在了解比如校验码和偏移量等各种机制,它们是手艺修改文件中的难点。

可是长话短说,被我证明的是,只要存在classes.dex文件,APK 文件就能装置。因而,只要简略地删去原始文件并在终端运转touch classes.dex,运用这一空文件就能获得近 10% 的规划减少。

有时看上去最愚笨的办法反而最有用。

了解 Manifest 文件(优化为 1961 字节,减少 0%)

非签名 APK 中的 Manifest 文件是二进制的 XML 格式,该格式看上去并没有官方的文档。咱们能够运用 HexFiend编译器去修改文件内容

(github.com/ridiculousf…) 。

咱们能够猜测出位于文件头部的数个感兴趣项。头四个字节编码了38,是与 Dex 文件所运用的版别相同。随后的两个字节编码为660,这无疑是文件的巨细。

下面,咱们测验经过设置 targetSdkVersion 为1并更新文件巨细头部为659,去删去一个字节。不幸的是,Android 体系拒绝了这个不合法的 APK,因而看上去这儿另有玄机。

无需了解 Manifest 文件(优化为 1777 字节,减少 9%)

下面咱们让咱们对整个文件输入虚字符,然后在不更改文件巨细的情况下测验装置 APK。这将确定校验码是否发挥作用,以及更改是否使得文件头部的偏移值失效。

令人惊奇的是,下图的 Manifest 文件被解释为一个有用的 APK,可运转在运转 Oreo 的 Nexus 5X 手机上:

实现Android APK瘦身99.99%

我想我听到了负责保护BinaryXMLParser.java的 Android Framework 工程师对着枕头在大声尖叫。

为最大化收益,咱们将运用空字节(Null)替换这些虚字符。这可使简化运用 HexFiend 检查文件的重要部分,也将使前期的紧缩破解可减少一些字节。

UTF-8 格式的 Manifest 文件

下图给出了一些 Manifest 文件中的重要成分。假如没有这些成分,APK 将会装置失利。

实现Android APK瘦身99.99%

一些工作立刻是很明显的,例如 Manifest 文件和软件包标记。在字符串池中还能够找到软件包称号和 versionCode。

十六进制的 Manifest 文件

实现Android APK瘦身99.99%

以十六进制检查文件可显现文件头部的值,这些值描绘了字符串池及其它值,例如0x9402是文件的巨细。字符串也具有一种有意思的编码。假如字段超出了 8 个字节,它们的总长度将在随后的两个字节中指定。

可是,看上去咱们并不能从中做更进一步的减少。

功德圆满?(优化为 1757 字节,减少 1%)

让咱们检查一下最终的 APK。

实现Android APK瘦身99.99%

终归,咱们运用 v2 签名在 APK 中留名。让咱们创立一个利用紧缩破解的新密钥库。

实现Android APK瘦身99.99%

这可减少 20 个字节。

第五阶段:最终采纳

现在的1757个字节是适当的小。据我所知,这是最小的现有 APK。

可是我彻底有理由坚信,Android 社区中会有人能再做进一步的优化,并打破我的记录。

更多Android进阶指南 能够具体Vx重视公众号:Android老皮 解锁 《Android十大板块文档》

1.Android车载运用开发体系学习指南(附项目实战)

2.Android Framework学习指南,助力成为体系级开发高手

3.2023最新Android中高档面试题汇总+解析,离别零offer

4.企业级Android音视频开发学习道路+项目实战(附源码)

5.Android Jetpack从入门到精通,构建高质量UI界面

6.Flutter技能解析与实战,跨平台首要之选

7.Kotlin从入门到实战,全方面提升架构基础

8.高档Android插件化与组件化(含实战教程和源码)

9.Android 功能优化实战+360全方面功能调优

10.Android零基础入门到精通,高手进阶之路

敲代码不易,重视一下吧。ღ( ・ᴗ・` )