专栏首页原创面试官:同学,说说 Applink 的使用以及原理

面试官:同学,说说 Applink 的使用以及原理

简介

通过 Link这个单词我们可以看出这个是一种链接,使用此链接可以直接跳转到 APP,常用于应用拉活,跨应用启动,推送通知启动等场景。

流程

在AS 上其实已经有详细的使用步骤解析了,这里给大家普及下

快速点击 shift 两次,输入 APPLink 即可找到 AS 提供的集成教程。 在 AS 中已经有详细的使用步骤了,总共分为 4 步

add URL intent filters

创建一个 URL

或者也可以点击 “How it works” 按钮

Add logic to handle the intent

选择通过 applink 启动的入口 activity。 点击完成后,AS 会自动在两个地方进行修改,一个是 AndroidManifest

 <activity android:name=".TestActivity">
            <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="http"
                    android:host="geyan.getui.com" />
            </intent-filter>
        </activity>

此处多了一个 data,看到这个 data 标签,我们可以大胆的猜测,也许这个 applink 的是一个隐式启动。 另外一个改动点是

    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        // ATTENTION: This was auto-generated to handle app links.
        Intent appLinkIntent = getIntent();
        String appLinkAction = appLinkIntent.getAction();
        Uri appLinkData = appLinkIntent.getData();
    }

applink 的值即为之前配置的 url 链接,此处是为了接收数据用的,不再多说了。

Associate website

这一步最关键了,需要根据 APP 的证书生成一个 json 文件, APP 安装的时候会去联网进行校验。选择你的线上证书,然后点击生成会得到一个 assetlinks.json 的文件,需要把这个文件放到服务器指定的目录下

基于安全原因,这个文件必须通过 SSL 的 GET 请求获取,JSON 格式如下:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.lenny.myapplication",
    "sha256_cert_fingerprints":
    ["E7:E8:47:2A:E1:BF:63:F7:A3:F8:D1:A5:E1:A3:4A:47:88:0F:B5:F3:EA:68:3F:5C:D8:BC:0B:BA:3E:C2:D2:61"]
  }
}]

sha256_cert_fingerprints 这个参数可以通过 keytool 命令获取,这里不再多说了。 最后把这个文件上传到 你配置的地址/.well-know/statements/json,为了避免今后每个 app 链接请求都访问网络,安卓只会在 app 安装的时候检查这个文件。,如果你能在请求 https://yourdomain.com/.well-known/statements.json 的时候看到这个文件(替换成自己的域名),那么说明服务端的配置是成功的。目前可以通过 http 获得这个文件,但是在M最终版里则只能通过 HTTPS 验证。确保你的 web 站点支持 HTTPS 请求。 若一个host需要配置多个app,assetlinks.json添加多个app的信息。 若一个 app 需要配置多个 host,每个 host 的 .well-known 下都要配置assetlinks.json 有没有想过 url 的后缀是不是一定要写成 /.well-know/statements/json 的? 后续讲原理的时候会涉及到,这里先不细说。 ###Test device 最后我们本质仅是拿到一个 URL,大多数的情况下,我们会在 url 中拼接一些参数,比如

https://yourdomain.com/products/123?coupon=save90

其中 ./products/123?coupon=save90 是我们之前在第二步填写的 path。 那测试方法多种多样,可以使用通知,也可以使用短信,或者使用 adb 直接模拟,我这边图省事就直接用 adb 模拟了

adb shell am start
-W -a android.intent.action.VIEW
-d "https://yourdomain.com/products/123?coupon=save90"
[包名]

使用这个命令就会自动打开 APP。前提是 yourdomain.com 网站上存在了 web-app 关联文件。

原理

上述这些都简单的啦,依葫芦画瓢就行,下面讲些深层次的东西,不仅要知道会用,还得知道为什么可以这么用,不然和咸鱼有啥区别。

上诉也说了,我们配置的域名是在 activity 的 data 标签的,那是否是可以认为 applink 是一种隐式启动,应用安装的时候根据 data 的内容到这个网页下面去获取 assetlinks.json 进行校验,如果符合条件则把 这个 url 保存在本地,当点击 webview 或者短信里面的 url的时候,系统会自动与本地库中的域名相匹配, 如果匹配失败则会被自动认为是 deeplink 的连接。确认过眼神对吧~~~ 也就说在第一次安装 APP 的时候是会去请求 data 标签下面的域名的,并且去请求所获得的域名,那 安装->初次启动 的体验自然会想到是在源码中 PackageManagerService 实现。 一个 APk 的安装过程是极其复杂的,涉及到非常多的底层知识,这里不细说,直接找到校验 APPLink 的入口 PackageManagerService 的 installPackageLI 方法。

PackageMmanagerService.class

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
    final int installFlags = args.installFlags;
    <!--开始验证applink-->
    startIntentFilterVerifications(args.user.getIdentifier(), replace, pkg);
    ...
    
    }
    
    private void startIntentFilterVerifications(int userId, boolean replacing,
        PackageParser.Package pkg) {
    ...

    mHandler.removeMessages(START_INTENT_FILTER_VERIFICATIONS);
    final Message msg = mHandler.obtainMessage(START_INTENT_FILTER_VERIFICATIONS);
    msg.obj = new IFVerificationParams(pkg, replacing, userId, verifierUid);
    mHandler.sendMessage(msg);
}

可以看到这边发送了一个 message 为 START_INTENT_FILTER_VERIFICATIONS 的 handler 消息,在 handle 的 run 方法里又会接着调用 verifyIntentFiltersIfNeeded。

private void verifyIntentFiltersIfNeeded(int userId, int verifierUid, boolean replacing,
        PackageParser.Package pkg) {
        ...
        <!--检查是否有Activity设置了AppLink-->
        final boolean hasDomainURLs = hasDomainURLs(pkg);
        if (!hasDomainURLs) {
            if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
                    "No domain URLs, so no need to verify any IntentFilter!");
            return;
        }
        <!--是否autoverigy-->
        boolean needToVerify = false;
        for (PackageParser.Activity a : pkg.activities) {
            for (ActivityIntentInfo filter : a.intents) {
            <!--needsVerification是否设置autoverify -->
                if (filter.needsVerification() && needsNetworkVerificationLPr(filter)) {
                    needToVerify = true;
                    break;
                }
            }
        }
      <!--如果有搜集需要验证的Activity信息及scheme信息-->
        if (needToVerify) {
            final int verificationId = mIntentFilterVerificationToken++;
            for (PackageParser.Activity a : pkg.activities) {
                for (ActivityIntentInfo filter : a.intents) {
                    if (filter.handlesWebUris(true) && needsNetworkVerificationLPr(filter)) {
                        if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
                                "Verification needed for IntentFilter:" + filter.toString());
                        mIntentFilterVerifier.addOneIntentFilterVerification(
                                verifierUid, userId, verificationId, filter, packageName);
                        count++;
                    }    }   } }  }
   <!--开始验证-->
    if (count > 0) {
        mIntentFilterVerifier.startVerifications(userId);
    } 
}

对 APPLink 进行了检查,搜集,验证,主要是对 scheme 的校验是否是 http/https,以及是否有 flag 为 Intent.ACTION_DEFAULT与Intent.ACTION_VIEW 的参数,接着是开启验证

PMS#IntentVerifierProxy.class

public void startVerifications(int userId) {
        ...
            sendVerificationRequest(userId, verificationId, ivs);
        }
        mCurrentIntentFilterVerifications.clear();
    }

    private void sendVerificationRequest(int userId, int verificationId,
            IntentFilterVerificationState ivs) {

        Intent verificationIntent = new Intent(Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION);
        verificationIntent.putExtra(
                PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID,
                verificationId);
        verificationIntent.putExtra(
                PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME,
                getDefaultScheme());
        verificationIntent.putExtra(
                PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS,
                ivs.getHostsString());
        verificationIntent.putExtra(
                PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME,
                ivs.getPackageName());
        verificationIntent.setComponent(mIntentFilterVerifierComponent);
        verificationIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);

        UserHandle user = new UserHandle(userId);
        mContext.sendBroadcastAsUser(verificationIntent, user);
    }

目前 Android 的实现是通过发送一个广播来进行验证的,也就是说,这是个异步的过程,验证是需要耗时的(网络请求),发出去的广播会被 IntentFilterVerificationReceiver 接收到。这个类又会再次 start DirectStatementService,在这个 service 里面又会去调用 DirectStatementRetriever 类。在此类的 retrieveStatementFromUrl 方法中才是真正请求网络的地方

DirectStatementRetriever.class

  @Override
    public Result retrieveStatements(AbstractAsset source) throws AssociationServiceException {
        if (source instanceof AndroidAppAsset) {
            return retrieveFromAndroid((AndroidAppAsset) source);
        } else if (source instanceof WebAsset) {
            return retrieveFromWeb((WebAsset) source);
        } else {
            throw new AssociationServiceException("Namespace is not supported.");
        }
    }
  private Result retrieveFromWeb(WebAsset asset)
            throws AssociationServiceException {
        return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset);
    }
    private String computeAssociationJsonUrl(WebAsset asset) {
        try {
            return new URL(asset.getScheme(), asset.getDomain(), asset.getPort(),
                    WELL_KNOWN_STATEMENT_PATH)
                    .toExternalForm();
        } catch (MalformedURLException e) {
            throw new AssertionError("Invalid domain name in database.");
        }
    }
private Result retrieveStatementFromUrl(String urlString, int maxIncludeLevel,
                                        AbstractAsset source)
        throws AssociationServiceException {
    List<Statement> statements = new ArrayList<Statement>();
    if (maxIncludeLevel < 0) {
        return Result.create(statements, DO_NOT_CACHE_RESULT);
    }

    WebContent webContent;
    try {
        URL url = new URL(urlString);
        if (!source.followInsecureInclude()
                && !url.getProtocol().toLowerCase().equals("https")) {
            return Result.create(statements, DO_NOT_CACHE_RESULT);
        }
        <!--通过网络请求获取配置-->
        webContent = mUrlFetcher.getWebContentFromUrlWithRetry(url,
                HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS,
                HTTP_CONNECTION_BACKOFF_MILLIS, HTTP_CONNECTION_RETRY);
    } catch (IOException | InterruptedException e) {
        return Result.create(statements, DO_NOT_CACHE_RESULT);
    }
    
    try {
        ParsedStatement result = StatementParser
                .parseStatementList(webContent.getContent(), source);
        statements.addAll(result.getStatements());
        <!--如果有一对多的情况,或者说设置了“代理”,则循环获取配置-->
        for (String delegate : result.getDelegates()) {
            statements.addAll(
                    retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source)
                            .getStatements());
        }
        <!--发送结果-->
        return Result.create(statements, webContent.getExpireTimeMillis());
    } catch (JSONException | IOException e) {
        return Result.create(statements, DO_NOT_CACHE_RESULT);
    }
}

到了这里差不多就全部讲完了,本质就是通过 HTTPURLConnection 去发起来一个请求。之前还留了个问题,是不是一定要要 /.well-known/assetlinks.json,到这里是不是可以完全明白了,就是 WELL_KNOWN_STATEMENT_PATH 参数

    private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json";

缺点

  1. 只能在 Android M 系统上支持 在配置好了app对App Links的支持之后,只有运行Android M的用户才能正常工作。之前安卓版本的用户无法直接点击链接进入app,而是回到浏览器的web页面。
  2. 要使用App Links开发者必须维护一个与app相关联的网站 对于小的开发者来说这个有点困难,因为他们没有能力为app维护一个网站,但是它们仍然希望通过web链接获得流量。
  3. 对 ink 域名不太友善 在测试中发现,国内各大厂商对 .ink 域名不太友善,很多的是被支持了 .com 域名,但是不支持 .ink 域名。

机型

版本

是否识别ink

是否识别com

小米

MI6 Android 8.0 MIUI 9.5

小米

MI5 Android 7.0 MIUI 9.5

魅族

PRO 7 Android 7.0 Flyme 6.1.3.1A

三星

S8 Android 7.0

是,弹框

华为

HonorV10 Android 8.0 EMUI 8.0

oppo R11s Android 7.1.1 ColorOS 3.2

oppo

A59s Android 5.1 ColorOS 3.0

是,不能跳转到app

是,不能跳转到app

vivo

X6Plus A Android 5.0.2 Funtouch OS_2.5

vivo

767 Android 6.0 Funtouch OS_2.6

是,不能跳转到app

是,不能跳转到app

vivo

X9 Android 7.1.1 Funtouch OS_3.1

是,不能跳转到app

是,不能跳转到app

参考

1.官方文档: https://developer.android.com/studio/write/app-link-indexing.html

作者:哈哈将

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!
本文分享自作者个人站点/博客:https://my.oschina.net/u/1782938/blog复制
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 面试官:说说CountDownLatch,CyclicBarrier,Semaphore的原理?

    CountDownLatch适用于在多线程的场景需要等待所有子线程全部执行完毕之后再做操作的场景。

    艾小仙
  • 面试官:说说 ConcurrentHashMap 线程安全的实现原理?

    首先,将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

    终码一生
  • 面试官:兄弟,说说 Spring Cloud 的核心架构原理吧

    最近在补一些分布式系列的面试内容,提前几个月做做准备吧,你们懂的,也跟大家分享分享。现在分布式系统基本上都是标配了,如果你现在还在玩儿单机,没有接触过这些东西的...

    纯洁的微笑
  • 58同城面试官:说说Java常用的主流框架,以及它们的运用!

    一个java框架大概学习一到两个月就能比较熟练地使用它了,但是想要精通一个框架就需要熟悉它的各种底层原理及工作机制,这就不是一两个月的事情了,需要你坚持不懈地学...

    Java程序猿
  • 陌陌面试官:说说Spring AOP 的原理、SpringMVC 的处理过程?

    Spring AOP ,SpringMVC ,这两个应该是国内面试必问题,网上有很多答案,其实背背就可以。但今天笔者带大家一起深入浅出源码,看看他的原理。以期让...

    程序员白楠楠
  • 锁的使用以及底层原理

    我们知道,@synchronized是一把互斥锁(互斥锁保证在任何时刻都只能有一个线程访问该对象),它通过大括号来作为加锁、解锁的标识。

    拉维
  • ThreadLocal 类 的源码解析以及使用原理

      首先看这一张图,我们可以看出,每一个Thread类中都存在一个属性 ThreadLocalMap 成员,该成员是一个map数据结构,map中是一个Entry...

    小勇DW3
  • 面试官:说说你对slot的理解?slot使用场景有哪些?

    在HTML中 slot 元素 ,作为 Web Components 技术套件的一部分,是Web组件内的一个占位符

    @超人
  • MySQL索引算法原理以及常见索引的使用

    B Tree指的是Balance Tree,也就是平衡树。平衡树是一颗查找树,并且所有叶子节点位于同一层,如下:

    爱拼才会赢
  • Conccrent中 Unsafe类原理 以及 原子类AutomicXX的原理以及对Unsafe类的使用

    Java中基于操作系统级别的原子操作类sun.misc.Unsafe,它是Java中对大多数锁机制实现的最基础类。请注意,JDK 1.8和之前JDK版本的中su...

    小勇DW3
  • 阿里面试官:分别说说微信和淘宝扫码登录背后的实现原理?

    扫码登录这个功能,最早应该是微信的PC端开始搞,虽然有点反人类的功能(不扫码也没别的方式登录),但不得不说还是很酷的。

    乔戈里
  • concrrent类下ReentrantReadWriteLock类的原理以及使用

      Lock接口下的子类存在 ReentrantLock子类,该子类是一个线程同步处理类;ReentrantLock类的介绍详见XXX;

    小勇DW3
  • 美团面试官:你说你们公司的Mybatis分页插件是你写的,给我说说它的设计原理?

    大多数框架,都支持插件,用户可通过编写插件来自行扩展功能,Mybatis也不例外。

    乔戈里
  • 并发工具箱 concurrent包的原理分析以及使用

    BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。下图是对这个原理的阐述:

    小勇DW3
  • 京东一面:说说 CompletableFuture 的实现原理和使用场景?懵逼了。。

    CompletableFuture是jdk1.8引入的实现类。扩展了Future和CompletionStage,是一个可以在任务完成阶段触发一些操作Futur...

    肉眼品世界
  • 金九银十:一年前端的字节三面面经(下)

    ❗️至此经历了字节一轮的面试感觉到了难度,对整体知识的把握,绝对不可能靠背的,同样给一面的面试官点赞

    落落落洛克
  • 京东一面:说说 CompletableFuture 的实现原理和使用场景?我懵了。。

    点击上方“芋道源码”,选择“设为星标” 管她前浪,还是后浪? 能浪的浪,才是好浪! 每天 10:33 更新文章,每天掉亿点点头发... 源码精品专栏 原创 |...

    芋道源码
  • 记自己2018年三月份的面试经历烦烦烦烦面试的几家公司写在结尾

    俞其荣

扫码关注腾讯云开发者

领取腾讯云代金券