前言
项目求人前段时间有个需求,希望提高应用的用户日活量,在用户使用Google或者Chrome浏览器检索“求人”相关关键字后,点击检索的网页,如果网页链接的前缀是项目的官网(https://www.xxxx.xx.com/…),那么需要启动App, 并在合适的地方解析出获取到的链接,跳转到指定的检索内容页面。
项目采用了谷歌官方提供的API-- DeepLink,也就是深度链接(https://developer.android.com/training/app-links/deep-linking)来实现。
DeepLink介绍
Deeplink又叫深度链接,是一项减少运营难度的技术,它在手机上的应用场景十分广泛。比如
朋友在微信上分享一条资讯, 我点击打开就能跳转到资讯来源的app,并在app指定页面进行打开。浏览网页点击微博相关的网页、点击知乎相关的网页等等,都会弹出以下类型的提示框,提示用户打开相关app进行访问。(当然你需要已经安装了与之相关的app应用)
提示弹框的样式在不同浏览器(或应用)中有所不同,UI效果由其本身决定。
DeepLink的分类
- Deeplink: 深度链接,指已安装相应App的情况下,把特定的参数通过url 的形式传递给
App,从而直接打开指定的内部页面,实现从链接直达App内部页面的跳转。 - Deferred Deeplink:延迟深度链接,主要增加了一个是否已安装相应App的判断,用户点
击链接时,如果未安装App,则引导用户前往应用市场,下载完对应App后,首次打开该
App时自动跳转进入指定的内部页面。
DeepLink使用与否的区别
在整个流程中,Deeplink 起到的作用是显而易见的,一方面,随着操作步骤的减少,用户体
验也就随之提高;另一方面,用户点击H5链接后-键拉起App,不会再因为找不到相应页面而产生流失,App的分享页面、推广活动、投放广告的价值和转化率都将大大提高。
DeepLink实现原理
deepLink本质上是通过web网页调用Andriod原生App,然后将参数通过URL的形式传递给App实现拉起,App所注册的活动即为拉起的页面,通过获取的URL按照一定逻辑解析成跳转到指定页面所需的数据。
- scheme
scheme是用于标识具体跳转app,可以避免歧义对话框的出现(具体样式可参考上方Google浏览器提示框),唯一性的scheme大多用于 app A 启动 app B(比如微信分享的资讯打开相关的app)。而如果是点击检索网页实现跳转app,scheme自然是 http或者https。(允许网页和app皆可解析访问)
- host
为了进一步细化能够启动Activity的链接,需host指明URL的具体类型。当然,如果希望Activity能够接收更多链接,可以注册多个标签,对应不同的链接。
如果多个链接的URL相似,前缀一致,只是路径有所区别,可按照实际需求分情况处理:
eg: https://www.fenrir-inc.com.cn/aaaa/bbbb/?ccc=sfsifhihloh
https://www.fenrir-inc.com.cn/ddd/
https://www.fenrir-inc.comcn/eee/J12526/fff/
1. 统一在一个Activity中处理,只需要注册scheme,以及host,根据网页跳转的app后获取的URL进行分类讨论。
<activity
android:name="com.example.android.SplashActivity" >
<intent-filter >
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="www.fenrir-inc.com.cn" />
</intent-filter>
</activity>
- 在多个Activity中处理,通过使用 android:path 属性或其 pathPattern 或 pathPrefix 变体区分系统应针对不同 URI 路径打开哪个 Activity。
path、pathPattern、pathPrefix的区别:
- path 用来匹配完整路径的,只有特定链接才能启动Activity.
- pathPrefix 用来匹配路径的开头部分,只需指明host之后的部分前缀路径。
- pathPattern 用来表达式匹配整个路径,就是下面这些匹配符号与转义。
匹配符号:- “*” 用来匹配0次或者更多次,比如:"a *“就可以包含"a”,“aa”,"aaa"等
- “.” 用来匹配任意字符,比如:".“就可以包含"a”,“b”,"c"等
- 两种符号组合,". *"就能够批撇任意字符任意次数,比如 ". *html"就包含了“abcchtml”,"pdf.html"等
- 当然也要注意转义符号
在XML文件中,“\字符”是转义的,比如“\n”换行,所以如果想规定处理的链接是以 【“fenrir-inc.com.cn*happy”】结尾,比如这种链接【https://www.fenrir-inc.com.cn/…fenrir.com.cn *happy】(当然没有链接这么怪的)
那么应该写成这样:
<intent-filter>
<action android:name="android.intent.action.VIEW"></action>
<category android:name="android.intent.category.DEFAULT"></category>
<data android:scheme="https" android:host="www.fenrir-inc.com.cn"
android:pathPattern=".*fenrir-inc\\.com\\.cn\\*happy"></data>
</intent-filter>
对于使用哪种方式处理,应根据项目实际需求决定。求人这边我用的就是第一种,省事些,需要跳转的链接scheme、host都一致的,拿到链接后对其分类讨论就可以了。
在Activity获取链接
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_browser);
handleIntent(getIntent);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
private void handleIntent(Intent intent){
if(intent.getData != null && intent.getAction.equals(Intent.ACTION_VIEW)){
String strUri = intent.getData.toString;
}
}
服务器端配置
服务器端必须在指定路径配置好 assetlink.json,这个json文件采用Digital Asset Links协议让网站和app建立关联绑定。
- 自动生成assetlink.json的方法:https://developers.google.com/digital-asset-links/tools/generator
生成的文件大致如下:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example",
"sha256_cert_fingerprints":
["14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"]
}
}]
参数说明:
- relation 关系声明,用于描述目标程序与网站关系
delegate_permission/common.handle_all_urls 向目标授权处理所有url
delegate_permission/common.get_login_creds 向目标授权登录凭证(一般用于google smart lock)
- target 用于描述需要建立关系的目标
- namespace 应用标志,可为【web】或者【android_app】
- package_name 就是Android App包名
- sha256_cert_fingerprints: Android App key store 证书SHA265指纹
文件放置位置(引用施老师调查结果)
- 将assetlinks.json文件放在网站根目录的/.well-known/ 中
- 保证https://hostname/.well-known/assetlinks.json和http://hostname/.well-known/assetlinks.json能正常访问,并且不需要任何代理,以及没有重定向
- robot.txt中允许爬虫抓取/.well-known/assetlinks.json文件
以上便是关于DeepLink基本配置的内容,接下来就是实际应用。
在实际应用前,我想先谈谈任务栈切换、回退栈的问题,当然这些都是基础性的知识,不过我一开始做的时候确实是没有具体去搞明白,所以走了些许弯路。
任务栈、回退栈
- 手机开机启动后, Home(Launcher)所在的Activity在回退栈的栈底。
- 从Launcher上的图标点击进入一个应用A(Activity)时,默认在启动整个Activity的Intent的flag里面加入了FLAG_ACTIVITY_NEW_TASK标记。
(这个标记的作用是:首先会查找是否存在和被启动的Activity具有相同的亲和性的任务栈,如果有,刚直接把这个栈整体移动到前台,并保持栈中的状态不变,即栈中的activity顺序不变,如果没有,则新建一个栈来存放被启动的activity)。
也就是说从launcher启动的Activity默认会在一个新的Task里面。
比如我们启动了一个应用,ABC三个Activity是同一个应用(ABC没有设置亲和性,默认都是跟随启动它的那个activity的亲和性的),都归为Task2。
- 如果在C中启动浏览器那么就会另起一个Task。
- 此时按下Home键,那么Task1就会到回退栈的栈顶.
- 点击应用A图标(Task2), 由于回退栈中已经存在这个应用的任务栈,所以会复用这个任务栈,并保持栈内的活动顺序不变,也就是点击应用A看到的就是C活动的UI界面。
- 此时,如果应用A(Task2)的A活动在IntentFilter注册了 www.xxxx.xx链接启动,这时,切换浏览器,点击检索此链接的网页,选择App打开方式,那么就能够看到启动了应用A(Task2)并显示的是A活动,
【假设应用A中所有的活动启动模式是Standard】
任务栈变化过程为:
- 为何要提及任务栈呢?
因为当时做使用DeepLink跳转到SplashActivity时,总是会有以下现象。
(1)有一个应用只有一个活动MainActivity【standard模式】, 如果先点击应用图标Launcher MainActivity后, 使用浏览器打开这个活动,那么就会新建一个新的活动,每用浏览器打开一次就会启动一个新的MainActivity,而此时点击图标pp却不会启动新的Activity,而是启动栈顶的活动。
(2)可如果,一开始应用并没有启动过,直接使用浏览器打开,启动MainActivity后,我们重复这个操作【使用浏览器打开MainActivity】,浏览器就不会启动新的MainActivity,而是从第二次操作开始,改为直接启动这个应用的任务栈顶部Activity(当然还是MainActivity)。
有意思的是,如果你此时选择点击app图标启动app,那么每点击一次就会创建一个新的MainActivity.
这个就是我不懂的地方
???????????
网上有很多方法,比如将要跳转的活动设置为singleTask等等,这类方法大多不符合实际需求。
为了避免【app通过Launcher启动在后台,使用浏览器跳转到app却仍然会启动新的SplashActivity】的出现,我做了一个较为优解的方法:
1. 新建一个BrowserIntentActivity,专门用于处理浏览器启动app情况
2.每次浏览器启动app时,都会启动这个活动,这个活动是透明的,如果app在后台,那么直接finish,即视觉上“打开了任务栈栈顶的活动”,如果app未启动,则finish,并跳转到SplashActivity,
3.在这个BrowserIntentActivity接收到 链接Uri, 并将其作为MainApplication的一个变量存放.
public class BrowserIntentActivity extends BaseActivity {
@Inject
BrowserIntentPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_browser);
val mainApp = MainApplication.getInstance(this);
presenter.setMainAppUri(getIntent(), mainApp);
finish();
createIntent();
}
// Start SplashActivity or the Activity at the top of the task stack
private void createIntent() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setClass(this, SplashActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
startActivity(intent);
}
4. 在需要跳转的活动中,拿到Uri变量,将其分类,解析成目标跳转界面所需的参数,并进行跳转。
HomeFragment.java
@Override
public void onResume() {
super.onResume();
handleMainAppIntent();
}
// Get Uri from MainApplication
private void handleMainAppIntent() {
val mainApp = MainApplication.getInstance(getContext());
String strUri = mainApp.getStrUri();
if (!strUri.equals("")) {
if (strUri.contains("/search/")) {
startFragment(, ,);
} else if (strUri.contains("/place/")) {
startFragment(, ,);
} else if (strUri.contains("/genre/")) {
startFragment(, ,);
} else if (strUri.contains("/style/")) {
startFragment(, ,);
} else if (strUri.contains("/rail/")) {
startFragment(, ,);
} else if (strUri.contains("/form")) {
startFragment(, ,);
} else if (strUri.contains("/job/J") && !strUri.contains("/tel/")) {
startFragment(, ,);
} else if (strUri.contains("/job/J") && strUri.contains("/tel/")) {
sstartFragment(, ,);
} else {
// Do nothing
}
}
mainApp.setStrUri("");
}