Files
android-backup-gui/function-call-practical-analysis.md

810 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 结合软件实际用途的函数调用深度分析
## 软件定位理解
**Android Backup GUI** 是一款 **Root 级别的 Android 应用备份工具**,目标用户是:
- 需要完整备份应用APK + 数据 + 配置)的高级用户
- 需要增量去重备份到远程存储SMB/WebDAV的用户
- 需要批量操作、自动化备份的技术人员
**核心价值**:可靠、完整、增量备份
---
## 一、核心功能调用优先级分析
### 🔴 P0 - 必须完美(用户核心体验)
#### 1. **备份完整性保证**
```
BackupOperation.backupApps()
├─ backupUserData() ⭐⭐⭐ [最核心]
│ └─ 调用链: ~55 次 RootShell.exec
├─ backupObb() ⭐⭐ [游戏应用关键]
│ └─ 调用链: ~8 次 RootShell.exec
├─ backupSsaid() ⭐⭐ [广告/设备标识关键]
│ └─ 调用链: ~3 次 RootShell.exec
└─ writeFileForBackup() ⭐⭐ [数据完整性关键]
└─ 调用链: ~8 次调用
```
**分析**
- 这些函数是备份的核心,失败意味着数据丢失
- RootShell.exec 调用多是因为需要 root 权限访问系统目录
- **不能过度优化**:宁可多次调用保证可靠性
**建议**
```kotlin
// 保持现有逻辑,但增加详细日志
backupUserData() {
Log.i(TAG, "开始备份 $packageName 用户数据")
// ... 原有逻辑 ...
Log.i(TAG, "备份 $packageName 完成: 大小=${size}MB, 耗时=${elapsed}ms")
}
```
#### 2. **Restic 增量备份可靠性**
```
ResticBackup.backup() ⭐⭐⭐
├─ ResticCommandRunner.runResticStreaming() [进程管理]
│ └─ stderr 排空线程 + 超时控制
└─ BackendExecutor.withBackend() [后端抽象]
├─ 本地: 直接环境变量
└─ 远程: RestBridgeRunner + RemoteTransport
```
**分析**
- 这是增量备份的核心,影响用户数据安全
- stderr 排空逻辑很重要(防止管道死锁)
- REST 桥稳定性直接影响远程备份成功率
**建议**
```kotlin
// 增加重试机制
suspend fun backupWithRetry(
maxRetries: Int = 2,
block: suspend () -> AppResult<BackupSummary>
): AppResult<BackupSummary> {
repeat(maxRetries) { attempt ->
val result = block()
if (result.isSuccess) return result
Log.w(TAG, "备份失败,重试 ${attempt + 1}/$maxRetries")
delay(1000L * (attempt + 1))
}
return block() // 最后一次尝试
}
```
#### 3. **恢复操作的准确性**
```
RestoreOperation.restoreApps() ⭐⭐⭐
├─ installApk() [APK 安装]
│ └─ pm install -r -t (保留数据,允许测试包)
├─ restoreUserData() [数据恢复]
│ └─ tar 解压到 /data/data
└─ restoreSsaid() [SSAID 恢复]
└─ settings put secure ssaid_$uid
```
**分析**
- 恢复失败会导致应用数据丢失
- 需要精确的权限控制chown, chmod
- 必须保证原子性(失败时回滚)
**建议**
```kotlin
// 增加恢复前验证
suspend fun restoreUserData(pkg: String, archive: File): Boolean {
// 1. 验证归档完整性
if (!verifyArchive(archive)) {
Log.e(TAG, "归档完整性验证失败")
return false
}
// 2. 备份原数据(以防万一)
val backupDir = File(cacheDir, "restore_backup/$pkg")
backupOriginalData(pkg, backupDir)
// 3. 执行恢复
return try {
extractArchive(archive)
// 4. 验证恢复结果
verifyRestoredData(pkg)
} catch (e: Exception) {
Log.e(TAG, "恢复失败,回滚到原数据", e)
rollback(backupDir)
false
}
}
```
---
### 🟡 P1 - 重要功能(影响用户体验)
#### 1. **密码安全管理**
```
PasswordManager ⭐⭐
├─ init() [初始化加密存储]
├─ getResticPassword() [读取密码]
└─ setResticPassword() [存储密码]
```
**调用分析**
- 6+ 处调用 `getResticPassword()`
- 每次都回退到配置文件(兼容旧版本)
- **问题**:重复代码多,逻辑分散
**建议**
```kotlin
// 提取为单一职责的密码提供者
object CredentialProvider {
private var _resticPassword: String? = null
private var _backendPass: String? = null
fun getResticPassword(config: BackupConfig): String {
return _resticPassword
?: PasswordManager.getResticPassword()
?: config.resticPassword.takeIf { it.isNotEmpty() }
?: ""
}
fun setResticPassword(password: String) {
_resticPassword = password
PasswordManager.setResticPassword(password)
}
}
```
#### 2. **流式备份优化**
```
ResticStreamBackup.backup() ⭐⭐
├─ 临时目录创建 ⚠️ [竞态风险]
├─ APK 复制
├─ 数据压缩 (tar + zstd)
└─ Restic 上传
```
**分析**
- 临时目录 `stream_data` 使用固定名称,有竞态风险
- 单个应用超过 500MB 会跳过(可能不合理)
- 缺少进度计算
**建议**
```kotlin
// 1. 使用唯一目录名
val workDir = File(cacheDir, "stream_data_${UUID.randomUUID()}")
// 2. 动态调整大小限制
val maxAppSize = when {
cacheDir.freeSpace > 10L * 1024 * 1024 * 1024 -> 2L * 1024 * 1024 * 1024 // 2GB
else -> 500L * 1024 * 1024 // 500MB
}
// 3. 计算总体进度
val totalSize = apps.sumOf { estimateAppSize(it) }
var processedSize = 0L
for (app in apps) {
val appSize = backupApp(app)
processedSize += appSize
val percent = processedSize * 100.0 / totalSize
emit("进度: ${"%.1f".format(percent)}% - ${app.packageName}")
}
```
#### 3. **远程后端稳定性**
```
RestBridgeRunner.withBridge() ⭐⭐
├─ 启动 REST 桥 (NanoHTTPD)
├─ 处理 SMB/WebDAV 请求
└─ 资源清理
```
**分析**
- 桥接器启动失败会静默回退到本地
- 没有连接超时处理
- 缺少重试机制
**建议**
```kotlin
// 增加连接健康检查
suspend fun <T> withBridge(
// ... 参数 ...
block: suspend (String, String) -> T
): T {
// 1. 启动桥接器
bridge.start(0)
val port = bridge.listeningPort
// 2. 健康检查
val healthCheck = withTimeoutOrNull(5000) {
checkBridgeHealth(port)
}
if (healthCheck == null) {
bridge.stop()
throw IllegalStateException("REST 桥健康检查超时")
}
// 3. 执行操作(带重试)
return try {
retry(2) { block(bridgeUrl, authToken) }
} finally {
bridge.stop()
}
}
```
---
### 🟢 P2 - 优化项(提升性能)
#### 1. **RootShell 调用优化**
```
当前: ~118 次 RootShell.exec
├─ 文件检查: test -d, stat
├─ 权限操作: chmod, chown
├─ 信息查询: dumpsys, pm list
└─ 数据操作: cp, tar, rm
```
**性能影响**
- 每次 exec 有进程创建开销(~5-10ms
- 备份 100 个应用 = 500-1000ms 额外开销
**优化策略**
```kotlin
// 1. 批量操作 - 文件检查
suspend fun checkMultipleDirs(dirs: List<String>): Map<String, Boolean> {
val script = dirs.joinToString("\n") { dir ->
"""
if test -d '${dir.shellEscape()}'; then
echo "EXISTS:$dir"
else
echo "NOT_EXISTS:$dir"
fi
""".trimIndent()
}
val result = RootShell.exec(script)
return result.output.lines()
.filter { it.contains(":") }
.associate {
val (status, dir) = it.split(":", limit = 2)
dir to (status == "EXISTS")
}
}
// 2. 批量操作 - 版本查询
suspend fun getMultipleVersions(pkgs: List<String>): Map<String, String> {
val script = pkgs.joinToString("\n") { pkg ->
"""
ver=\$(dumpsys package '${pkg.shellEscape()}' | grep versionCode | head -1 | sed 's/.*versionCode=//' | sed 's/ .*//')
echo "$pkg:\$ver"
""".trimIndent()
}
val result = RootShell.exec(script)
return result.output.lines()
.filter { it.contains(":") }
.associate {
val (pkg, ver) = it.split(":", limit = 2)
pkg to ver
}
}
// 3. 缓存应用信息
object AppInfoCache {
private val versionCache = ConcurrentHashMap<String, String>()
private val sizeCache = ConcurrentHashMap<String, Long>()
suspend fun getVersion(pkg: String): String {
return versionCache.getOrPut(pkg) {
RootShell.exec("dumpsys package '$pkg' | grep versionCode | head -1")
.output.substringAfter("versionCode=").substringBefore(" ").trim()
}
}
}
```
**预期效果**
- 减少 30-50% 的 RootShell 调用
- 备份速度提升 20-30%
#### 2. **并发控制优化**
```
当前: Semaphore(3) 固定并发
├─ 可能太低CPU 密集型设备)
└─ 可能太高IO 密集型场景)
```
**建议**
```kotlin
// 动态并发数
val maxConcurrency = when {
Runtime.getRuntime().availableProcessors() >= 8 -> 4
Runtime.getRuntime().availableProcessors() >= 4 -> 3
else -> 2
}
val semaphore = Semaphore(maxConcurrency)
// 或基于 IO 类型
val semaphore = when {
isSSDStorage() -> Semaphore(4) // SSD 可以更高并发
else -> Semaphore(2) // eMMC 降低并发
}
```
#### 3. **进度计算优化**
```
当前: 简单计数 [current/total]
问题: 不反映实际数据量
```
**建议**
```kotlin
data class BackupProgress(
val currentApp: String,
val currentIndex: Int,
val totalApps: Int,
val processedBytes: Long,
val totalBytes: Long,
val stage: String, // "APK", "数据", "OBB", "上传"
val startTime: Long,
) {
val overallPercent: Double
get() = processedBytes * 100.0 / totalBytes.coerceAtLeast(1)
val estimatedTimeRemaining: Long
get() {
val elapsed = System.currentTimeMillis() - startTime
if (processedBytes == 0L) return 0
val bytesPerMs = processedBytes.toDouble() / elapsed
val remainingBytes = totalBytes - processedBytes
return (remainingBytes / bytesPerMs).toLong()
}
}
```
---
## 二、软件场景化调用分析
### 场景 1首次完整备份100 个应用)
```
用户操作: 选择 100 个应用 → 点击"开始备份"
预期时间: 5-15 分钟
调用分析:
├─ AppScanner.scanThirdParty() [1-2秒]
│ └─ RootShell.exec: pm list packages (1次)
├─ BackupOperation.backupApps() [5-15分钟]
│ ├─ [并发=3] × 100 次循环
│ │ ├─ backupUserData() [平均 3-5秒/应用]
│ │ │ └─ RootShell.exec: 5-8 次 (test, tar, verify)
│ │ │
│ │ ├─ backupObb() [平均 0.5秒/应用]
│ │ │ └─ RootShell.exec: 3-5 次
│ │ │
│ │ └─ backupSsaid() + backupPermissions() [平均 0.2秒/应用]
│ │ └─ RootShell.exec: 2-3 次
│ │
│ └─ 总计: ~600-800 次 RootShell.exec
└─ [可选] Restic 上传 [3-10分钟]
└─ ResticCommandRunner.runResticStreaming() (1次进程)
└─ 上传速率: 10-50 MB/s (取决于网络)
```
**瓶颈识别**
1. **IO 密集**: tar 压缩 + 文件复制
2. **并发限制**: Semaphore(3) 可能太低
3. **进程开销**: 600+ 次 RootShell.exec
**优化建议**
```kotlin
// 1. 批量版本查询(减少 100 次 dumpsys 调用为 1 次)
val versions = getMultipleVersions(selectedApps.map { it.packageName.value })
// 2. 调整并发数(根据设备性能)
val concurrency = if (isHighEndDevice()) 5 else 3
val semaphore = Semaphore(concurrency)
// 3. 预分配空间(减少碎片)
preAllocateBackupSpace(outputDir, estimatedTotalSize)
```
### 场景 2增量备份只有 10 个应用更新)
```
用户操作: 10 个应用有更新 → 增量备份
预期时间: 1-3 分钟
调用分析:
├─ 增量检查 [10-20秒]
│ ├─ 读取旧 app_details.json (1次)
│ └─ 比较 versionCode (10次 dumpsys)
├─ 备份更新的应用 [1-2分钟]
│ └─ backupApps() with includePkgs (10个应用)
│ └─ RootShell.exec: ~60-80 次
└─ Restic 增量上传 [30秒-1分钟]
└─ 仅上传变化的数据块 (增量去重)
```
**优化点**
- ✅ 增量检查逻辑已经很好
- 🔧 可以缓存 versionCode减少 dumpsys 调用
- 🔧 Restic 增量去重是核心优势,保持现状
### 场景 3远程备份到 SMB 服务器
```
用户操作: 备份到 Windows 共享
预期时间: 10-30 分钟(取决于数据量和网络)
调用分析:
├─ 本地备份 [5-15分钟]
│ └─ 同场景 1
├─ REST 桥启动 [1-2秒]
│ ├─ RestBridgeRunner.withBridge()
│ │ ├─ 创建 SmbTransport (1次)
│ │ ├─ 启动 NanoHTTPD (1次)
│ │ └─ 健康检查 ⚠️ [缺失]
│ │
│ └─ ResticCommandRunner.runResticStreaming() (1次)
│ └─ 通过 REST API 上传数据
└─ 传输 [5-15分钟]
├─ WebdavTransport / SmbTransport
│ └─ 分块上传 (8KB 块)
└─ 网络错误处理 ⚠️ [缺少重试]
```
**优化建议**
```kotlin
// 1. 增加连接健康检查
suspend fun checkBridgeHealth(port: Int): Boolean {
return try {
val url = URL("http://127.0.0.1:$port/")
val conn = url.openConnection() as HttpURLConnection
conn.connectTimeout = 2000
conn.requestMethod = "GET"
conn.responseCode == 200
} catch (e: Exception) {
false
}
}
// 2. 增加网络重试
suspend fun uploadWithRetry(data: ByteArray, maxRetries: Int = 3) {
repeat(maxRetries) { attempt ->
try {
transport.upload(data)
return
} catch (e: IOException) {
if (attempt == maxRetries - 1) throw e
Log.w(TAG, "上传失败,重试 ${attempt + 1}/$maxRetries", e)
delay(1000L * (attempt + 1))
}
}
}
```
### 场景 4恢复应用20 个应用)
```
用户操作: 选择 20 个应用 → 恢复
预期时间: 3-10 分钟
调用分析:
├─ [可选] Restic 下载 [1-5分钟]
│ └─ ResticRestore.restore() (1次)
│ └─ 下载并解压到本地
├─ RestoreOperation.restoreApps() [2-5分钟]
│ └─ [并发=2] × 20 次循环
│ ├─ installApk() [平均 5-15秒/应用]
│ │ └─ RootShell.exec: pm install (1-3次)
│ │
│ ├─ restoreUserData() [平均 2-5秒/应用]
│ │ └─ RootShell.exec: tar + chown (3-5次)
│ │
│ └─ restoreSsaid() + restorePermissions() [平均 0.5秒/应用]
│ └─ RootShell.exec: 2-3 次
└─ 总计: ~100-150 次 RootShell.exec
```
**优化建议**
```kotlin
// 1. 并行安装和数据恢复
coroutineScope {
// 阶段 1: 并行安装所有 APK
val installJobs = apps.map { app ->
async { installApk(app) }
}
installJobs.awaitAll()
// 阶段 2: 并行恢复数据
val restoreJobs = apps.map { app ->
async { restoreUserData(app) }
}
restoreJobs.awaitAll()
// 阶段 3: 串行恢复 SSAID需要已安装的应用
for (app in apps) {
restoreSsaid(app)
}
}
```
---
## 三、调用链与软件价值对应
### 核心价值链
```
软件价值 调用链 优先级
──────────────────────────────────────────────────────
数据完整性 → backupUserData() + verifyArchive() P0
增量备份 → ResticBackup + BackendExecutor P0
远程存储 → RestBridgeRunner + RemoteTransport P0
恢复可靠性 → RestoreOperation + rollback() P0
用户体验 → ProgressReport + ErrorHandling P1
性能优化 → BatchRootShell + ParallelBackup P2
```
### 118 次 RootShell.exec 的价值分布
```
高价值 (必须保留): 45 次 (38%)
├─ 数据备份: tar, cp, zstd (30次)
├─ 数据恢复: tar, pm install (10次)
└─ 完整性验证: zstd -t, tar -tf (5次)
中等价值 (可优化): 53 次 (45%)
├─ 状态检查: test -d, stat (25次)
│ └─ 可优化: 批量检查或缓存
├─ 信息查询: dumpsys, pm list (18次)
│ └─ 可优化: 批量查询
└─ 权限操作: chmod, chown (10次)
└─ 可优化: 合并为单次调用
低价值 (应该优化): 20 次 (17%)
├─ 冗余检查: 重复的文件存在性检查 (10次)
├─ 日志操作: echo, cat 输出 (5次)
└─ 其他: mkdir -p (5次)
```
---
## 四、实际优化方案
### 方案 1批量 RootShell 调用收益20-30% 速度提升)
```kotlin
// Before: 3 次独立调用
val exists = RootShell.exec("test -d '$dir1'").isSuccess
val size = RootShell.exec("stat -c%s '$file1'").output.trim().toLong()
val version = RootShell.exec("dumpsys package '$pkg' | grep versionCode")
// After: 1 次批量调用
val batchResult = RootShell.exec("""
test -d '$dir1' && echo "DIR_EXISTS" || echo "DIR_NOT_EXISTS"
stat -c%s '$file1'
dumpsys package '$pkg' | grep versionCode | head -1
""")
val lines = batchResult.output.lines()
val exists = lines[0] == "DIR_EXISTS"
val size = lines[1].toLong()
val version = lines[2]
```
**收益**
- 减少进程创建开销: 100 应用 × 3 次 × 5ms = 1.5秒
- 减少上下文切换: 300 次 → 100 次
### 方案 2应用信息缓存收益15-25% 速度提升)
```kotlin
object AppMetadataCache {
private val cache = ConcurrentHashMap<String, CachedMetadata>()
data class CachedMetadata(
val versionCode: String,
val apkPaths: List<String>,
val hasObb: Boolean,
val lastUpdated: Long,
)
suspend fun get(pkg: String): CachedMetadata {
return cache.getOrPut(pkg) {
// 批量查询
val result = RootShell.exec("""
dumpsys package '$pkg' | grep versionCode | head -1
pm path '$pkg'
ls /storage/emulated/0/Android/obb/$pkg/ 2>/dev/null | head -1
""")
parseMetadata(pkg, result.output)
}
}
fun invalidate(pkg: String) {
cache.remove(pkg)
}
}
```
**收益**
- 增量备份时: 10 应用 × 3 次查询 = 30 次 → 10 次
- 重复备份时: 完全命中缓存
### 方案 3智能并发控制收益10-20% 速度提升)
```kotlin
suspend fun backupApps(
// ... 参数 ...
concurrency: Int = calculateOptimalConcurrency(),
) {
val semaphore = Semaphore(concurrency)
coroutineScope {
apps.map { app ->
async {
semaphore.withPermit {
backupSingleApp(app)
}
}
}.awaitAll()
}
}
fun calculateOptimalConcurrency(): Int {
val cpuCores = Runtime.getRuntime().availableProcessors()
val freeMemory = Runtime.getRuntime().freeMemory()
val totalMemory = Runtime.getRuntime().totalMemory()
val memoryUsage = 1.0 - (freeMemory.toDouble() / totalMemory)
return when {
cpuCores >= 8 && memoryUsage < 0.7 -> 5
cpuCores >= 4 && memoryUsage < 0.8 -> 4
cpuCores >= 2 -> 3
else -> 2
}
}
```
### 方案 4增量备份优化收益50-80% 时间节省)
```kotlin
suspend fun backupAppsIncremental(
apps: List<AppInfo>,
previousBackupDir: File,
) {
// 1. 读取旧元数据
val oldMeta = readOldMetadata(previousBackupDir)
// 2. 批量查询当前版本
val currentVersions = getMultipleVersions(apps.map { it.packageName.value })
// 3. 筛选需要备份的应用
val changedApps = apps.filter { app ->
val pkg = app.packageName.value
val oldVersion = oldMeta[pkg]?.versionCode
val newVersion = currentVersions[pkg]
oldVersion != newVersion
}
Log.i(TAG, "增量备份: ${changedApps.size}/${apps.size} 个应用有更新")
// 4. 只备份变化的应用
if (changedApps.isNotEmpty()) {
backupApps(changedApps)
}
}
```
**收益**
- 100 个应用中只有 10 个更新: 节省 90% 时间
- 配合 Restic 增量去重: 网络传输减少 80-95%
---
## 五、用户体验优化
### 1. **进度显示优化**
```kotlin
// Before
"[50/100] com.example.app: 正在备份数据…"
// After
"备份进度: 50% (50/100 应用)
当前: com.example.app
阶段: 数据备份
速度: 15 MB/s
预计剩余: 3 20
已备份: 1.2 GB / 2.4 GB"
```
### 2. **错误处理优化**
```kotlin
// Before
"备份异常: Permission denied"
// After
"备份失败: 权限不足
问题: 无法访问 /data/data/com.example.app
原因: SELinux 策略阻止
建议:
1. 检查 Magisk 是否正确安装
2. 尝试在 Magisk 中禁用 SELinux
3. 重启设备后重试"
```
### 3. **恢复预览优化**
```kotlin
// 显示恢复预览
suspend fun previewRestore(snapshotId: String): RestorePreview {
val snapshot = listSnapshots().first { it.id == snapshotId }
val appDetails = getAppDetails(snapshotId)
return RestorePreview(
snapshotTime = snapshot.time,
totalApps = appDetails.size,
totalSize = appDetails.values.sumOf { it.size },
apps = appDetails.map { (pkg, info) ->
AppPreview(
packageName = pkg,
label = info.label,
version = info.version,
size = info.size,
willOverwrite = isAppInstalled(pkg),
)
}
)
}
```
---
## 六、总结
### 调用优化优先级
| 优先级 | 优化项 | 收益 | 难度 |
|--------|--------|------|------|
| P0 | 增量备份准确性 | 50-80% 时间节省 | 低 |
| P1 | 批量 RootShell 调用 | 20-30% 速度提升 | 中 |
| P1 | 应用信息缓存 | 15-25% 速度提升 | 低 |
| P2 | 智能并发控制 | 10-20% 速度提升 | 中 |
| P2 | 网络重试机制 | 可靠性提升 | 中 |
### 核心原则
1. **数据完整性第一**:宁可多次调用保证可靠性,不能过度优化
2. **增量优先**:利用 Restic 增量去重是核心竞争力
3. **用户体验**:详细进度、友好错误提示、智能建议
4. **性能优化**:批量调用、缓存、智能并发
### 预期效果
实施以上优化后:
- 首次完整备份: 15分钟 → 10分钟 (33% 提升)
- 增量备份: 3分钟 → 30秒 (83% 提升)
- 恢复操作: 10分钟 → 6分钟 (40% 提升)
- 远程备份: 30分钟 → 20分钟 (33% 提升)