技术经验谈 技术经验谈
首页
  • 最佳实践

    • 抓包
    • 数据库操作
  • ui

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • 总纲
  • 整体开发框架
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

hss01248

一号线程序员
首页
  • 最佳实践

    • 抓包
    • 数据库操作
  • ui

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • 总纲
  • 整体开发框架
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 最佳实践

  • ui

  • 优化

  • aop

  • apm

  • 架构

  • webview

    • webview和deeplink跳转
      • 对intent://的处理
        • 方式1:
        • 方式2:
      • 自定义协议
      • http协议
      • 1 初次进入,使用webview.loadUrl(url):
      • 2 在里面点击某个链接
      • 3 重定向的页面
      • 2.1 为什么扫码直接访问applink或intent开头的link无法跳转,而把这些link放到web页面点击打开就能跳?
      • 2.2 如何实现adjust那样的一个https链接自动识别是否安装app,自动跳转的功能
      • 2.3 google play的延迟深度链接怎么做?
      • 如何让自己的app具有deeplink功能,如何接收deeplink传入的参数
      • Android代码里对deeplink和intent协议的处理
        • 原生代码里
        • 对普通deeplink协议:
        • 对intent协议
      • 查看这个deeplink对应的app有没有安装
      • webview的client回调里
    • webview和js的一些疑难问题
    • webview工程化
    • webview文件选择-input-file适配
    • webview权限适配和getUserMedia适配
  • rxjava

  • activity-fragment-view的回调和日志
  • Android加密相关
  • Android命令行操作
  • app后台任务
  • kotlin
  • kotlin漫谈
  • kotlin语言导论
  • sentry上传mapping.txt文件
  • so放于远程动态加载方案
  • states
  • Xposed模块开发
  • 一个关于manifest合并的猥琐操作
  • 玩坏android存储
  • 获取本app的安装来源信息
  • Android
  • webview
hss01248
2022-05-27
目录

webview和deeplink跳转

# webview和deeplink跳转

# demo工程

https://github.com/hss01248/deeplinkDemo

# deeplink接收

参考 Android 上玩转 DeepLink:如何最大程度的向 App 引流 (opens new window)

建一个activity,贴入:

<activity
            android:name=".RouterActivty"
            android:exported="true"
            android:theme="@style/Theme.AppCompat.Light.NoActionBar">
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data
                    android:host="news.zhoulujue.com"
                    android:pathPattern="/.*"
                    android:scheme="http" />
                <data
                    android:host="news.zhoulujue.com"
                    android:pathPattern="/.*"
                    android:scheme="https" />
            </intent-filter>
            <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="zljnews" />
                <data android:host="news.zhoulujue.com" />
                <data android:pathPattern="/.*" />
            </intent-filter>
        </activity>
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

zljnews://news.zhoulujue.com/vvvvv

activity里接收入参:

public class RouterActivty extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        AlertDialog dialog = new AlertDialog.Builder(this)
                .setTitle("router")
                .setMessage(getIntent().getDataString())//拿到uri
                .setPositiveButton("ok", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        finish();

                    }
                }).create();
        dialog.show();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# webview对deeplink的处理

参考:Android WebView 跳转第三方App (opens new window)

重写shouldOverrideUrlLoading + 处理onReceivedError回调里的errorCode == WebViewClient.ERROR_UNSUPPORTED_SCHEME的情况.

因为有的不走shouldOverrideUrlLoading

public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    String scheme = request.getUrl().getScheme();
                    if(!TextUtils.isEmpty(scheme) && !scheme.contains("http")){
                        return DeepLinkJumpUtil.jump(view.getContext(),request.getUrl().toString());
                    }
                    return super.shouldOverrideUrlLoading(view, request);
                }
                return super.shouldOverrideUrlLoading(view, request);
            }
1
2
3
4
5
6
7
8
9
10

处理方式:

public static void jump(Context context,String newurl){
        //弹窗选择:
        //if (!newurl.startsWith("http")) { 这个判断在shouldOverrideUrlLoading里加
            try {
                final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(newurl));
                //List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(intent, 0);
                ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0);
                if(resolveInfo != null){
                    String name = "";
                    if(resolveInfo.activityInfo != null && !TextUtils.isEmpty(resolveInfo.activityInfo.packageName)){
                        name = AppUtils.getAppName(resolveInfo.activityInfo.packageName);
                    }
                    XLogUtil2.w(resolveInfo, name);
                    AlertDialog dialog = new AlertDialog.Builder(context)
                            .setTitle("跳转")
                            .setMessage("是否跳转到 "+ name)
                            .setPositiveButton("ok", new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                                            | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                                    context.startActivity(intent);
                                }
                            }).setNegativeButton("cancel", new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {

                                }
                            }).create();
                    dialog.setCancelable(false);
                    dialog.setCanceledOnTouchOutside(false);
                    dialog.show();
                }else {
                    //引导跳到应用市场
                    ToastUtils.showLong("没有对应的activity,跳去应用市场吧");
                }

            } catch (Exception e) {
                // 防止没有安装的情况
                XLogUtil.exception(e);
                ToastUtils.showLong("没有对应的activity,跳去应用市场吧");
                //No Activity found to handle Intent { act=android.intent.action.VIEW
                // dat=tunaikumobile://open?_branch_referrer=H4sIAAAAAAAAA8soKSkottLXLynNS8zMLtVLLCjQy8nMy9Z3zC7NScwuDYGIG9mrGpkYF9gmxidCxAHgNy05OAAAAA==&link_click_id=991275456483771349 flg=0x30000000 }

                //{ act=android.intent.action.VIEW dat=intent:// flg=0x30000000 }

            }

        //}
    }
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

# 对intent://的处理

注意,如果在shouldOverrideUrlLoading里不处理,直接调用super.shouldOverrideUrlLoading(),那么会没有翻译,系统webview默认不处理,而不是默认处理.

# 方式1:

完全手动

分析intent://的格式,手动截字符串

<a href="intent://news.zhoulujue.com/recipe/100390954#Intent;scheme=zljnews;package=com.zhoulujue.news;end"> Intent scheme </a> 
1

解析

 /**
     * chrome不会帮我们处理,构建系统intent也找不到对应的activity,需要自己改成标准格式: zljnews://news.zhoulujue.com/recipe/100390954
     * @param context
     * @param newurl
     * @return
     */
    private static boolean jumpByIntent(Context context,String newurl) {
        int idx = newurl.indexOf("#");
        if(idx<0){
            LogUtils.w("非法intent,不带#",newurl);
            return false;
        }
        String path0 = newurl.substring(0,idx);
        String next = newurl.substring(idx+1);
        if(!next.startsWith("Intent;") || !next.endsWith(";end")){
            LogUtils.w("非法intent,不以end结尾",newurl);
            return false;
        }
        Map<String,String> map = new HashMap<>();

        String[] strings = next.split(";");
        for (String string : strings) {
            if(string.contains("=")){
                String[] split = string.split("=");
                map.put(split[0], URLDecoder.decode(split[1]));
            }
        }
        if(map.containsKey("scheme") && map.containsKey("package")){
            String realUrl = path0.replace("intent://",map.get("scheme")+"://");
            LogUtils.w("realUrl",realUrl,map);
            final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(realUrl));
            ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0);
            if(resolveInfo == null){
                ToastUtils.showLong("没有对应的activity,跳去应用市场吧: "+ map.get("package"));
                return false;
            }
            return jump(context, realUrl);
        }
        LogUtils.w("非法intent,不含scheme 和 package",newurl);
        return false;
    }
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

复杂参数示例:

intent://xxx.yyy.com?actionKey=DzAAAAAuuAA=&adjust_reftag=cmPkr9gOrmOOK#Intent;scheme=zzz;package=io.zzzz.uuuu;S.market_referrer=adjust_reftag%3DcmPkr9gOrmOOK;S.browser_fallback_url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dio.zzzz.uuuu%26referrer%3Dadjust_reftag%253DcmPkr9gOrimOOK;end

需要解析并转换成真正的deeplink:

│ args[0] = realUrl
│ args[1] = zzz://xxx.yyy.com?actionKey=DzAAAAAuuAA=&adjust_reftag=cmPkr9gOrmOOK
│ args[2] = {package=io.zzzz.uuuu, S.browser_fallback_url=https://play.google.com/store/apps/details?id=io.zzzz.uuuu&referrer=adjust_reftag%3DcmPkr9giOrmOOK, scheme=zzz, S.market_referrer=adjust_reftag=cmPkr9gOrmOOK}
1
2
3
4
5
6
7

# 方式2:

利用Intent.URI_INTENT_SCHEME

private static boolean jumpByIntent2(Context context, String newurl) {
        Intent intent = null;
        try {
            //使用Intent.URI_INTENT_SCHEME,表示适配url为intent://开头的情况
            intent = Intent.parseUri(newurl, Intent.URI_INTENT_SCHEME);
            intent.addCategory(Intent.CATEGORY_BROWSABLE);

            intent.setComponent(null);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
                intent.setSelector(null);
            }
            ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0);
            if(resolveInfo == null){
                ToastUtils.showLong("没有对应的activity,跳去应用市场吧: " );//map.get("package")
                return false;
            }

            //todo 弹窗提示
            ActivityStackManager.getInstance().getTopActivity().startActivityIfNeeded(intent, -1);
            //return jump(context, realUrl);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return false;
    }
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

# 测试

# 自定义协议

zljnews://zljnews/article/123456/

image-20220429160114417

image-20220429160138980

# http协议

触发404或者dns未解析: errorCode == WebViewClient.ERROR_HOST_LOOKUP

image-20220429160422138

image-20220429160445484

# 关于webview拦截的回调

  • 直接用webvie.loadUrl(url), 走的是shouldInterceptRequest() 判断是否 main frame
  • ,如果是web页面里点击链接,则走shouldOverrideUrlLoading

# 关于一个h5页面在webview里加载耗时的计算

同理,页面加载耗时,其开始的判定,应该统一由shouldInterceptRequest() 里开始计时,而不是由onPageStart

使用下面的库来观测:

api 'com.github.skyNet2017:webviewdebug:1.2.2-from117'
1

# 1 初次进入,使用webview.loadUrl(url):

shouldInterceptRequest()->onPageStarted()->onPageFinished()

其中onPageFinished() 可能回调多次,onProgressChanged() 的100进度也可能回调多次.

image-20220520100200416

# 2 在里面点击某个链接

shouldOverrideUrlLoading()->shouldInterceptRequest()->onPageStarted()->onPageFinished()

其中onPageFinished() 可能回调多次,onProgressChanged() 的100进度也可能回调多次

image-20220520100334728

# 3 重定向的页面

# 2 一些问题

# 2.1 为什么扫码直接访问applink或intent开头的link无法跳转,而把这些link放到web页面点击打开就能跳?

如下,intent://和zljnews://的链接,直接在各种浏览器输入框输入,或者生成二维码扫码进入,都无法直接唤起app

但是把它们放到一个网页中,则用浏览器都可以点击打开?

<br/>
<a href="intent://news.zhoulujue.com/recipe/100390954#Intent;scheme=zljnews;package=com.hss.deeplinktargetapp;end"> chrome规定的Intent scheme-> intent://news.zhoulujue.com/ </a>
<br/>
<br/>
<a href="zljnews://news.zhoulujue.com/article/123456/"> zljnews://news.zhoulujue.com </a>
<br/>
1
2
3
4
5
6

原因是因为新打开webview时,访问的url不会走shouldOverrideUrlLoading(). 而在web页面内点击链接,则会走.

一般浏览器在实现对 applink或intent开头的link的处理都是只处理shouldOverrideUrlLoading(),不会去额外处理shouldInterceptRequest().

我们自己实现webview时,应参考这个惯例,只在shouldOverrideUrlLoading()里处理deeplink相关问题.

那么还有问题,新打开webview访问deeplink,因为没有走shouldOverrideUrlLoading(),最终必然会走到webviewClient的onReceiveError里,如果项目真的要对此种情况特殊处理,可以在这个回调进行:

   if (errorCode == WebViewClient.ERROR_UNSUPPORTED_SCHEME) {
     //类似shouldOverrideUrlLoading()的操作
   }
1
2
3

# 2.2 如何实现adjust那样的一个https链接自动识别是否安装app,自动跳转的功能

使用iframe打开deeplink,同时主window重定向到market://xxxx

示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>仿adjust deeplink</title>

</head>
<body>
<iframe src="ff://m.yyyy.com?actionKey=zCIMVAIAAAA=&id=6666666" width="0px" height="0px"></iframe>


<script type="text/javascript">
    var u = navigator.userAgent;
    var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; //android终端
    var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
    if(isAndroid){
        window.location.href = "market://details?id=cc.dd.ff&referer=xxxxxx";
    }else if(isiOS){
        window.location.href = 'itms-apps://itunes.apple.com/app/id414478724?action=write-review'
    }else {
        window.location.href = "https://play.google.com/store/apps/details?id=cc.dd.ff&referer=xxxxxx";
    }

</script>
</body>
</html>
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

# 2.3 google play的延迟深度链接怎么做?

Facebook和adjust , appsFlyer,branchIO均具有延迟深度链接的功能.

延迟深度链接的本质是从点击链接开始,如果没有安装该app,则跳转google play并携带一个特定参数给google play, google play在用户点击安装并打开这个app后,会将该参数传入到app中.

这个参数是: referrer

比如一个adjust的广告链接,在没有安装app时,跳入google play:

https://play.google.com/store/apps/details?id=com.xxx.yyy&referrer=adjust_reftag%3DctdsesfDFRE3Z%26utm_source%3D%25E9%2582%25AE%25E4%25BB%25B6%25E8%2590%25A5%25E9%2594%2580%25E8%25BF%25BD%25E8%25B8%25AA%25E9%2593%25BE%25E6%258E%25A5
1

第一次打开app内接收referrer参数,可以手动接收,

也可以使用谷歌的sdk: Google Play Install Referrer (opens new window)

此sdk能提供一些广告相关的额外信息:

image-20220526102525204

android developers-处理 Android 应用链接 (opens new window)

adjust-深入了解移动应用深度链接 (opens new window)

为应用广告添加深度链接 (opens new window)

App深度链接与延迟深度链接 (opens new window)

# 几句话总结

# 如何让自己的app具有deeplink功能,如何接收deeplink传入的参数

  • 配置intent filter,标明shceme和host. ios和Android最好统一.
  • 目标activity的oncreate里,getIntent().getData(),就能拿到对应Uri
  • deeplink+ 特定参数后台解析为内部通用跳转,可以便捷实现配置一个activity,而能deeplink至app内所有页面的能力

# Android代码里对deeplink和intent协议的处理

# 原生代码里

# 对普通deeplink协议:

String newUrl = "zljnews://news.zhoulujue.com/recipe/100390954";
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(newurl));
1
2

# 对intent协议

String newUrl ="intent://zljnews/recipe/100390954#Intent;scheme=zljnews;package=com.zhoulujue.news;end"
1

方式1:

Intent intent = Intent.parseUri(newurl, Intent.URI_INTENT_SCHEME);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
1
2

方式2: 自己截取字符串

private static boolean jumpByIntent(Context context,String newurl) {
        int idx = newurl.indexOf("#");
        if(idx<0){
            LogUtils.w("非法intent,不带#",newurl);
            return false;
        }
        String path0 = newurl.substring(0,idx);
        String next = newurl.substring(idx+1);
        if(!next.startsWith("Intent;") || !next.endsWith(";end")){
            LogUtils.w("非法intent,不以end结尾",newurl);
            return false;
        }
        Map<String,String> map = new HashMap<>();

        String[] strings = next.split(";");
        for (String string : strings) {
            if(string.contains("=")){
                String[] split = string.split("=");
                map.put(split[0], URLDecoder.decode(split[1]));
            }
        }
        if(map.containsKey("scheme") && map.containsKey("package")){
            String realUrl = path0.replace("intent://",map.get("scheme")+"://");
            LogUtils.w("realUrl",realUrl,map);
            final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(realUrl));
            ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0);
            if(resolveInfo == null){
                ToastUtils.showLong("没有对应的activity,跳去应用市场吧: "+ map.get("package"));
                return false;
            }
            return jump(context, realUrl);
        }
        LogUtils.w("非法intent,不含scheme 和 package",newurl);
        return false;
    }
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

# 查看这个deeplink对应的app有没有安装

两种方式:

ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0);
1

或者

List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(intent, 0);
1

获取对应app的包名和应用名

AppUtils.getAppName(resolveInfo.activityInfo.packageName)
1

# webview的client回调里

几个事实

  • 1 Android sdk的 webview 本身不处理这些deeplink,以及intent协议. 即使这些协议是Android或chrome本身要求的.必须自己写上面的代码去处理

  • 2 一般都是处理shouldOverrideUrlLoading(),在这里拿到对应url,通过上面的代码进行处理. 一般浏览器app都是有处理的. 大多是通过resolveActivity/queryIntentActivities检测到有安装目标app后, 弹窗询问用户是否打开,如果没有安装,则没有任何反应

  • 直接扫码或在浏览器地址栏输入deeplink或intent协议,不会触发跳转,而是显示错误页面,因为第一次浏览器第一次load的url不会走shouldOverrideUrlLoading(). 最终走到onReceiveError()里,对应errorCode 为WebViewClient.ERROR_UNSUPPORTED_SCHEME. 如果项目中webview需要兼容,可以在这里处理.

  • 对于像adjust这种通过https链接来提供打开app或跳转应用市场的能力的广告sdk, 链接本身并没有探测对应app是否安装的能力,只能通过iframe打开deeplink,同时重定向到market://协议的方式,同时试图打开对应app和跳到应用市场.

编辑 (opens new window)
上次更新: 2022/08/25, 20:20:31
设备唯一性id问题
webview和js的一些疑难问题

← 设备唯一性id问题 webview和js的一些疑难问题→

最近更新
01
截图后的自动压缩工具
12-27
02
图片视频文件根据exif批量重命名
12-27
03
chatgpt图片识别描述功能
02-20
更多文章>
Theme by Vdoing | Copyright © 2020-2025 | 粤ICP备20041795号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式