Applink使用教程及原理解析

今年11.11大促期间,各大电商平台都使出了浑身解数,吸引剁手族买买买。个推作为大促期间的消息推送服务商,为蘑菇街等电商APP在消息的稳定下发环节提供着强大支撑和保障。今年的11.11个推全球消息下发总量再创新高,超过274亿条。而2017年和2018年11.11当天个推推送的总下发量分别是超过110亿条和232亿条。

那么个推是如何在11.11期间支撑起数百亿级别的推送量,且使消息推送稳定率达到了99.9%的呢?这背后离不开个推强大智能的技术服务。而Applink 在推送中也发挥了一定的做用。它使消息不再局限于手机通知栏。开发者可以通过AppLink技术,让用户在点击短信、信息流或Banner后,直接跳转到APP指定页面,在打造流畅用户体验的同时实现了高效的转化,提升了消息推送的到达率与点击率。本文将着重分析一下个推Applink的技术原理和使用方式。

简介

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

流程

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

快速点击 shift 两次,输入 APPLink 即可找到 AS 提供的集成教程。详细教程可参加AS,总共分为 4 步:

add URL intent filters

创建一个 URL 

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

Add logic to handle the intent

选择通过 applink 启动的入口 activity。点击完成后,AS 会自动在两个地方进行修改,详情如下:

(一)

 <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可能是一种隐式的APP启动方式。

(二)

    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 标签下面的域名,那我们可以推理出安装APP的底层实现其实是在源码中 PackageManagerService进行的。以下方法可以帮助你快速找到校验 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);    } }

安卓底层在安装APP时会对 APPLink 进行检查、搜集、验证,判断其是否为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);    }}

以上讲解我们可以得出一个结论,即Applink的本质是通过 HTTPURLConnection 去发起请求。前文还留了个问题,url 的后缀是不是一定要写成是不是一定要写成/.well-known/assetlinks.json 格式呢?看到这里相信大家都已经明白了,格式一定要这么写!!格式就是 WELL_KNOWN_STATEMENT_PATH 参数!

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

缺点

1. 只能在 Android M 系统上支持 在配置好了app对Applink的支持之后,只有运行Android M的用户才能使Applink正常工作。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

总结

通过使用Applink,我们个推拓宽了交互场景,对App的启动有了一个更多元化的选择,缩短了App的启动路径,能够使用户更快地启动App;同时,因为Applink的内在逻辑已经在 Android framework 层兼容,这使得我们推送服务的准确性也得到了一定的保障。未来,个推也将持续优化消息推送服务,并进一步提高推送的到达率与点击率,以满足一些实时性要求更高更复杂的业务场景需求。

发布了77 篇原创文章 · 获赞 26 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/Androilly/article/details/103060214