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

简介

经过 Link这个单词咱们能够看出这个是一种连接,使用此连接能够直接跳转到 APP,经常使用于应用拉活,跨应用启动,推送通知启动等场景。html

流程

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

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

add URL intent filters

建立一个 URLshell

或者也能够点击 “How it works” 按钮json

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 格式以下:app

[{
  "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

做者:哈哈将

行业前沿、移动开发、数据建模等干货内容,尽在公众号:个推技术学院

相关文章
相关标签/搜索