Google的自定义iOS键盘Gboard如何以编程方式解散最前端的应用程序?

Pra*_*ogg 39 ios ios-keyboard-extension

谷歌的自定义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使用自定义URL方案来启动Gboard容器应用程序.Gboard键盘和Apple消息应用程序在App堆栈中向下推送一层,而Gboard容器应用程序现在是App堆栈中最前面的应用程序.Gboard容器应用程序使用麦克风收听用户的语音并将其转换为放置在屏幕上的文本.
  5. 当用户对他们在屏幕上看到的文本输入感到满意时,用户点击"完成"按钮.
  6. 这就是魔术发生的地方......当文本输入屏幕被取消时,Gboard容器应用程序也会自动被解雇.Gboard容器应用程序消失并被Apple Messages应用程序取代(有时Gboard键盘扩展程序仍处于活动状态,有时会重新启动,有时需要通过点击文本字段手动重新启动.).Google如何实现这一目标?
  7. 最后,用户在文本输入字段中看到刚刚翻译的文本自动插入.据推测,谷歌通过在Gboard容器应用和键盘扩展之间共享数据来实现这一目标.

我假设Google使用私有API,通过使用Objective-C运行时内省探索状态栏的视图层次结构,并以某种方式合成tap事件或调用公开的目标/操作.我已经很好地探索了这个,并且能够在状态栏中找到有趣的UIView子类,比如UIStatusBarBreadcrumbItemView,它包含一个UISystemNavigationAction数组.我正在继续探索这些类,希望我能找到一些复制用户交互的方法.

我知道使用私有API是一种很好的方式来让您的应用程序提交从App Store中被拒绝 - 这不是我想在答案中解决的问题.我主要想找到关于Google如何完成以编程方式弹出Gboard中应用程序切换堆栈中的一个应用程序的具体任务的具体答案.

ken*_*ytm 36

你的猜测是正确的 - Gboard正在使用私有API来做到这一点.

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

当语音到文本操作完成后,我们可以从Xcode或Console中检查它调用该-[AVAudioSession setActive:withOptions:error:]方法的syslog .所以我对Gboard应用程序进行了逆向工程,并寻找与此相关的堆栈跟踪.

爬上调用堆栈,我们可以找到-[GKBVoiceRecognitionViewController navigateBackToPreviousApp]方法,并...

在此输入图像描述

...... _systemNavigationAction是的,绝对私密的API.

由于class_getInstanceVariable是公共API并且"_systemNavigationAction"是字符串文字,因此自动检查程序无法记录私有API使用情况,并且人工审阅者可能看不到"跳回到以前的应用程序"行为有任何问题.或者可能是因为他们是谷歌而你不是......


执行"跳回以前的应用程序"操作的实际代码如下所示:

@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];
}
Run Code Online (Sandbox Code Playgroud)

特别地,该-sendResponseForDestination:方法执行实际的"返回"动作.

(由于API没有记录,Gboard实际上使用的API 不正确.他们使用了错误的签名-(void)sendResponseForDestination:(id)destination.但实际上所有数字1都是相同的,所以Google开发人员这次很幸运)


Phi*_*hov 6

更新:此 hack 在 iOS 17 上停止工作,Gboard 也无法再执行此操作


@kennytm 答案的Swift 版本

@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
}
Run Code Online (Sandbox Code Playgroud)

  • 不幸的是,这在 iOS 17 中停止工作。object_getIvar 现在返回“nil”。 (2认同)