从工程资源目录到 APK 文件结构,再到编译签名全流程,搞懂 Android 应用打包原理
反编译过 APK 的人都知道,一个安装包拆开全是陌生文件——classes.dex、resources.arsc、META-INF 里一堆签名文件。但很少有人能一口气说清楚:工程里的 res 目录怎么组织、清单文件写了什么、编译时到底经历了哪几步。这篇文章把工程文件简介、APK 文件格式、APK 编译三块内容串起来,一次讲清楚。

一、Android 工程文件简介
一个 Android 工程参与编译的就三个:java 文件夹、res 文件夹、清单文件。搞懂这三块,后面看 APK 结构就不费劲了。
1.1 资源 ID
资源被编译时,编译器会自动生成一个 R.java 类,资源 ID 就保存在这个类里。正常情况下,保存资源文件就会生成对应的资源 ID。添加资源文件后,IDE 会即时更新 R 类中的常量定义,开发阶段就能在代码里直接引用。
代码里通过 R.layout.xxx、R.drawable.xxx 引用资源,本质上就是在引用这些常量。R.java 是编译资源的副产品——它把每个资源的文件名映射成一个整型 ID,Java 代码和布局 XML 都通过这个 ID 与具体资源关联。如果不要 R.java,代码里的 ID 号就得全部写死,维护成本极高。

1.2 资源文件命名规范
资源文件命名有四条硬性规范:① 多个单词要用下划线隔开;② 不能用中文;③ 不能数字开头;④ 不能有大写字母。
因为编译器会为资源文件生成常量名,常量名需要遵守 Java 变量命名规范。Android Studio 的「安全删除」选项很实用——删除了目标资源,引用它的代码也会一并被检测并提示删除。
1.3 资源目录结构
新建资源目录时,res 下的子文件夹按资源类型划分,也可以手动新建,比如字体目录 font/。不区分分辨率的目录如 drawable/,放一张图所有设备通用;若是区分分辨率,则每一种分辨率都需要放置对应分辨率的图片,例如 drawable-hdpi、drawable-xhdpi 等。
除了常规图片目录,还有放视频之类媒体的专用目录。assets/ 是自定义资源目录,这里可能藏着代码或特殊格式的文件,不会被编译成 axml,而是原样打包进 APK。raw/ 目录一般放 mp3 等原始文件,通过 InputStream 读取。
· layout/:界面布局文件· drawable/:图片资源(不区分分辨率)· drawable-hdpi、drawable-xhdpi 等:区分分辨率的图片· values/:字符串、颜色、样式等配置· raw/:一般放 mp3 等原始文件· font/:字体目录(可手动新建)· 视频之类的专用目录· assets/:自定义资源目录,这里可能藏着代码
API 版本 21 以上使用 v21 的资源文件夹(如 drawable-v21),否则使用不带版本的资源文件夹。添加资源文件后保存,资源 ID 会自动生成。

1.4 多语言本地化配置
不同语言方案的适配通过 values 子目录完成:values-en 放英文,values-zh 放中文。这是 Android 语言本地化配置的标准做法。
操作步骤:新建对应语言目录,将 values 里的 strings.xml 拷贝到其他两个文件夹里,然后分别更改内容——英文目录改英文文案,中文目录改中文文案。回到工程,准备工作就完成了,编译、运行即可验证。
切换系统语言后重新打开应用:英文环境呈现英文内容,中文环境呈现中文内容。不仅 strings.xml 支持多语言,colors.xml、styles.xml 等 values 下的文件都可以用同样方式适配。

1.5 位图和矢量图
位图:用像素来组成。一个像素的图放大会用相近像素来补,所以会模糊。常见的 PNG、JPG 都是位图,适合照片和复杂图像,但放大会损失清晰度,需要提供多套分辨率资源。
矢量图:用大量三角形来描述图片,三角形切得越小,描述的越清晰、越真实。游戏用矢量图,精细化场景就把三角形切得更小。Android 中的 VectorDrawable 就是矢量图方案,一个文件适配所有分辨率,适合图标和简单图形。

1.6 AndroidManifest 清单文件
四大组件在清单文件的标签名分别是:activity、service、broadcastreceiver、contentprovider。每个组件都必须在清单中注册,系统才能识别和管理。
清单中的「.」代表当前包名——编译完 Gradle 脚本后包名就会被加入这里,应用中所有点都代表这个包名;不使用点的写法需要自己带上完整包名。比如 android:name=".MainActivity" 实际指向的是 com.example.app.MainActivity。设置包名在 application 标签的 package 属性中完成。
几个关键配置项:· application:描述应用全局属性,设置包名· activity + intent-filter:通知系统新添加的界面· action:描述主界面· category:描述桌面图标,删除就没桌面图标了,但软件仍在安装列表· uses-permission:权限描述,声明应用需要使用的权限· uses-feature:描述需要的硬件特征(如摄像头、蓝牙等)
权限描述(uses-permission)告诉系统和用户这个应用会访问哪些敏感能力。如果清单里没声明权限,运行时调用相关 API 会直接崩溃。硬件特征(uses-feature)则用于 Google Play 的设备过滤——没有对应硬件的设备将看不到该应用。

1.7 Intent 过滤器与桌面图标
Intent 过滤器(intent-filter)是清单文件中的核心配置。action 描述主界面,category 描述桌面图标——删了 category 就没桌面图标了,但软件还在安装列表里。
主界面图标有方形图标和圆形图标两种,根据系统主题确定使用哪一个。修改图标有两种方式:① 修改清单里的图标引用;② 直接修改图标资源文件。

1.8 Gradle 脚本配置
Gradle 脚本分两层:整个工程的脚本配置(项目级 build.gradle)和当前模块的脚本配置(模块级 build.gradle)。项目级脚本管理全局依赖和插件版本,模块级脚本配置 compileSdkVersion、minSdkVersion、targetSdkVersion 等 SDK 版本,以及 JDK 版本、应用 versionCode 和 versionName。
手动编译或调试构建工具时,需要配置 SDK 环境:把 Android SDK 下的 platform-tools 和 tools 两个目录加入系统 Path 环境变量,才能在命令行直接调用 aapt、aapt2、adb 等工具。

1.9 资源引用方式
清单文件中的元素、属性遵循 XML 规范。如果属性有多个值的写法,可以重复提供同一个属性名。资源文件里引用资源使用 @资源文件夹/资源ID号,例如 @drawable/ic_launcher。代码文件中使用资源则通过 R.xxx.xxx 的形式。字符串中的特殊字符需要用转义字符处理。
二、APK 文件格式
APK 本质上是一个 ZIP 压缩包,用解压工具直接打开就能看到内部结构。编译工具目录里放着不同版本的编译工具,不同 Android Gradle Plugin 版本会选用不同工具链。
理解 APK 格式,关键是分清「编译前」和「编译后」的资源形态:工程里是可读的 XML 文本,APK 里是可快速加载的二进制格式。清单文件编译后就是 axml 格式,用 010 Editor 等十六进制编辑器可以解析查看。
增量编译:每个资源文件单独编译成一个文件,改了谁就只编译谁,连接时再合并所有文件,省时又省力。aapt 只能全部编译资源;aapt2 可以增量编译资源,还支持 dump 命令,将 axml 二进制解压缩为 xml 查看。

2.1 APK 结构
把 APK 当作 ZIP 解压,就能看到完整的文件结构。编译工具目录里放着不同版本的 aapt、aapt2、d8 等工具,不同 Android Gradle Plugin 版本会选用不同工具链。
APK 内部主要文件:· AndroidManifest.xml:清单文件,编译后是 axml 格式· classes.dex:Java 字节码转换后的 Dalvik 可执行文件· resources.arsc:values 类资源的二进制索引表· res/:layout、drawable 等,编译后打包成 axml· lib/:各 CPU 架构的 native 库(armeabi-v7a、arm64-v8a 等)· assets/:原样打包的自定义资源· META-INF/:签名相关文件目录

2.2 资源为什么需要编译
因为 Java 代码使用了资源 ID,所以必须先编译资源。资源为什么需要编译?xml 格式解析速度慢,没有二进制快。因此资源编译器将 xml 格式转换为二进制结构体(axml 格式的二进制格式)。清单文件编译后就是 axml 格式的,可以使用 010 Editor 软件解析查看。
资源文件编译后分为两部分:① res 文件夹内资源文件打包成 axml;② values 文件夹内资源文件打包成 resources.arsc。

三、APK 编译流程
完整编译流程七步,手动编译和 Gradle 自动编译底层逻辑一致。下面就逐步拆解每一步在做什么。

3.1 编译资源(aapt2 compile)
第一步:编译资源。res 目录下的资源都要一个个编译,每个文件产出独立的编译结果,这就是增量编译的基础。编译资源的同时会顺便生成 R.java 文件——这个文件可以不要,不要的话代码里 ID 号就得写死。
aapt2 compile 命令把 layout、drawable 等 XML 资源编译成扁平化的中间文件。编译资源目录后,会生成压缩包形式的中间产物。高版本项目手动编译时,有时需要删除大量的高版本资源和配置信息才能编译通过。
xml 文本转 axml 二进制,靠的就是 aapt/aapt2 这套工具。编译结果可以在中间目录里查看,确认每个资源是否被正确处理。
3.2 链接 APK(aapt2 link)
第二步:链接 APK。aapt2 link 用 /i 指明版本号,--java 参数指定 R.java 的生成路径。需要引用 android.jar 作为 Android 框架 API,它包含了 Android SDK 中所有系统类的定义。
链接阶段把所有 compile 产出的碎片合并,生成 resources.arsc 索引表和完整的 res 目录结构,同时输出 R.java 供后续 javac 编译使用。这一步相当于把散落的资源编译结果「组装」成 APK 的资源部分。
3.3 编译代码(javac)
第三步:编译代码。通过 javac 把 Java 源码(含自动生成的 R.java)编译成 class 字节码文件。javac 需要指定 classpath,包含 android.jar 和项目依赖的所有 jar。所有 .java 文件都会被编译成对应的 .class 文件。
3.4 生成 dex 文件
第四步:生成 dex 文件。通过 dx 或 d8 工具将 class 文件转换为 dex 文件。Android 运行时不能直接执行 Java 字节码,dex 是 Dalvik/ART 虚拟机能识别的格式。dex 出来以后拖进 APK 包(或让构建工具自动放入)。一个 APK 里可以有多个 dex 文件(classes.dex、classes2.dex 等),当方法数超过 65536 限制时就会分包。
3.5 zipalign 对齐优化
第六步:对齐。zipalign 做软件对齐优化——APK 作为 ZIP 包,内部文件如果未按 4 字节边界对齐,系统在加载时无法直接 mmap,需要额外拷贝,影响启动速度。图片顺序错了也会影响对齐效果。
对齐应该在签名之前执行。不对齐也能跑,但内存对齐更好。对齐完成后文件会略微变大,这是正常现象——因为 ZIP 条目之间插入了填充字节来满足对齐要求。

3.6 数字签名(jarsigner)
第七步:数字签名。没签名安装不了——APK 签名用 jarsigner 工具完成,没有签名的 APK 系统拒绝加载。签名是对压缩包里每个文件进行加密,得到加密值存储到 META-INF 目录下的签名文件中。
重新打包的流程是:修改资源后重新计算每个文件的 HASH,新的 HASH 覆盖原来的 HASH,然后用私钥重新加密密文,再提供新的公钥——就是把密文和公钥都换了。如果只有 HASH 变了但签名没更新,安装时校验就会失败。

3.7 文件校验与加密原理
文件校验算法方面,可以在文件后加垃圾数据去计算得到 MD5 去碰撞——但 MD5 已经被数学公式碰撞了,不能作为可靠校验手段。APK 签名实际是对压缩包里每个文件计算 HASH 值。
加密分为两种:对称加密和非对称加密(公钥私钥)。公钥私钥是一套一套对应的,可以理解为穷举碰撞才能找到解。数字证书:用私钥加密出 sha256 的密文,再把公钥放进去。用公钥解密密文,就知道 HASH 值了。

3.8 META-INF 签名文件
打开 APK 包,签名文件在 META-INF 目录下:· MANIFEST.MF(MF):每个文件的 HASH 值· CERT.SF(SF):MF 文件的 HASH 值· CERT.RSA(RSA):公钥信息 + HASH 值
SF 的 hash 值和公钥都在 RSA 里,想要修改 RSA 必须要有私钥。重新打包:密文用自己的私钥加密,再提供新的公钥——就是把密文和公钥都换了。

3.9 生成密钥与签名操作
生成一个公钥私钥的 key:算法是 RSA,位数是 256,时间是 10000 天。输入私钥密码后,再为私钥密码生成一个 2048 位的证书。对应脚本执行后,生成公钥私钥文件,最后用 jarsigner 完成签名。就此,APK 就编译、链接、签名完成了。

3.10 签名安全与中间人攻击
签名验证失败时,系统检查软件跟原始公钥不一样,就认为是盗版软件。杀毒软件检测公钥不一样就直接干掉 APK。这也是为什么二次打包的修改版 APK 经常被杀软拦截。
数字签名漏洞之一是中间人攻击:中间人拦截通信链路,用公钥加密解密。对官方都是原公钥,对下游则是公钥解密修改后转发给其他人,或公钥加密后提交给官方。攻击者可以在用户不知情的情况下篡改 APK 内容,同时让官方端看到的签名仍然有效。

3.11 版本更新与密钥管理
软件下个版本公钥私钥要变吗?规则很清晰:· 包名一样,公钥也一样 → 系统选择覆盖安装· 包名一样,公钥不一样 → 系统拒绝安装· 软件需要卸载安装,说明公钥不一样
软件不需要卸载安装的两个可能性:① 公钥一样;② 包名不一样。公钥私钥全换,除了官方通知,否则都检测不出来。
四、环境配置与安装测试
编译出 APK 后需要安装测试。配置 SDK 环境时,Path 环境变量需要设置两个目录:platform-tools(含 adb、fastboot)和 tools(含 aapt 等旧工具)。
adb 安装软件时,手机要连接电脑,设置里要开启 USB 调试;Android 模拟器不用手动开,自动开启 USB 调试。连接成功后用 adb install xxx.apk 即可安装。没签名的 APK 安装时会提示解析包错误,装不了。
调试常用命令:adb shell ps 遍历进程,查看当前运行的应用;adb shell ls 遍历设备上的文件。fastboot 用来刷机,进入 fastboot 模式后刷入系统镜像。Windows 命令行下 cd /d 可以自动切换盘符,跨盘符执行编译脚本时经常用到。

五、总结
工程参与编译就三块:java 源码、res 资源、AndroidManifest 清单
资源编译自动生成 R.java,资源 ID 全保存在 R 类常量里
资源命名四规则:下划线分隔、无中文、无数字开头、无大写字母
res 目录按类型划分:layout/drawable/values/raw/assets,v21+ 用带版本号文件夹
多语言适配:values-en 英文、values-zh 中文,拷贝 strings.xml 分别改内容
位图用像素组成放大模糊,矢量图用三角形描述放大更清晰
四大组件清单标签:activity/service/broadcastreceiver/contentprovider
清单中「.」代表当前包名,编译后 Gradle 包名写入清单
intent-filter:action 描述主界面,category 描述桌面图标,删了 category 无图标但仍在安装列表
权限 uses-permission 和硬件特征 uses-feature 必须在清单声明
资源引用两种方式:XML 用 @文件夹/ID,代码用 R.xxx.xxx
APK 本质是 ZIP 包,清单和 res 编译成 axml,values 编译成 resources.arsc
aapt 全量编译,aapt2 支持增量编译,改了谁编译谁链接时合并
因 Java 代码使用资源 ID,必须先编译资源再编译代码
七步编译:aapt2 compile → link → javac → dex → 打包 → zipalign → jarsigner 签名
zipalign 对齐在签名之前,不对齐能跑但内存对齐更好,对齐后文件会变大
无签名 APK 系统拒绝加载,META-INF 下 MF 记文件 HASH、SF 记 MF HASH、RSA 存公钥+HASH
数字证书:私钥加密 sha256 密文,公钥解密验 HASH;改 RSA 必须有私钥
包名一样+公钥一样=覆盖安装,包名一样+公钥不一样=拒绝安装需先卸载
SDK 环境 Path 配两个目录,adb 装包要 USB 调试,fastboot 刷机,ps/ls 调试
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
如果觉得有帮助,欢迎转发!
关注公众号,输出安卓逆向与底层技术干货。
#APK编译#Android工程#资源文件#aapt2#数字签名#AndroidManifest#四大组件#zipalign#META-INF #安卓逆向
夜雨聆风