深入了解Apple CallKit框架:集成、功能与实现详解

尽管“CallKit插件”这个称呼可能不是官方最常用的说法,因为它更多是Apple提供的一个集成框架而非独立可安装的“插件”,但它确实是帮助第三方应用(尤其是VoIP和视频通话应用)深度融入iOS/iPadOS原生电话体验的关键。本文将围绕CallKit,解答一系列关于它是什么、为什么使用、在哪里可用、是否有成本、如何工作以及开发者如何集成等核心问题,旨在提供一个详细且具操作性的视角。

CallKit是什么?(澄清“插件”概念)

首先,需要明确的是,CallKit并非一个可以单独下载或安装到其他应用中的“插件”。它是由Apple提供的一个原生框架(Framework),集成在iOS和iPadOS操作系统中。第三方开发者可以在自己的应用中调用这个框架提供的API(应用程序接口),从而让应用的通话功能与系统的电话功能进行深度集成。

你可以将CallKit理解为:

  • 系统提供的一套标准接口:允许你的应用“告诉”系统有来电或去电,或者某个通话状态发生了变化。

  • 系统提供的原生UI适配层:当你的应用通过CallKit报告一个通话时,系统可以利用原生电话界面的外观和交互方式来呈现这个通话,而无需应用自己从头绘制界面。

  • 音频和系统事件协调者:CallKit帮助你的应用管理音频会话,并协调处理系统级的事件,比如其他应用的音频播放、Siri的激活、系统电话的介入等。

因此,“使用CallKit框架”更准确地描述了开发者在做的事情,而非安装一个“CallKit插件”。当人们谈论“CallKit插件”时,他们通常指的是“集成了CallKit框架的应用所拥有的功能或实现方式”。

CallKit的核心功能包括:

  • 原生来电/去电界面: 让应用的通话显示在锁屏界面或屏幕顶部,外观与原生电话一致。

  • 呼叫记录集成: 允许应用的通话出现在系统的“最近通话”列表中。

  • 与其他通话应用的互操作: 当有原生电话或其他CallKit集成的VoIP应用来电时,系统能够正确处理音频焦点和中断。

  • 静音、免提、群聊控制: 应用可以通过CallKit向系统报告这些状态变化,并在原生界面上显示对应的控制。

  • 蓝牙和车载系统集成: 更好的支持蓝牙设备和CarPlay/CarLife等车载系统对通话的控制和音频路由。

  • 支持“通话等待”和“保持通话”: 处理多个并发通话的情况。

为什么开发者要使用CallKit?

使用CallKit为第三方通话应用带来了巨大的优势,这不仅仅是界面的美观统一,更关乎用户体验、系统集成度和稳定性:

  • 提升用户体验:

    • 熟悉的界面: 用户无需学习新的接听/拨打电话界面,降低了使用门槛。
    • 锁屏接听: 即使手机锁屏,来电也能像原生电话一样全屏显示并可直接接听,极大提高了来电的可见性和便捷性。
    • 不打断使用: 当用户在使用其他应用时,新来电会以顶部横幅的形式出现,用户可以选择接听或忽略,而不会强制打断当前操作(除非用户设置了全屏来电)。
    • 系统音频协调: 确保通话音频能够正确地与其他应用音频、系统声音协调工作。
  • 增强系统集成度:

    • 集成到最近通话: 用户的通话历史记录统一管理,方便回拨。
    • 支持勿扰模式: 尊重用户的勿扰设置。
    • 联系人匹配: 如果来电号码在用户的通讯录中,系统会自动显示联系人姓名和照片。
    • 更好的蓝牙/车载支持: 语音路由更可靠,控制更便捷。
  • 提升应用可靠性:

    • 处理系统中断: CallKit帮助应用优雅地处理系统电话、Siri、闹钟等事件导致的音频中断。
    • 后台执行支持: 即使应用在后台,也能通过CallKit接收和处理重要的通话事件。

总而言之,使用CallKit能够让第三方通话应用获得几乎与原生电话应用相同的系统级待遇,这对于提升用户满意度、确保通话功能的稳定可靠至关重要。

CallKit在哪里可用?

CallKit框架是Apple iOS和iPadOS操作系统的组成部分。

  • 支持的平台: CallKit框架从iOS 10和iPadOS 10开始引入,并在后续版本中不断完善。因此,任何运行iOS 10或更新版本的iPhone和iPad设备都支持CallKit。

  • 在应用中的位置: CallKit的代码实现是开发者在构建自己的应用时,集成在应用的代码内部的。它不是一个独立的应用或服务。

  • 系统中的体现: 当一个应用集成了CallKit并通过它报告通话时,CallKit的功能体现在系统的原生电话UI、锁屏界面、顶部横幅、最近通话列表、通讯录等地方。

开发者只需要确保他们的开发环境(Xcode)是最新的,并且应用的目标部署版本设定为iOS 10或更高,就可以在代码中导入并使用CallKit框架了。

使用CallKit需要多少成本?

CallKit框架本身是Apple操作系统的一部分,使用它**没有额外的框架许可费用或使用费**。

但是,开发和发布集成了CallKit的应用会涉及开发者通常需要承担的成本:

  • Apple开发者计划费用: 每年需要支付Apple开发者计划的费用(目前是每年99美元),这才能在设备上测试应用、提交应用到App Store以及使用Apple提供的各种开发工具和服务(包括访问完整的SDK,其中就包含CallKit)。

  • 开发成本: 这是主要的成本,包括开发者的人力资源投入,用于学习CallKit API、设计和编写代码、进行测试和调试。CallKit的集成涉及音频会话管理、后台模式配置以及处理各种系统事件,需要专业的iOS开发知识。

  • 后端服务成本: 虽然CallKit管理的是前端的UI和系统集成,但实际的语音/视频通话功能需要自己的后端服务(如信令服务器、媒体服务器)。这些后端服务的搭建、运营和维护会产生相应的成本。

所以,CallKit本身是“免费”的,但将其集成到功能完整的通话应用中,是整个应用开发和运营成本的一部分。

CallKit是如何工作的?(技术概述)

CallKit的工作原理主要围绕着应用与操作系统之间的信息交换。它扮演着一个中间人的角色,协调应用的通话状态与系统UI和行为的同步。

核心组件和流程:

  1. CXProvider: 这是你的应用用来“告诉”CallKit系统有关通话信息的对象。当你的应用收到一个新来电或发起一个去电时,你需要通过`CXProvider`向系统报告这个事件,并提供必要的通话信息(如对方号码/姓名、是否是视频通话等)。`CXProvider`有一个关联的Delegate (`CXProviderDelegate`),CallKit系统会通过这个Delegate方法向你的应用发送指令,例如“用户想接听这个来电”、“用户想结束这个通话”、“用户按下了静音按钮”等。

  2. CXCallController: 这是你的应用用来“请求”CallKit系统执行某个动作的对象。例如,当用户在你的应用界面上点击“拨打”按钮时,你的应用会使用`CXCallController`向系统请求发起一个外呼操作。当系统批准了这个请求后,会通过`CXProviderDelegate`方法通知你的应用开始实际的拨号过程。它也可以用来请求挂断电话、保持/恢复通话等。

  3. CXAction: 代表一个由系统发起(通过`CXProviderDelegate`)或由应用请求(通过`CXCallController`)的通话操作,例如`CXStartCallAction`(发起通话)、`CXAnswerCallAction`(接听通话)、`CXEndCallAction`(结束通话)、`CXSetMutedCallAction`(设置静音)等。你的应用在Delegate方法中收到这些Action后,需要执行相应的操作(如建立网络连接、播放提示音等),并在操作完成后调用Action的`fulfill()`方法通知系统操作已成功完成。

  4. 音频会话管理 (AVAudioSession): CallKit与AVAudioSession紧密集成。当一个CallKit通话开始时,CallKit会帮助应用激活音频会话,并配置正确的音频分类(如`.playAndRecord`)和模式(如`.voiceChat`),确保通话音频的正常输入输出。当通话结束时,CallKit也会协助应用停用或调整音频会话。正确处理音频会话是CallKit集成的关键部分。

整个流程可以概括为:
应用 <--> (使用 CXProvider/CXCallController) <--> CallKit Framework <--> (与系统电话UI、音频服务等交互) <--> iOS/iPadOS System

例如:当你的应用收到一个新来电的Push Notification -> 你的应用调用`CXProvider`的`reportNewIncomingCall`方法向CallKit报告 -> CallKit在锁屏或顶部显示来电界面 -> 用户在系统界面上点击“接听”-> CallKit调用你的`CXProviderDelegate`的`provider:performAnswerCallAction:`方法 -> 你的应用在这个方法中建立网络连接、启动音频 -> 连接成功后,调用`action.fulfill()` -> CallKit更新系统UI显示通话中。

开发者如何集成CallKit?(详细步骤和代码思路)

集成CallKit是一个涉及多个步骤的过程,需要开发者熟悉Swift或Objective-C以及iOS开发环境。以下是主要的集成思路和关键点:

步骤1:配置项目能力(Capabilities)

  • 在你的Xcode项目设置中,选择你的Target。
  • 进入”Signing & Capabilities”标签页。
  • 点击左上角的”+ Capability”按钮。
  • 搜索并添加”Push Notifications”(用于接收来电通知)和”Background Modes”。
  • 在”Background Modes”中,勾选”Voice over IP (VoIP)”。这是让你的应用在后台也能处理CallKit事件的关键。
  • 如果你的应用需要加密功能(大多数通话应用都需要),可能还需要添加其他与数据保护相关的能力。

步骤2:导入CallKit和AVFoundation框架

在你的需要使用CallKit相关代码的文件顶部,导入必要的框架:


import CallKit
import AVFoundation

或者Objective-C:


#import <CallKit/CallKit.h>
#import <AVFoundation/AVFoundation.h>

步骤3:设置CXProvider

  • 创建一个`CXProvider`的实例。通常在应用启动时或者负责通话管理的单例对象中创建。
  • 为`CXProvider`配置一个`CXProviderConfiguration`。这个配置决定了你的应用在系统UI上的显示方式,比如:
    • `supportsVideo`: 是否支持视频通话。
    • `maximumCallsPerCallGroup`: 群聊最大人数。
    • `supportedHandleTypes`: 支持的联系人类型(如`phoneNumber`, `emailAddress`)。
    • `ringtoneSound`: 自定义来电铃声文件名(可选)。
    • `iconTemplateImageData`: 应用图标(可选,用于群聊等)。
    • `localizedName`: 在系统界面上显示的应用名称。
  • 将你的通话管理对象设置为`CXProvider`的delegate,并实现`CXProviderDelegate`协议中的方法。

// Swift 示例
class CallManager: NSObject {
    private let provider: CXProvider
    private let callController = CXCallController() // 也创建CXCallController

    override init() {
        let configuration = CXProviderConfiguration(localizedName: "My Calling App") // 你的应用名称
        configuration.supportsVideo = true
        configuration.maximumCallsPerCallGroup = 5
        configuration.supportedHandleTypes = [.phoneNumber, .emailAddress]
        // ... 其他配置
        provider = CXProvider(configuration: configuration)
        super.init()
        provider.setDelegate(self, queue: nil) // 设置代理,nil表示在主线程队列
    }

    // ... 实现 CXProviderDelegate 方法
}

步骤4:实现CXProviderDelegate方法

这是核心部分,你需要在这里处理系统发来的各种动作指令:

  • providerDidActivate(_ provider: CXProvider): CallKit provider激活时调用,通常在这里激活并配置你的`AVAudioSession`。
  • providerDidDeactivate(_ provider: CXProvider): CallKit provider停用时调用,通常在这里停用你的`AVAudioSession`。
  • provider:performStartCallAction:: 处理由`CXCallController`请求发起的去电或由系统建议的回拨。在这里建立实际的去电连接。连接成功后,调用`action.fulfill()`。如果失败,调用`action.fail(withError:)`。
  • provider:performAnswerCallAction:: 处理用户在系统界面上点击“接听”的操作。在这里建立实际的来电连接并开始音频传输。连接成功后,调用`action.fulfill()`。失败调用`action.fail(withError:)`。
  • provider:performEndCallAction:: 处理用户在系统界面上点击“挂断”的操作。在这里断开通话连接。断开完成后,调用`action.fulfill()`。失败调用`action.fail(withError:)`。
  • provider:performSetMutedCallAction:: 处理用户在系统界面上点击静音按钮的操作。根据`action.isMuted`设置应用的麦克风状态。完成后调用`action.fulfill()`。
  • provider:performSetHeldCallAction:: 处理用户在系统界面上保持/恢复通话的操作。根据`action.isOnHold`保持或恢复通话。完成后调用`action.fulfill()`。
  • 其他Delegate方法处理如群聊、DTMF按键等。

步骤5:使用CXCallController请求动作(发起通话等)

当你的应用内部需要发起一个CallKit相关的动作(如用户点击拨打按钮)时,使用`CXCallController`。

  • 创建一个`CXStartCallAction`实例,指定对方的`CXHandle`(号码或Email)。
  • 创建一个`CXTransaction`,将这个`CXStartCallAction`添加到事务中。
  • 使用`CXCallController`的`request(_ transaction:completion:)`方法向系统请求执行这个事务。
  • 在Completion Handler中检查请求结果。如果成功,系统会通过`CXProviderDelegate`的`provider:performStartCallAction:`方法通知你的应用开始实际的拨号。如果失败(例如用户在忙线),可以在Completion Handler中处理。

// Swift 示例:发起去电
func startOutgoingCall(handle: String, isVideo: Bool) {
    let cxHandle = CXHandle(type: .phoneNumber, value: handle) // 或 .emailAddress
    let startCallAction = CXStartCallAction(callUUID: UUID(), handle: cxHandle)
    startCallAction.isVideo = isVideo

    let transaction = CXTransaction(action: startCallAction)

    callController.request(transaction) { error in
        if let error = error {
            print("Error requesting start call action: \(error.localizedDescription)")
            // 处理请求失败,例如用户拒绝权限等
        } else {
            print("Start call action requested successfully")
            // 请求已发送给系统,实际拨号将在provider:performStartCallAction:中处理
        }
    }
}

步骤6:报告通话状态变化

当你的应用内部的通话状态发生变化时(如连接成功、通话结束、对方挂断、通话静音状态改变),需要通过`CXProvider`向CallKit报告,以便系统更新UI。

  • 报告新来电: 当收到Push通知等方式告知有新来电时,使用`provider.reportNewIncomingCall(with:uuid:update:completion:)`。需要提供一个UUID(用于唯一标识这次通话)、对方信息(通过`CXCallUpdate`)、Handle等。
  • 报告去电已连接: 当发起的去电网络连接成功并可以开始通话时,使用`provider.reportOutgoingCall(with:callUUID:connectedAtDate:)`。
  • 报告通话结束: 当通话以任何原因结束时,使用`provider.reportCall(with:callUUID:endedAtDate:reason:)`,并提供结束原因(如`failed`, `remoteEnded`, `localEnded`, `unanswered`, `declinedWhileWaiting`)。
  • 报告通话更新: 使用`provider.reportCall(with:callUUID:updated:)`报告通话状态更新,例如静音状态改变、是否开启免提、连接状态变化等,通过`CXCallUpdate`对象传递更新信息。

步骤7:管理AVAudioSession

音频会话管理是CallKit集成中容易出错的部分。通常在`providerDidActivate`中配置并激活音频会话,在`providerDidDeactivate`中停用。同时,在处理`performStartCallAction`和`performAnswerCallAction`时,也需要确保在连接建立并准备开始音频传输时,音频会话已正确配置并激活。处理`performEndCallAction`时,需要在通话结束后妥善处理音频会话(例如,如果应用没有其他音频需要播放,可以停用会话)。


// Swift 示例:在providerDidActivate中配置并激活音频会话
extension CallManager: CXProviderDelegate {
    func providerDidActivate(_ provider: CXProvider) {
        print("CallKit provider did activate")
        // 配置和激活音频会话
        do {
            let audioSession = AVAudioSession.sharedInstance()
            try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers])
            try audioSession.setPreferredIOBufferDuration(0.005) // 根据需要调整
            try audioSession.setActive(true)
            print("AVAudioSession activated")
        } catch {
            print("Failed to activate AVAudioSession: \(error.localizedDescription)")
            // 处理错误
        }
    }

    func providerDidDeactivate(_ provider: CXProvider) {
        print("CallKit provider did deactivate")
        // 停用音频会话,或者根据应用需求调整
        do {
             // 停用会话,如果应用没有其他音频需要播放
             try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
             print("AVAudioSession deactivated")
        } catch {
             print("Failed to deactivate AVAudioSession: \(error.localizedDescription)")
             // 处理错误
        }
    }

    // ... 其他 CXProviderDelegate 方法实现
}

步骤8:处理系统中断和音频路由变化

监听`AVAudioSession`的中断通知(`.AVAudioSessionInterruption`)和路由变化通知(`.AVAudioSessionRouteChange`),并根据通知信息暂停/恢复音频处理或调整音频输出设备。CallKit在处理系统来电等中断时会触发Provider的停用,这也会间接帮助处理中断。

步骤9:处理后台执行

确保你的VoIP应用在后台也能维持网络连接并接收到Push通知,以便在有来电时能够唤醒应用并通过CallKit报告来电。这依赖于之前配置的”Voice over IP (VoIP)”后台模式和相应的网络连接实现(如使用PushKit接收高优先级VoIP Push)。

这是一个高层次的集成概览,实际开发中还需要处理错误、复杂的通话状态转换、多个并发通话、与其他应用功能的协调等细节。Apple的官方文档和示例代码是深入学习CallKit的最佳资源。

总结

通过上述对CallKit是什么、为什么重要、在哪里使用、成本、工作原理以及如何实现的详细阐述,可以看出CallKit对于希望在iOS/iPadOS上提供优质通话体验的应用来说是不可或缺的框架。虽然将其称为“插件”可能稍有偏差,但它作为连接第三方通话功能与原生系统体验的桥梁,其作用和价值是巨大的。开发者通过细致地集成CallKit,能够显著提升应用的专业性和用户满意度,使通话功能真正融入到Apple设备的生态中。

callkit插件

By admin

发表回复