喝完几杯新鲜的 ☕,又堆积了一大批提交后,我们带着一个功能丰富的版本回来了。亮点包括更聪明的编译器、更稳固的 Darwin 内部实现,以及大规模的 Swift 重构——绑定现在优先使用 async/await,不再依赖委托,并且大体上与平台无关。

亮点

  • compiler:向 CompilerOptions 添加 platform 和 externals 选项,并将它们一路传递到 Go 后端。这让 frida-compile(以及 Frida.Compiler)能针对目标平台定制输出,并将选定的模块视为外部依赖——例如为 GumJS agent 构建插件时,agent 公开的 API 应在运行时链接,而不是打包进去。(感谢 @leonitousconforti)
  • darwin:重写 query_shared_cache_range(),改为解析 dyld 共享缓存头,而不是从 AllImageInfos 中找到的基址开始遍历虚拟内存区域。这消除了猜测,并确保页面发生写时复制后仍能得到正确的区间。(感谢 @hsorbo 参与结对编程)
  • darwin:AllImageInfos 现在会报告 Dyld Shared Cache 的 UUID 和滑动值。(感谢 @hsorbo 参与结对编程)
  • simmy:spawn() 现已正确接入 argv 和 env,因此模拟器的行为更接近真实设备。
  • frida-node:生成的 from_value() 辅助函数现在会包含继承的属性,因此 externals 等选项能正确传递。
  • frida-python:修复 PackageManager 规格选项解析中一处很小但会泄漏的角落问题。

Swift 绑定:现代化的跨平台革新 🍎

Frida Swift 绑定已经进行了大规模重构,使其符合 Swift 习惯、优先并发,并支持跨平台。

  • 全面采用 async/await——大多数 API 现在使用 Swift Concurrency,并通过 GCancellable 支持取消 Task。
  • 移除委托——基于委托的回调已替换为 异步事件流(AsyncStream),使事件处理更符合习惯,也更易于组合。
  • 线程友好——api: Support invocation from any thread 允许从非主线程安全调用。(感谢 @hsorbo 参与结对编程。)
  • 纯 Swift 核心与跨平台支持——核心绑定现已不依赖 Foundation 和 Dispatch,并使用纯 Swift 类型(二进制数据表示为 [UInt8])。目前仍有两个小缺口:Marshal 辅助函数中的 JSON 编解码当前使用 Foundation;以后会添加不依赖 Foundation 的后备实现。
  • SwiftUI 友好——新增 DeviceListModel,它是一个 @MainActor ObservableObject,公开 @Published devices 和 discoveryState,便于顺滑集成 SwiftUI。
  • 图标可移植性——用可移植的 Icon 枚举取代特定于平台的图像处理,并为 CGImage、NSImage、UIImage 和 SwiftUI.Image 提供平台适配器。
  • API 稳定性改进——公开枚举标注了 @frozen,并在必要时将一些复杂引用类型标记为 @unchecked Sendable。

注:本版本不包含 frida-swift 预构建二进制文件;如果你使用 Swift 绑定,应执行 git clone 并从 main 构建,以获得最新变更。

还请注意,Swift 绑定仍然处于实验和演进阶段——虽然新 API 向前迈出了一大步,但在即将推出的版本中 Swift 层稳定之前,它们可能还会继续变化。

快速示例(来自 frida-swift README)

DeviceListModel(界面友好的模型):

import Combine

@MainActor
public final class DeviceListModel: ObservableObject {
    @Published public private(set) var devices: [Device] = []
    @Published public private(set) var discoveryState: DiscoveryState = .discovering

    @frozen
    public enum DiscoveryState: Equatable {
        case discovering
        case ready
    }

    public let manager: DeviceManager

    public init(manager: DeviceManager) { … }
}

现在可以这样使用:

import Frida
import SwiftUI

struct DevicesView: View {
    @StateObject private var model = DeviceListModel(manager: DeviceManager())
    @State private var selectedDevice: Device?
    @State private var session: Session?

    var body: some View {
        NavigationStack {
            List(model.devices, id: \.id) { device in
                Button {
                    Task {
                        selectedDevice = device
                        session = try? await device.attach(to: 12345)
                    }
                } label: {
                    VStack(alignment: .leading) {
                        Text(device.name)
                            .font(.headline)
                        Text(device.kind.rawValue)
                            .font(.subheadline)
                            .foregroundStyle(.secondary)
                    }
                }
            }
            .navigationTitle("Frida Devices")
            .overlay {
                if model.devices.isEmpty {
                    ProgressView("Searching for devices…")
                }
            }
        }
    }
}

完整脚本生命周期示例:

func testFullCycle() async throws {
    let manager = DeviceManager()

    for await devices in await manager.snapshots() {
        guard let local = devices.first(where: { $0.kind == .local }) else {
            continue
        }

        let session = try await local.attach(to: 12345)
        let script = try await session.createScript("""
            console.log("hello");
            send(1337);
        """)

        Task {
            for await event in script.events {
                switch event {
                case .message(let message, _):
                    print("Message:", message)
                case .destroyed:
                    print("Script destroyed")
                }
            }
        }

        try await script.load()
        break
    }
}

祝使用愉快。一如既往,如果遇到任何问题,请告诉我们!