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

22 KiB
Raw Permalink Blame History

结合软件实际用途的函数调用深度分析

软件定位理解

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 权限访问系统目录
  • 不能过度优化:宁可多次调用保证可靠性

建议

// 保持现有逻辑,但增加详细日志
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 桥稳定性直接影响远程备份成功率

建议

// 增加重试机制
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
  • 必须保证原子性(失败时回滚)

建议

// 增加恢复前验证
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()
  • 每次都回退到配置文件(兼容旧版本)
  • 问题:重复代码多,逻辑分散

建议

// 提取为单一职责的密码提供者
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 会跳过(可能不合理)
  • 缺少进度计算

建议

// 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 请求
└─ 资源清理

分析

  • 桥接器启动失败会静默回退到本地
  • 没有连接超时处理
  • 缺少重试机制

建议

// 增加连接健康检查
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 额外开销

优化策略

// 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 密集型场景)

建议

// 动态并发数
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]
问题: 不反映实际数据量

建议

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

优化建议

// 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 块)
    │
    └─ 网络错误处理 ⚠️ [缺少重试]

优化建议

// 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

优化建议

// 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% 速度提升)

// 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% 速度提升)

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% 速度提升)

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% 时间节省)

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. 进度显示优化

// 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. 错误处理优化

// Before
"备份异常: Permission denied"

// After
"备份失败: 权限不足
问题: 无法访问 /data/data/com.example.app
原因: SELinux 策略阻止
建议:
1. 检查 Magisk 是否正确安装
2. 尝试在 Magisk 中禁用 SELinux
3. 重启设备后重试"

3. 恢复预览优化

// 显示恢复预览
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% 提升)