【问题标题】:How does Google's custom iOS keyboard, Gboard, programmatically dismiss the frontmost app?Google 的自定义 iOS 键盘 Gboard 如何以编程方式关闭最前面的应用程序?
【发布时间】:2026-02-23 14:35:01
【问题描述】:

Google 的自定义 iOS 应用 Gboard 有一个有趣的功能,使用 iOS SDK 中的公共 API 无法实现(从 iOS 10 开始)。 我想知道 Google 如何以编程方式在 Gboard 的 App Switching 堆栈中弹回一个应用程序。

自定义 iOS 键盘有两个主要组件:容器应用和键盘应用扩展。键盘应用程序扩展在单独的操作系统进程中运行,只要用户在手机上需要文本输入的任何应用程序中,该进程就会启动。

这些是使用 Gboard 可以遵循的大致步骤,以查看以编程方式返回到上一个应用程序的效果:

  1. 用户在其 iPhone 上启动 Apple Messages 应用程序并点击文本字段以开始输入文本。
  2. Gboard 键盘扩展程序启动,用户会看到 Gboard 自定义键盘(当他们仍在 Apple Messages 应用程序中时)。
  3. 用户点击 Gboard 键盘扩展内的麦克风键进行语音转文本输入。
  4. Gboard 使用custom url scheme 启动 Gboard 容器应用。 Gboard 键盘和 Apple 消息应用程序在应用程序堆栈中被下推一层,Gboard 容器应用程序现在是应用程序堆栈中最前面的应用程序。 Gboard 容器应用使用麦克风收听用户的语音并将其转换为放置在屏幕上的文本。
  5. 当用户对他们在屏幕上看到的文本输入感到满意时,点击“完成”按钮。
  6. 这就是奇迹发生的地方……当文本输入屏幕被关闭时,Gboard 容器应用程序也会自动关闭。 Gboard 容器应用程序消失并被 Apple Messages 应用程序取代(有时 Gboard 键盘扩展过程仍然存在,有时会重新启动,有时需要通过在文本字段内点击手动重新启动。)。 Google 如何做到这一点?
  7. 最后,用户会看到刚刚翻译的文本自动插入到文本输入字段中。据推测,Google 在 Gboard 容器应用和键盘扩展之间通过sharing data 实现了这一点。

我假设 Google 通过使用 Objective-C 运行时内省探索状态栏的视图层次结构并以某种方式合成点击事件或调用公开的目标/动作来使用私有 API。我对此进行了一些探索,并且能够在状态栏中找到有趣的 UIView 子类,例如包含UISystemNavigationActions 数组的UIStatusBarBreadcrumbItemView。我将继续探索这些类,希望能找到一些复制用户交互的方法。

我了解使用私有 API 是让您的应用提交被 App Store 拒绝的好方法 - 这不是我希望在答案中解决的问题。我主要在寻找有关 Google 如何以编程方式在 Gboard 的 App Switching 堆栈中弹回一个应用程序这一任务的具体答案。

【问题讨论】:

  • 任何应用程序都会发生这种情况吗?在系统消息应用程序的情况下,这很容易 - 有一个启动它的计划:developer.apple.com/library/content/featuredarticles/…。如果这也适用于其他应用程序(尤其是不太受欢迎的应用程序),那确实很好奇。
  • @Losiowaty 这似乎发生在任何任意应用程序中。
  • 关于第4步,我无法打开容器/主机应用程序,你是怎么做到的?谢谢

标签: ios ios-keyboard-extension


【解决方案1】:

你的猜测是正确的——Gboard 正在使用私有 API 来做这件事。

...虽然不是通过探索视图层次结构或事件注入。

语音转文本操作完成后,我们可以从 Xcode 或控制台检查系统日志,它调用了-[AVAudioSession setActive:withOptions:error:] 方法。所以我对 Gboard 应用程序进行了逆向工程,并寻找与此相关的堆栈跟踪。

爬上调用栈,我们可以找到-[GKBVoiceRecognitionViewController navigateBackToPreviousApp] 方法,然后……

_systemNavigationAction? 是的,绝对是私有 API。

由于class_getInstanceVariable 是公共API 而"_systemNavigationAction" 是字符串文字,因此自动检查器无法记录私有API 的使用情况,人工审阅者可能不会看到“跳转回上一个应用程序”的行为。或者可能是因为他们是 Google 而你不是……


执行“跳回上一个应用”动作的实际代码是这样的:

@import UIKit;
@import ObjectiveC.runtime;

@interface UISystemNavigationAction : NSObject
@property(nonatomic, readonly, nonnull) NSArray<NSNumber*>* destinations;
-(BOOL)sendResponseForDestination:(NSUInteger)destination;
@end

inline BOOL jumpBackToPreviousApp() {
    Ivar sysNavIvar = class_getInstanceVariable(UIApplication.class, "_systemNavigationAction");
    UIApplication* app = UIApplication.sharedApplication;
    UISystemNavigationAction* action = object_getIvar(app, sysNavIvar);
    if (!action) {
        return NO;
    }
    NSUInteger destination = action.destinations.firstObject.unsignedIntegerValue;
    return [action sendResponseForDestination:destination];
}

特别是,-sendResponseForDestination: 方法执行实际的“返回”操作。

(由于 API 未记录在案,Gboard 实际上错误地使用了该 API。他们使用了错误的签名 -(void)sendResponseForDestination:(id)destination。但碰巧1 以外的所有数字都可以正常工作,所以这次谷歌开发者很幸运)

【讨论】:

  • 很好的答案!不过,我怀疑谷歌是否有意隐藏这一点。我猜大公司之间在一定程度上允许使用私有 api。
  • 简直令人印象深刻!感谢您阐明这一点。只是为了确保我做对了:提供的代码是否应该也适用于我们,还是仍然是私有 API?
  • @AhmetAkkök 它仍然是 Apple 私有的。
  • @kennytm ? 我想我会试一试的。如果您能添加解决方案的 Swift 版本,我将不胜感激...
  • Swift 版本将不胜感激。目前,在成功桥接后,构建失败是我从 ViewController 调用“jumpBackToPreviousApp()”并出现错误:“:-1: linker command failed with exit code 1 (use -v to see invocation)”
【解决方案2】:

Swift 版本的@kennytm 答案:

@objc private protocol PrivateSelectors: NSObjectProtocol {
    var destinations: [NSNumber] { get }
    func sendResponseForDestination(_ destination: NSNumber)
}

func jumpBackToPreviousApp() -> Bool {
    guard
        let sysNavIvar = class_getInstanceVariable(UIApplication.self, "_systemNavigationAction"),
        let action = object_getIvar(UIApplication.shared, sysNavIvar) as? NSObject,
        let destinations = action.perform(#selector(getter: PrivateSelectors.destinations)).takeUnretainedValue() as? [NSNumber],
        let firstDestination = destinations.first
    else {
        return false
    }
    action.perform(#selector(PrivateSelectors.sendResponseForDestination), with: firstDestination)
    return true
}

【讨论】:

    最近更新 更多