Leon’s Blog

Dreams don't work unless you do.

iOS应用间通信

我们在日常使用移动应用产品时经常会遇到正在使用的这款APP在操作中经常会跳转到另一款APP中去操作,比如支付宝支付,微信支付,微博分享等。这不禁让我们联想到苹果的沙盒机制,受苹果的这一机制影响,我们不能应用之间资源共享,也就是说我们的应用都是独立的块,应用内所需要的资源都是自给自足不受外界的”救济”,那么应用之间通信的行为就是通过URL Schemes来实现的。

URL Schemes

我们通过对比网页链接来理解iOS上的 URL Schemes。

URL Schemes有两个单词:

  • URL:链接地址或网址,https://www.baidu.com 就是一个URL。
  • Schemes:表示的是URL中的最初位置,即://之前的那段字符,比如 https://www.baidu.com 这个URL的Schemes就是https。

根据上面对URL Schemes的理解,我们在iOS中如果想定位到某个APP,只需要知道 Schemes 部分就可以做到了,比如我们要打开微信这个APP,我们只需要知道微信的 Schemeswexin ,在后面拼接上 :// 即: wexin://
然后在 safari 浏览器输入网址的地方输入 wexin:// 确认后就会在屏幕中弹出 在"微信"中打开链接吗? 的提示框,当你选择打开操作后就会自动跳转到微信界面了。

目前我们仅仅是定位到了某个APP,如果想跳转到这个APP并让它做一些我们想要的操作的话,我们需要在://后面跟一些路径参数让它定位到具体的功能或页面:

网页(百度) iOS应用(微信)
网站首页: https://www.baidu.com 打开应用: wexin://
子页面: https://www.baidu.com/duty(百度免责声明页面) wexin://dl/moments (朋友圈)

通过上面的表格对比异同:

  • 不同点:定位到网站首页需要一个完整的URL,而定位某个应用需要 Schemes 后面拼接上 ://
  • 相同点:网站定位到具体的某个页面都需要提供路径,比如定位到百度的免责声明页面需要在 https://www.baidu.com 后面拼接/duty,iOS应用中定位到APP的具体功能或页面需要在 wexin:// 后面拼接 dl/moments

补充:

  • 苹果没有硬性要求APP必须要有URL Schemes,所以我们所装的APP不是每一款都会有`URL Schemes,即使有URL Schemes也仅仅是能让我们从自己的应用A跳转到另一个应用B,如果想跳转到应用B后再根据你传递的内容跳转到具体的页面或执行某些功能,那么要看应用B的开发者是否制定了统一的参数规则在里面。
  • 苹果没有要求URL Schemes是唯一的,所以不是每一个URL Schemes都对应一款应用,如果两个应用拥有相同的URL Schemes,当你通过这个URL Schemes去打开应用的时候,它会打开最后安装的那款APP。实践:你新建一个工程设置URL Schemes和支付宝的相同为 alipay,当你用饿了么订餐下单并通过支付宝支付的时候,发现跳转的是你生成的APP,因为支付宝在你的工程之前安装的。基于这一特性,曾经也出现过 App 使用支付宝的 URL Schemes 拦截支付帐号和密码的事件

使用

场景:假设我们要从应用URLSchemesB跳转到应用URLSchemesA,并要求应用URLSchemesA 弹出一个新的控制器,并根据传递的参数改变控制器的背景颜色,接下来我们来实现这一过程:

1.分别新创建URLSchemesA和URLSchemesB两个工程项目

2.在URLSchemesA工程的target->info中最下方的URL Types条目中新增URL Schemes:

3.然后在URLSchemesA工程info.plist文件中查看:

可以发现URL Schemes是一个数组,所以在它下面可以添加很多个Item,比方说我们又新增了一个URLSchemesAA,这样做是允许的,说明我们不仅可以通过URLSchemesA://打开APP,还可以使用URLSchemesAA://打开APP,就像微信一样自定义了很多URL Schemes,例如:weixin://,wechat://等。

4.由于我们要实现的不仅仅是简单的跳转过程,还包括跳转后 弹出控制器 + 改变控制器的背景色,所以我们要对URL制定统一规范(scheme://[target]/[action]?[params]),target指的是某个类,action指的是该类的具体方法,params指的是参数,根据这种规范我们生成符合我们要求的URL:URLSchemesA://SecondVC/setupBackgroundColorParmas?color=blueColor,具体代码如下:

1
2
3
4
5
6
7
8
9
- (IBAction)clickedOpenURLSchemesA:(id)sender {
NSString *urlSchemesAStr = @"URLSchemesA://SecondVC/setupBackgroundColorParmas?color=blueColor";
NSURL *urlSchemesA = [NSURL URLWithString:urlSchemesAStr];
UIApplication *application = [UIApplication sharedApplication];
if ([application canOpenURL:urlSchemesA]) {
// 如果可以打开的话,在这里实现跳转
[application openURL:urlSchemesA];
}
}

首先要用canOpenURL:方法判断是否可以打开指定的URL,如果手机中没有这个URL Schemes的应用,会返NO。还有就是iOS9之后,如果没有在info.plist中添加白名单,结果也会返回NO,并且会在控制台输出

1
canOpenURL: failed for URL: "URLSchemesA://" - error: "This app is not allowed to query for scheme URLSchemesA"

这就需要我们把URLSchemesA添加到URLSchemesB工程的info.plist的白名单中:

5.当跳转到URLSchemesA时,如果是iOS9之前会调用AppDelegate类中的application:openURL:sourceApplication:annotation:方法,iOS9之后会调用application:openURL:options:方法,偷下懒- -,我就只写在application:openURL:options:了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// AppDelegate.m

/**
通过指定的URL跳转到应用时调用(iOS9之前)

@param url 指定的URL
@param sourceApplication 请求打开应用的Bundle ID
*/
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {

return YES;
}

/**

通过指定的URL跳转到应用时调用(iOS9之后)

@param url 指定的URL
@param options 通过UIApplicationOpenURLOptionsSourceApplicationKey键可以获得请求打开应用的Bundle ID
*/
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {

// 判断是否与自己自定义的URL Schemes相同
if (![url.scheme isEqualToString:@"URLSchemesA"]) {

return NO;
}

NSLog(@"从URLSchemesB应用跳转过来的Bundle ID:%@", options[UIApplicationOpenURLOptionsSourceApplicationKey]);

// 获取类名
NSString *targetClass = url.host;
// 获取要执行的方法名
NSString *actionName = [[url.path stringByReplacingOccurrencesOfString:@"/" withString:@""] stringByAppendingString:@":"];
SEL actionSEL = NSSelectorFromString(actionName);
// 参数字符串转化为参数字典
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
NSString *urlString = [url query];
for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
NSArray *elts = [param componentsSeparatedByString:@"="];
if([elts count] < 2) continue;
[params setObject:[elts lastObject] forKey:[elts firstObject]];
}


UIViewController *secondVC = [[NSClassFromString(targetClass) alloc]init];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[secondVC performSelector:actionSEL withObject:params];
#pragma clang diagnostic pop
[self.window.rootViewController presentViewController:secondVC animated:YES completion:nil];

return YES;
}
1
2
3
4
5
6
7
8
9
// SecondVC.m

- (void)setupBackgroundColorParmas:(NSDictionary *)params {
NSString *colorStr = params[@"color"];
if ([colorStr isEqualToString:@"blueColor"]) {

self.view.backgroundColor = [UIColor blueColor];
}
}

当然,上述的这个简单例子只是我临时编造的一个情景,意在说明应用之间能够通过自定义URL Schemes来以这种方式通信,实际的使用情景比这要复杂的多,可能渗透到各个功能及业务模块之间,推荐大家看一下Casa Taloyum的组件化方案,其中也有谈及到应用之间的各模块调用。

这样就大工告成啦!Demo已上传至Github请点击这里查看

相关技能

如何获得手机中应用的URL Schemes?

以微信为例:

  • 首先用电脑打开iTunes,然后在商店搜索微信并下载下来。
  • 在资料库的iPhone应用中找到微信应用,右键在Finder中显示:
  • 找到微信 6.5.5.ipa包所在目录后开始解压:
  • 解压后在Payload文件下找到WeChat.app文件,右键显示包内容:
  • 在wechat.app中搜索.plist文件:
  • 在搜索到的众多info.plist中所搜URLSchemes关键字:

参考文献: