AppCompat
和ConstraintLayout
的支持库;- 一个
AndroidManifest.xml
文件; - PNG 格式的启动图标,分别是正方形、圆形和前台的。
看上去首当其冲的目标是启动图标文件,因为 APK 中共包含了 15 个图像文件,并且在mipmap-anydpi-v26
下还有两个 XML 文件。下面,让我们使用 Android Studio 的 APK Analyser
(https://developer.android.com/studio/build/apk-analyzer.html)
对该 APK 文件做一个定量分析。
给出的结果与我们的最初假设大相径庭,其中显示 Dex 文件是大头,而上述资源仅占 APK 大小的 20%。
文件 | 大小占比 |
---|---|
classes.dex | 74% |
res | 20% |
resources.arsc | 4% |
META-INF | 2% |
AndroidManifest.xml | <1% |
下面让我们逐个分析每个文件的行为。
####Dex 文件
看上去罪魁祸首是classes.dex
文件,它占据了 73% 的空间,因而它成为我们的首要削减目标。该文件为 Dex 格式
(https://source.android.com/devices/tech/dalvik/dex-format) ,
其中包含了我们的全部编译后代码,以及对 Android 框架和支持库中外部方法的引用。
然而android.support
软件包中引用了超过 13000 种的方法,对于一个简单的“Hello World”App 而言,完全没有必要。
####资源
目录“res”中包含了大量的布局(Layout)文件、Drawable 和动画,它们并非在 Android Studio UI 中立刻可见。同样,它们也是由支持库推入其中的,约占 APK 规模的 20%。
在resources.arsc
文件中,还包含了对每个资源的引用。
####签名
目录“META-INF
”中包含有CERT.SF
、MANIFEST.MF
和CERT.RSA
文件,这些文件都需要 v1 APK 签名
(https://source.android.com/security/apksigning/v2#v1-verification) 。
如果有攻击者修改了我们 APK 中的代码,签名就会不匹配。这一机制保障了用户能避免执行第三方恶意软件的风险。
在MANIFEST.MF
文件中列出了 APK 中的所有文件。其中,CERT.SF
文件中包含了文件清单的摘要,以及每个文件的独立摘要。CERT.RSA
文件中包含了一个公钥,用于验证CERT.SF
文件的完整性。
在签名文件中,没有目标明显可优化。
####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
(https://www.guardsquare.com/en/proguard) ,
该功能将从 App 中剥离出那些未使用的代码,并对符号的名称做模糊化处理,使得 App 难以被反向工程。
设置shrinkResources
属性,将会在 APK 中移除任何并非直接引用的资源。这时如果我们使用反射机制间接地访问资源,就会导致问题,但是本文给出的 App 并不存在这样的问题。
##优化为 786 Kb(削减 50%)
我们已经实现了 APK 规模减半,并未对我们的 APP 有任何可见的影响。
对于那些尚未在 App 中启用AndroidManifest.xml
和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
。
- 删除
styles.xml
文件,并从AndroidManifest
文件的<application>
元素中移除android:theme
属性。 - 删除
colors.xml
文件。 - 在 gradle 同步时做 50 次上推(push-up)。
##优化为 108 Kb(削减 87%)
天哪,我们刚刚实现了近十倍的削减,即从 786Kb 削减到 108Kb。唯一可见的更改是工具条(Toolbar)的颜色,现在它使用了缺省的 OS 主题。
目录“res”现在占用 APK 规模约 95%,原因是所有的加载图标。如果这些 PNG 图片是由我们自己的设计师所给出的,那么我们可以尝试 将它们转换为 WebP 格式,该格式更加高效,并被 API 15 及以上所支持。
幸运的是,Google 已经优化了我们的 Drawable。即便没有这种优化,ImageOptim 也可优化 PNG 并从中剥离不必要的元数据。
让我们当一次坏人,将我们所有的加载图标替换为单一的单像素黑点,并置于未验证的res/drawable
目录中。图片大小约 67 个字节。
##优化为 6808 字节(削减 94%)
我们已经移除了几乎全部的资源,因此毫不奇怪 APK 规模已经削减了约 95%。但是resources.arsc
依然引用了如下项:
- 一个布局文件;
- 一个字符串资源;
- 一个调用图标。
让我们从第一项着手。
####布局文件(优化为 6262 字节,削减 9%)
Android 框架会膨胀我们的 XML 文件
(https://developer.android.com/reference/android/view/LayoutInflater.html) ,
并自动创建一个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
的文档
(https://android.googlesource.com/platform/frameworks/native/+/jb-dev/libs/utils/README)
告诉我们,APK 中的每个资源通过resources.arsc
中的一个整数 ID 引用。这些 ID 具有两个命名空间(Namespace):
0x01: 系统资源(预装在 framework-res.apk 中);
0x7f: 应用资源(捆绑在应用的.apk 文件中)。
那么如果在0x01
命名空间中引用了一个资源,我们的 APK 发生了什么?我们应该可以在削减文件规模的同时,得到一个更漂亮的图标。
android:icon=“@android:drawable/btn_star”
虽然文档是这样说的,但是在一个生产 App 中,我们应该保持“永远不要信任系统资源”这一原则。该步骤会导致 Google Play 验证失败,而且考虑到我们知道某些制造商已经重定义了白色
(https://www.reddit.com/r/androiddev/comments/71fpru/android_color_resources_not_safe/),
因此在具体操作时需要慎重。
####Manifest 文件(优化为 5252 字节,削减 1%)
目前为止,我们尚未对 Manifest 文件下手。
android:allowBackup=“true”
android:supportsRtl=“true”
移除这些属性将会削减 48 个字节。
####防止破解(优化为 4984 字节,削减 5%)
看上去 Dex 文件中依然包括BuildConfig
和R
。
-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 做哈希,提供了更高级的保护能力和性能
(https://source.android.com/security/apksigning/#apk-signing-schemes)。
在 APK Analyser 中,v2 签名并不可见,因为它在 APK 文件本身中以二进制块的形式存在。v1 签名是可见的,它是以CERT.RSA
和 CERT.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”
}
<application
android:icon=“@android:drawable/btn_star”
android:label=“c”
这将削减 9 个字节。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
尾声
评论里面有些同学有疑问关于如何学习material design控件,我的建议是去GitHub搜,有很多同行给的例子,这些栗子足够入门。
有朋友说要是动真格的话,需要NDK以及JVM等的知识,首现**NDK并不是神秘的东西,**你跟着官方的步骤走一遍就知道什么回事了,无非就是一些代码格式以及原生/JAVA内存交互,进阶一点的有原生/JAVA线程交互,线程交互确实有点蛋疼,但平常避免用就好了,再说对于初学者来说关心NDK干嘛,据鄙人以前的经历,只在音视频通信和一个嵌入式信号处理(离线)的两个项目中用过,嵌入式信号处理是JAVA->NDK->.SO->MATLAB这样调用的我原来MATLAB的代码,其他的大多就用在游戏上了吧,一般的互联网公司会有人给你公司的SO包的。
至于JVM,该掌握的那部分,相信我,你会掌握的,不该你掌握的,有那些专门研究JVM的人来做,不如省省心有空看看计算机系统,编译原理。
一句话,平常多写多练,这是最基本的程序员的素质,尽量挤时间,读理论基础书籍,JVM不是未来30年唯一的虚拟机,JAVA也不一定再风靡未来30年工业界,其他的系统和语言也会雨后春笋冒出来,但你理论扎实会让你很快理解学会一个语言或者框架,你平常写的多会让你很快熟练的将新学的东西应用到实际中。
初学者,一句话,多练。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
的SO包的。**
至于JVM,该掌握的那部分,相信我,你会掌握的,不该你掌握的,有那些专门研究JVM的人来做,不如省省心有空看看计算机系统,编译原理。
一句话,平常多写多练,这是最基本的程序员的素质,尽量挤时间,读理论基础书籍,JVM不是未来30年唯一的虚拟机,JAVA也不一定再风靡未来30年工业界,其他的系统和语言也会雨后春笋冒出来,但你理论扎实会让你很快理解学会一个语言或者框架,你平常写的多会让你很快熟练的将新学的东西应用到实际中。
初学者,一句话,多练。