Android App里实现开机动画替换
1. 传统方式:adb remount + adb push
先说电脑连接开发板时最常见的做法。假设开机动画文件名是 bootanimation.zip,目标路径是 /system/media/bootanimation.zip:
bash
adb root
adb remount
adb push bootanimation.zip /system/media/bootanimation.zip
adb shell chmod 644 /system/media/bootanimation.zip
adb shell chown root:root /system/media/bootanimation.zip
adb reboot
这几步分别完成:
text
adb root → 让 adbd 尝试以 root 身份运行
adb remount → 把系统分区重新挂载为可写
adb push → 把新的 bootanimation.zip 推到设备
chmod/chown → 修正文件权限和属主
adb reboot → 重启后观察开机动画
这套方式要求开发板允许 adb root/adb remount。普通量产版 user build 往往不允许直接修改 /system;开发板通常需要是 eng、userdebug、已关闭验证的内部版本,或者已经配置了相应的 root/写入能力。
另外,开机动画路径不是所有 ROM 都一样。示例使用 /system/media/bootanimation.zip,实际项目要先确认设备的启动动画加载路径,也可能位于 /product/media 或其他分区。
2. 为什么 App 里不能直接执行 adb remount
adb remount 不是开发板上的 Shell 内置命令。它是电脑端 adb Host 程序的子命令:
text
电脑上的 adb 客户端
↓
通过 ADB 传输层向 adbd 打开 "remount:" 服务
↓
开发板上的 adbd 处理 remount 请求
↓
执行系统 remount 逻辑
所以在 App 里执行下面这种代码没有意义:
kotlin
Runtime.getRuntime().exec("adb remount")
App 进程里通常没有 adb 可执行文件;即使把可执行文件打包进来,adb remount 也需要先连接一个 ADB transport,不能把它当作普通 Shell 命令直接在目标设备上执行。
3. 通过 ADB 源码找到 remount:
继续看 ADB 的服务定义,可以看到 remount: 是 adbd 提供的控制服务:
text
adb remount
↓
OPEN "remount:"
↓
adbd 接收服务请求
↓
执行 remount
这也是整个实现的关键:
kotlin
connection.open("remount:")
它不是 shell:remount,也不是 shell:mount -o remount,rw /system。remount: 和 shell: 属于 ADB 协议中的不同服务入口。
4. App 内的前置条件
这里使用:
kotlin
val socket = Socket("127.0.0.1", 5555)
127.0.0.1 表示 App 和 adbd 在同一块开发板上。开发板必须已经让 adbd 监听 TCP 5555,例如开发板端具备 root 能力时可以执行:
kotlin
Runtime.getRuntime().exec(new String[]{"su", "-c", "setprop service.adb.tcp.port 5555; stop adbd; start adbd"})
控制端 App 还需要网络权限:
xml
<uses-permission android:name="android.permission.INTERNET" />
Socket、密钥生成和 ADB 操作不能放在主线程,实际项目要放到后台线程或协程中。下面只贴 ADB 核心代码,不再封装类和辅助函数。
5. App 内最终实现
bootanimation 是已经准备好的本地文件路径,例如 App 从 assets 复制到自己的缓存目录后得到的绝对路径。adblib 可以直接作为依赖使用,也可以像示例工程一样把源码放进 App:
kotlin
implementation("com.tananaev:adblib:1.3")
核心代码如下:
kotlin
import android.util.Base64
import com.tananaev.adblib.AdbConnection
import com.tananaev.adblib.AdbCrypto
import java.net.Socket
val socket = Socket("127.0.0.1", 5555)
val crypto = AdbCrypto.generateAdbKeyPair { data -> Base64.encodeToString(data, Base64.NO_WRAP) }
val connection = AdbConnection.create(socket, crypto)
connection.connect()
connection.open("remount:")
connection.open("shell:cp $bootanimation /system/media/bootanimation.zip")
connection.open("shell:chmod 644 /system/media/bootanimation.zip")
connection.open("shell:chown root:root /system/media/bootanimation.zip")
这段代码对应的实际链路是:
text
Socket 连接本机 adbd
→ ADB RSA 握手
→ 打开 remount: 控制服务
→ 用 shell:cp 替换动画文件
→ 用 shell:chmod 修正权限
→ 用 shell:chown 修正属主
6. 为什么没有继续使用 adb push
严格来说,adb push 走的是 ADB 的 sync: 文件同步协议,不是 shell: 命令。当前示例选择 shell:cp,是因为动画文件已经位于开发板本地:
text
App 本地文件
→ shell:cp
→ /system/media/bootanimation.zip
这样不需要在 App 内再次实现 sync: 协议。换句话说:
text
adb remount → connection.open("remount:")
adb push → 本例用 shell:cp 替代
如果源文件路径对 adbd 不可读,例如 App 私有目录权限是 0700,而 adbd 不是 root,那么 shell:cp 会失败。此时要先把文件放到 /data/local/tmp 等 adbd 可读位置,或者在 adblib 上实现 sync: 的发送流程。
7. 开机动画文件本身的要求
bootanimation.zip 不是普通图片压缩包,通常至少要包含:
text
desc.txt
part0/
part1/
...
desc.txt 定义分辨率、帧率和播放分段,图片目录名和分段顺序由文件内容决定。替换前要确认目标设备的分辨率、像素格式和动画目录结构,否则即使文件复制成功,开机时也可能黑屏、卡住或回退到默认动画。
8. 实际项目容易踩的坑
remount:只是请求开始,不代表一定成功。user build、开启 dm-verity、动态分区或厂商安全策略都可能让 remount 失败。- 示例代码没有读取和关闭
connection.open()返回的AdbStream,适合验证链路;正式实现应该保存返回值、读取错误信息并及时关闭连接。 AdbCrypto.generateAdbKeyPair()每次都会生成新密钥。若开发板要求 RSA 授权,正式版本应该保存并复用密钥对。shell:cp使用的是开发板上的 Shell 权限,源文件和目标文件都必须对该权限可见。- 开机动画目标路径要根据 ROM 确认,不要只在一台设备上硬编码后直接推广。
- 修改
/system前要保留原始bootanimation.zip,写入失败时可以恢复;复制完成后最好校验文件大小和权限。 - 修改系统文件属于开发板系统能力,不要把 root adbd、
remount:和任意 Shell 执行能力暴露给普通用户。