一、迁移到苹果芯片后遇到的核心问题

不少做 macOS 开发的朋友应该都有过这种经历:之前在 Intel 芯片的 Mac 上跑的好好的软件,换了 M1、M2 这类苹果自研芯片的 Mac 后,突然就出问题了,最常见的就是软件里的某个功能完全失效,查日志才发现是「旧版内核扩展(kext)加载失败」。

为什么会这样?其实苹果从 2020 年推出 M1 芯片开始,就逐步淘汰了传统的 kext 机制。kext 是之前 macOS 里用来让软件获得系统底层权限的工具,比如杀毒软件要监控文件读写、虚拟网卡软件要修改网络规则,都得靠 kext 插在系统内核里才能实现。但苹果芯片的系统架构和 Intel 完全不一样,而且苹果本身也觉得 kext 太危险——一旦 kext 出问题,整个系统都会崩溃,所以就推出了「系统扩展(System Extension)」来替代 kext。

很多老软件没适配这个变化,直接把旧的 kext 拿到苹果芯片的 Mac 上跑,结果要么系统不让加载,要么加载后功能失效,甚至拖慢系统。所以现在要让老软件在苹果芯片上正常工作,就得把旧的 kext 替换成系统扩展,还要做严格的兼容性测试。

二、从 kext 到系统扩展的迁移策略

2.1 先搞懂两者的核心区别

在动手改之前,得先弄明白 kext 和系统扩展到底不一样在哪,不然改了也是白改。简单说,kext 是「插在内核里的程序」,权限特别大,但对系统的风险也高;系统扩展是「跑在用户空间的程序」,权限被严格限制了,但更安全,也更符合苹果芯片的系统设计。

举个例子,之前的 kext 可以直接修改系统的内存分配规则,系统扩展就不行,只能通过苹果开放的几个专门的 API 来做特定的事,比如监控文件、修改网络规则、控制设备等。

2.2 迁移的具体步骤

2.2.1 第一步:评估旧 kext 的功能

首先得搞清楚旧的 kext 到底是做什么的,有没有必要迁移,或者能不能用更简单的方式替代。比如如果旧 kext 只是用来显示软件的版本号,那完全没必要,直接把这个功能移到普通的应用程序里就行;如果是用来监控系统的网络流量,那才需要迁移成系统扩展。

举个实际的评估例子:假设我们有一个老的杀毒软件,它的旧 kext 有两个功能:一是监控所有文件的读写操作,二是给每个文件生成数字签名。评估后发现,第二个功能不需要系统底层权限,直接在应用里就能实现,只有第一个功能需要迁移成系统扩展。

2.2.2 第二步:选择对应的系统扩展类型

苹果的系统扩展不是单一的,而是分了好几种类型,每种对应一种特定的功能,不能随便用。主要有三种常用的类型:

  1. 文件系统扩展(File System Extension):用来监控文件的读写、修改、删除等操作,适合杀毒软件、备份软件这类需要监控文件的工具。
  2. 网络扩展(Network Extension):用来修改网络规则、监控网络流量、实现 VPN 等,适合网络工具、VPN 软件。
  3. 设备扩展(Device Extension):用来管理外接设备,比如打印机、扫描仪、外接存储设备等。

还是拿刚才的杀毒软件例子来说,它的 kext 是用来监控文件的,所以应该选择「文件系统扩展」。

2.2.3 第三步:用系统扩展的 API 重写功能

选好类型后,就得用苹果提供的新 API 来重写原来 kext 的功能了。这里要注意,系统扩展的开发方式和普通的 macOS 应用不一样,需要单独创建一个系统扩展的项目,然后和主应用绑定。

我们来举一个完整的、可运行的文件系统扩展的例子,所有代码都用 Swift(苹果官方推荐的开发 macOS 软件的语言),代码里会加详细的注释。

首先,创建一个新的 macOS 项目,然后在项目里添加一个「System Extension」的 target,类型选「File System Extension」。然后修改系统扩展里的核心文件,代码如下:

// 引入苹果提供的系统扩展相关的框架
import FileProvider
import Foundation

// 定义系统扩展的主类,必须继承自 NSFileProviderSystemExtension
class MyFileSystemExtension: NSFileProviderSystemExtension {
    // 这个方法是系统扩展的入口,当系统扩展启动时会调用
    override func startSystemExtension(completionHandler: @escaping (Error?) -> Void) {
        super.startSystemExtension(completionHandler: completionHandler)
        
        // 在这里初始化我们需要的功能,比如监控文件的配置
        print("系统扩展启动成功")
        
        // 初始化完成后,调用 completionHandler 通知系统启动成功,传 nil 表示没有错误
        completionHandler(nil)
    }
    
    // 这个方法是用来监控文件操作的,当有文件被读写、修改时会调用
    override func observeFileOperations(at url: URL, for operation: FileOperation, completionHandler: @escaping (Error?) -> Void) {
        super.observeFileOperations(at: url, for: operation, completionHandler: completionHandler)
        
        // 打印当前的文件操作和文件路径,方便调试
        print("检测到文件操作:\(operation.rawValue),文件路径:\(url.path)")
        
        // 这里可以添加自己的逻辑,比如判断这个文件是不是病毒,是就拦截
        // 比如如果是病毒文件,就返回一个错误,系统就会阻止这个操作
        if isVirusFile(at: url) {
            completionHandler(NSError(domain: "MyAntiVirus", code: -1, userInfo: [NSLocalizedDescriptionKey: "该文件是病毒,已被拦截"]))
            return
        }
        
        // 不是病毒的话,就允许这个操作,传 nil 表示没有错误
        completionHandler(nil)
    }
    
    // 自定义的方法,用来判断一个文件是不是病毒
    private func isVirusFile(at url: URL) -> Bool {
        // 这里是简化的判断逻辑,实际项目中可以对接病毒库
        // 比如判断文件是不是可执行文件,或者文件内容有没有特定的病毒特征码
        if url.pathExtension == "exe" {
            return true
        }
        return false
    }
}

然后,需要在主应用里添加加载系统扩展的代码,因为系统扩展不能自己启动,必须由主应用来触发加载。主应用里的代码如下:

// 引入系统扩展相关的框架
import SystemExtensions
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("杀毒软件主界面")
                .font(.largeTitle)
            // 点击按钮加载系统扩展
            Button("加载系统扩展") {
                loadSystemExtension()
            }
            .padding()
        }
        .padding()
    }
    
    // 自定义的方法,用来加载系统扩展
    private func loadSystemExtension() {
        // 创建系统扩展的请求,参数是系统扩展的 bundle ID(和之前创建的系统扩展 target 的 bundle ID 一致)
        let request = OSSystemExtensionRequest.activationRequest(
            forExtensionWithIdentifier: "com.mycompany.MyAntiVirus.MyFileSystemExtension",
            queue: .main
        )
        
        // 系统扩展的激活代理,用来处理加载的结果
        let delegate = SystemExtensionDelegate()
        request.delegate = delegate
        
        // 提交加载请求
        OSSystemExtensionManager.shared.submitRequest(request)
    }
}

// 系统扩展的激活代理类,必须实现 OSSystemExtensionRequestDelegate 协议
class SystemExtensionDelegate: NSObject, OSSystemExtensionRequestDelegate {
    // 当系统扩展加载成功时调用
    func request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) {
        print("系统扩展加载成功,结果:\(result.rawValue)")
    }
    
    // 当系统扩展加载失败时调用
    func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
        print("系统扩展加载失败,错误:\(error.localizedDescription)")
    }
}

写完代码后,还需要做一些配置:比如在主应用的 Info.plist 里添加系统扩展的配置,在苹果开发者账号里给系统扩展申请对应的权限,这些步骤苹果的官方文档里都有详细说明,这里就不展开了。

2.2.4 第四步:移除旧的 kext

当新的系统扩展功能完全正常后,就可以把旧的 kext 从软件里移除了。要注意的是,移除 kext 后,需要通知用户重新启动 Mac,因为旧的 kext 可能还留在系统的内核里,重启后才会完全清除。

三、兼容性测试的方法和要点

迁移完成后,不能直接把软件发布出去,必须做严格的兼容性测试,确保在不同的苹果芯片 Mac 上都能正常工作。

3.1 测试的环境准备

首先要准备不同的测试环境,因为不同的苹果芯片(M1、M2、M3)、不同的 macOS 版本(Ventura、Sonoma、Sequoia)对系统扩展的支持可能不一样。所以至少要准备以下几种测试环境:

  1. M1 芯片的 Mac,运行 macOS Ventura(13.x)
  2. M2 芯片的 Mac,运行 macOS Sonoma(14.x)
  3. M3 芯片的 Mac,运行 macOS Sequoia(15.x)

如果没有这么多实体 Mac,也可以用苹果的 Xcode 模拟器来测试,模拟器可以模拟不同的苹果芯片和 macOS 版本。

3.2 核心测试用例

测试的时候,不能只测系统扩展能不能加载,还要测原来 kext 的所有功能是不是都正常,还要测有没有影响系统的稳定性。主要的测试用例包括:

3.2.1 系统扩展的加载和卸载测试

测试系统扩展能不能正常加载、能不能正常卸载、加载后能不能正常启动。比如:

  1. 启动主应用,点击「加载系统扩展」按钮,看系统会不会弹出权限提示,用户同意后系统扩展能不能正常启动。
  2. 打开系统设置里的「隐私与安全性」,看系统扩展是不是在已安装的列表里。
  3. 卸载主应用后,看系统扩展会不会自动卸载(如果配置了的话)。

3.2.2 核心功能测试

测试原来 kext 的核心功能是不是都正常。比如刚才的杀毒软件例子,要测试:

  1. 复制一个 exe 文件到 Mac 上,看系统扩展会不会检测到这个文件,会不会弹出拦截提示。
  2. 复制一个 txt 文件到 Mac 上,看系统扩展会不会检测到这个文件,会不会允许复制。
  3. 修改一个已经存在的 txt 文件,看系统扩展会不会检测到修改操作。

3.2.3 系统稳定性测试

测试系统扩展会不会影响系统的稳定性,比如:

  1. 运行主应用和系统扩展一段时间,看系统会不会出现卡顿、崩溃、死机的情况。
  2. 同时运行多个其他软件,看系统扩展会不会和其他软件冲突。
  3. 重启 Mac 后,看系统扩展会不会自动启动,功能是不是正常。

3.2.4 权限测试

测试系统扩展的权限是不是符合预期,比如:

  1. 看系统扩展能不能访问到用户的桌面文件夹、下载文件夹等。
  2. 看系统扩展能不能访问到系统的核心文件夹(比如 /System 目录),如果不能访问,是不是符合预期(因为系统扩展本来就不能访问系统核心文件夹)。

3.3 测试工具的使用

测试的时候可以用一些工具来辅助,比如:

  1. Xcode 的调试工具:可以在 Xcode 里直接调试系统扩展,看系统扩展的日志、内存使用情况、CPU 使用情况等。
  2. 苹果的系统日志工具(Console):可以查看系统的日志,看系统扩展有没有报错。
  3. 第三方的性能测试工具:比如 Instruments,可以测试系统扩展的性能,看会不会占用太多的内存或者 CPU。

四、迁移的应用场景、优缺点和注意事项

4.1 应用场景

迁移的应用场景主要是那些原来依赖传统 kext 的 macOS 软件,比如:

  1. 杀毒软件、安全软件:需要监控文件、网络、系统操作。
  2. 网络工具、VPN 软件:需要修改网络规则、监控网络流量。
  3. 备份软件、同步软件:需要监控文件的变化,自动备份或者同步。
  4. 外接设备管理软件:需要管理打印机、扫描仪、外接存储设备等。

4.2 迁移的优缺点

优点

  1. 符合苹果的系统架构:系统扩展是苹果专门为苹果芯片设计的,迁移后软件可以在苹果芯片的 Mac 上正常运行。
  2. 更安全:系统扩展跑在用户空间,权限被严格限制,即使出问题也不会导致整个系统崩溃。
  3. 更容易维护:系统扩展的开发和调试比 kext 简单,因为 kext 是插在内核里的,调试起来非常麻烦,而系统扩展可以像普通应用一样调试。

缺点

  1. 功能受限:系统扩展的权限比 kext 小很多,原来 kext 能做的一些事,系统扩展可能做不了,比如直接修改系统的内存分配规则、访问系统的核心内存等。
  2. 开发成本高:迁移需要重新开发原来的 kext 功能,还需要做大量的兼容性测试,会花费很多时间和精力。
  3. 对用户的要求高:系统扩展需要用户手动授权,很多用户可能不知道怎么授权,导致软件功能无法正常使用。

4.3 注意事项

  1. 提前规划:迁移之前要先评估旧 kext 的功能,有没有必要迁移,或者能不能用更简单的方式替代,避免做无用功。
  2. 严格按照苹果的规范开发:系统扩展的开发有很多规范,比如权限的申请、代码的签名、配置的设置等,必须严格按照苹果的规范来,不然系统扩展可能无法正常加载。
  3. 做好兼容性测试:一定要在不同的苹果芯片和 macOS 版本上做测试,确保软件的兼容性。
  4. 给用户清晰的提示:系统扩展需要用户授权,要给用户清晰的提示,告诉用户为什么需要授权,怎么授权,避免用户因为不知道怎么操作而放弃使用软件。

五、文章总结

把旧的 kext 迁移成系统扩展,是 macOS 软件适配苹果芯片的必经之路。整个过程需要先评估旧 kext 的功能,选择合适的系统扩展类型,然后用新的 API 重写功能,最后做严格的兼容性测试。虽然迁移的过程比较麻烦,会花费很多时间和精力,但迁移后软件可以在苹果芯片的 Mac 上正常运行,还能提高软件的安全性和可维护性。

对于 macOS 开发者来说,掌握从 kext 到系统扩展的迁移方法,是必备的技能。只要按照正确的步骤来,做好充分的测试,就能顺利完成迁移,让软件适配苹果芯片的 Mac。