我们将这一原则应用于Android。创建可以安装在运行Oreo的设备上的最小的应用程序。
测量基线
我们将开始使用Android Studio生成的默认应用。我们创建一个密钥库,签署应用程序,并使用字节来测量文件大小stat -f%z $filename
。
我们还将在运行Oreo的Nexus 5x上安装APK,以确保一切正常。
美丽。我们的APK重量约为1.5Mb。
APK分析仪
1.5Mb似乎很多考虑到我们的应用程序,所以让我们探索这个项目,并寻找任何快速的胜利。Android Studio已经生成:
A
MainActivity
,延伸AppCompatActivity
。具有
ConstraintLayout
根视图的布局文件。包含三种颜色的值文件,一个字符串资源和一个主题。
该
AppCompat
和ConstraintLayout
支持库。一
AndroidManifest.xml
方形,圆形和前景启动器图标PNG。
图标看起来像最简单的目标,因为总共有15张图片,下面有2个XML文件mipmap-anydpi-v26
。我们来量化使用Android Studio的APK分析器。
与我们的初始假设相反,似乎我们的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
软件包引用了超过13,000种方法,对于Hello World应用程序来说,这似乎过多了。
资源
我们的res目录有大量的布局文件,可绘制和动画,在Android Studio的UI中不会立即显示。再次,这些已经从支持库中拉入,并且占据了APK大小的大约20%。
该resources.arsc
文件还包含对这些资源中的每一个的引用。
签名
该META-INF
文件夹包含CERT.SF
,,MANIFEST.MF
和CERT.RSA
文件,这是v1 APK签名所必需的。如果攻击者修改了我们的APK中的代码,则签名将不匹配,这意味着用户将被保存以执行第三方恶意软件。
MANIFEST.MF
列出APK中的文件,而CERT.SF
包含清单的摘要,以及每个文件的单个摘要。CERT.RSA
包含用于验证其完整性的公钥CERT.SF
。
这里没有明显的目标。
AndroidManifest
AndroidManifest看起来与我们原始的输入文件非常相似。唯一的例外是字符串和可绘制的资源已经被替换为整数资源ID,从头开始0x7F
。
启用分类
我们还没有尝试在我们的应用程序build.gradle
文件中实现小型化和资源缩减。让我们一起去吧
android { buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile( 'proguard-android.txt'), 'proguard-rules.pro' } }}
-keep class com.fractalwrench.** { *; }
设置minifyEnabled
为true可以使Proguard从我们的应用程序中删除未使用的代码。它也模糊符号名称,使得更难以逆向工程的应用程序。
shrinkResources
将从我们的APK中删除任何不直接引用的资源。如果您使用反射来间接访问资源,这可能会导致问题,但这不适用于我们的应用程序。
786 Kb(减少50%)
我们将APK大小减少一半,对我们的应用程序没有明显的影响。如果您还没有启用minifyEnabled
和shrinkResources
在您的应用程序中,这是您应该从这篇文章中删除的最重要的一件事。您可以轻松地节省几兆字节的空间,只需几个小时的配置和测试。
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
并删除该android:theme
属性<application>
AndroidManifest
删除
colors.xml
在渐进同步的同时做50个俯卧撑
108 Kb字节(减少87%)
圣牛,我们刚刚从786Kb降低了10倍,降至108Kb。唯一可辨别的更改是工具栏颜色,现在使用默认的操作系统主题。
由于所有这些启动器图标,res目录现在占用了我们的APK大小的95%。如果这些PNG由我们的设计人员提供,那么我们可以尝试将它们转换为WebP,这是API 15及更高版本支持的更有效的文件格式。
幸运的是,Google已经优化了我们的绘图,尽管如果不是这样,ImageOptim也可以优化并从PNG中删除不必要的元数据。
让我们成为一个不好的公民,并用一个1像素的黑点替换所有的发射图标,放在不合格的res/drawable
文件夹中。该图像重67页。
6808字节(减少94%)
我们已经摆脱了我们几乎所有的资源,所以我们看到我们的APK大小减少了95%,这并不奇怪。以下项目仍然参考resources.arsc
:
1个布局文件
1字符串资源
1启动器图标
我们从顶部开始吧。
布局文件(6262字节,减少9%)
Android框架将膨胀我们的XML文件,并自动创建一个TextView
对象设置为contentView
的Activity
。
我们可以尝试通过删除XML文件来跳过中间人,并以编程方式设置contentView。我们的资源大小会减少,因为XML文件少一些,但是我们的Dex文件会增加,因为我们将引用其他TextView
方法。
TextView textView = new TextView(this);textView.setText("Hello World!");setContentView(textView);
看起来我们的权衡已经有效了,我们下降到5710字节。
应用名称(6034字节,减少4%)
我们删除strings.xml
,并android:label
在AndroidManifest 替换为“A”。这可能看起来像是一个小小的变化,但删除一个条目resources.arsc
,减少清单中的字符数,并从res目录中删除一个文件。每一点帮助 - 我们刚刚保存了228个字节。
启动器图标(5300字节,减少13%)
该用于resources.arsc文档在Android平台库告诉我们,在APK每个资源被引用的resources.arsc
一个整数ID。这些ID有两个命名空间:
0x01:系统资源(预安装在framework-res.apk中)
0x7f:应用程序资源(捆绑在应用程序中.apk)
那么如果我们引用0x01命名空间中的资源,我们的APK会发生什么?我们应该能够获得更好的图标,同时减少我们的文件大小。
android:icon="@android:drawable/btn_star"
不用说,但是您不应该在生产应用程序中相信系统资源。这一步将失败Google Play验证,并且考虑到某些制造商已经知道重新定义颜色为白色,请谨慎行事。
清单(5252字节,减少1%)
我们还没有触及清单。
android:allowBackup="true"android:supportsRtl="true"
删除这些属性可以节省48个字节。
保卫黑客(4984字节,减少5%)
它看起来像BuildConfig
和R
仍然包括在塞米松文件。
-keep class com.fractalwrench.MainActivity { *; }
优化我们的Proguard规则将剥离这些类。
混淆(4936字节,减少1%)
让我们的Activity成为一个混淆的名字。Proguard自动为常规类执行此操作,但是由于可以通过Intents调用Activity类名称,因此默认情况下不会进行混淆。
MainActivity - > c.java
com.fractalwrench.apkgolf - > cc
META-INF(3307字节,减少33%)
目前,我们正在使用v1和v2签名签署我们的应用程序。这似乎是浪费的,特别是通过散列整个APK,v2提供卓越的保护和性能。
我们的v2签名在APK分析器中不可见,因为它在APK文件本身中被包含为二进制块。我们的v1签名是可见的,CERT.RSA
以及CERT.SF
文件的形式。
让我们取消选中Android Studio界面中的v1签名复选框,并生成一个已签名的APK。我们也会尝试相反的做法。
签名 | 尺寸 |
---|---|
V1 | 3511 |
V2 | 3307 |
看来我们现在将使用v2。
我们要去哪里,我们不需要IDE
现在是手动编辑我们的APK了。我们将使用以下命令:
# 1. Create an unsigned apk./gradlew assembleRelease# 2. Unzip archiveunzip app-release-unsigned.apk -d app# Do any edits# 3. Zip archivezip -r app app.zip# 4. Run zipalignzipalign -v -p 4 app-release-unsigned.apk app-release-aligned.apk# 5. Run apksigner with v2 signatureapksigner sign --v1-signing-enabled false --ks $HOME/fake.jks --out signed-release.apk app-release-unsigned.apk# 6. Verify signatureapksigner verify signed-release.apk
可以在这里找到APK签名的详细概述。总而言之,gradle生成一个无符号归档,zipalign更改未压缩资源的字节对齐方式,以便在APK加载时提高RAM使用率,最后,APK被加密地签名。
我们的未签名和未对齐的APK重量在1902字节,这表明这个过程增加了大约1Kb。
文件大小差异(2608字节,减少21%)
奇怪的!解压缩未对齐的APK并手动删除META-INF/MANIFEST.MF
,保存我们543字节。如果有人知道为什么会这样,请联系!
我们现在已经在我们签名的APK中下载了3个文件。但是,我们也可以摆脱resources.arsc
,因为我们没有定义任何资源!
这使我们有清单和classes.dex
文件,每个文件大小相同。
压缩黑客(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%)
我们可以通过删除我们活动的启动意图过滤器来进一步优化我们的清单。从现在开始,我们将使用以下命令启动我们的应用程序:
adb shell am start -a android.intent.action.MAIN -n c.c/.c
这是我们的新清单:
<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去的时候了。
我们的应用程序引用的方法中TextView
,Bundle
和Activity
。我们可以通过删除此活动来减少我们的Dex文件大小,并用自定义Application
类替换它。我们的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已成功安装,还可以通过“设置”应用进行检查。
Dex优化(1961字节,减少10%)
我花了几个小时研究Dex文件格式进行此优化,因为诸如校验和和偏移之类的各种机制使人工编辑变得更加困难。
然而,为了缩短长篇小说,事实证明,APK安装的唯一要求是classes.dex
文件必须存在。因此,我们只需删除原始文件,touch classes.dex
在终端中运行,并使用空文件减少10%。
有时最愚蠢的解决方案是最好的。
了解清单(1961字节,减少0%)
我们的未签名APK的清单是二进制XML格式,似乎没有正式记录。我们可以使用HexFiend编辑器来操作文件内容。
我们可以猜到文件头中的几个有趣的项目 - 前四个字节的编码38
,这是与Dex文件相同的版本号。接下来的两个字节编码660
,这是文件大小的方便。
我们尝试通过将targetSdkVersion设置为1
,并将文件大小标题更新为一个字节659
。不幸的是,Android系统拒绝将其视为无效的APK,所以看起来这里有一些额外的复杂性。
不了解清单(1777字节,减少9%)
我们在整个文件中输入哑字符,然后尝试安装APK,而不更改文件大小。这将确定是否存在校验和,或者如果我们的更改使文件头中的偏移值无效。
令人惊讶的是,以下清单被解释为运行Oreo的Nexus 5X上的有效APK:我想我可以听到Android框架工程师负责保持BinaryXMLParser.java
尖叫声大声地进入枕头。
为了最大化我们的收益,我们将用空字节替换这些虚拟字符。这将使您可以更轻松地查看HexFiend中文件的重要部分,并从先前的压缩黑客中获取字节数。
UTF-8清单
这些是清单的基本组件,没有这些组件,APK无法安装。一些事情很明显 - 比如清单和包标签。versionCode和包名也可以在字符串池中找到。
十六进制显示
以十六进制查看文件显示文件头中描述字符串池和其他值(如文件大小)的值0x9402
。字符串也有一个有趣的编码 - 如果它们超过8个字节,它们的总长度在前面的2个字节中指定。
然而,这看起来并不像在这里有更多的收获。
完成了吗?(1757字节,减少1%)
我们来检查一下最终的APK。毕竟这一次,我通过v2签名将我的名字留在了APK中。让我们创建一个利用压缩黑客的新密钥库。
这节省了我们20个字节。
阶段5:验收
1757
字节很小,据我所知,这是存在的最小的APK。
不过,我有信心在Android社区的某个人进行进一步的优化,这将会打败我的分数。