Google的自定义iOS键盘Gboard如何以编程方式忽略最前面的应用程序?

Google的自定义iOS应用程序Gboard具有无法使用iOS SDK中的公共API完成的有趣function(截至iOS 10)。 我想知道Google究竟是如何完成以编程方式在Gboard的App Switching堆栈中popup一个应用程序的任务。

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

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

  1. 用户在iPhone上启动Apple Messages应用程序,然后点击文本字段即可开始input文字。
  2. Gboard键盘扩展程序已启动,用户可以看到Gboard自定义键盘(当它们仍在Apple Messages应用程序中时)。
  3. 用户点击Gboard键盘扩展内的麦克风键进行语音到文本input。
  4. Gboard使用自定义urlscheme来启动Gboard容器应用程序。 Gboard键盘和Apple消息应用程序在应用程序堆栈中向下推进一层,而Gboard容器应用程序现在是应用程序堆栈中最前面的应用程序。 Gboard容器应用程序使用麦克风来聆听用户的语音,并将其转换为放置在屏幕上的文本。
  5. 当用户在屏幕上看到的文本input满意时,点击“完成”button。
  6. 这就是魔法发生的地方……当文本input屏幕被解散时,Gboard容器应用程序也被自动解除。 Gboard容器应用程序消失,并被Apple Messages应用程序取代(有时Gboard键盘扩展程序仍处于活动状态,有时会重新启动,有时需要通过点按文本框来手动重新启动)。 Google如何做到这一点?
  7. 最后,用户在文本input框中看到自动插入的文本。 据推测Google是通过在Gboard容器应用程序和键盘扩展之间共享数据来实现这一点的。

我会假设Google使用私有API,通过使用Objective-C运行时自省来探索状态栏的视图层次结构,并以某种方式合成轻击事件或调用暴露的目标/动作。 我已经探索了这一点,并已经能够在状态栏内find有趣的UIView子类,比如包含UISystemNavigationAction数组的UIStatusBarBreadcrumbItemView 。 我正在继续探索这些类,希望能find一些复制用户交互的方法。

我明白,使用私有API是一种很好的方式,可以让您的应用程序拒绝从App Store提交 – 这不是一个我想在答案中解决的问题。 我正在寻找关于Google如何完成以编程方式在Gboard中的应用切换堆栈中popup一个应用的任务的具体答案。

你的猜测是正确的 – Gboard使用私有API来做到这一点。

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

当语音到文本操作完成后,我们可以从Xcode或Console中检查系统日志,它调用-[AVAudioSession setActive:withOptions:error:]方法。 所以我已经对Gboard应用程序进行了反向devise,并查找与此相关的堆栈跟踪。

爬上调用堆栈,我们可以find-[GKBVoiceRecognitionViewController navigateBackToPreviousApp]方法,和…

在这里输入图像说明

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

由于class_getInstanceVariable是一个公共API, "_systemNavigationAction"是一个string文字,所以自动检查器不能注意到私有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没有logging,所以Gboard实际上是在错误地使用这个API,他们使用了错误的签名-(void)sendResponseForDestination:(id)destination ,但是除了1之外的所有数字都是一样的,所以Google开发者这次幸运)