美文网首页Android开发Android开发Android开发经验谈
面试官:同学,说说 Applink 的使用以及原理

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

作者: 4ca1bbef6a0c | 来源:发表于2020-06-18 20:38 被阅读0次

    简介

    通过 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-... 的时候看到这个文件(替换成自己的域名),那么说明服务端的配置是成功的。目前可以通过 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页面。

    1. 要使用App Links开发者必须维护一个与app相关联的网站

    对于小的开发者来说这个有点困难,因为他们没有能力为app维护一个网站,但是它们仍然希望通过web链接获得流量。

    1. 对 ink 域名不太友善
      在测试中发现,国内各大厂商对 .ink 域名不太友善,很多的是被支持了 .com 域名,但是不支持 .ink 域名。


    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/ssdpxktx.html