背景簡介
Deep Link 和 App Link 官方文檔
Deep link 是基于intentFilter(action ,category,data),可以讓用戶直接轉(zhuǎn)到特定應(yīng)用上的網(wǎng)址,但是如果多個應(yīng)用都符合相同的intent,例如發(fā)郵件,打開網(wǎng)頁,系統(tǒng)不知道用戶希望用哪個應(yīng)用打開,就會彈框讓用戶自己選。這時候就可以用到app-link。
App Link ,Android 6.0及以上才支持,可以理解為在deep-link上做了優(yōu)化,如果有很符合的intent,會直接打開相關(guān)應(yīng)用,不會再彈框提示。
| 屬性 | Deep Link | App Link |
|---|---|---|
| intent 網(wǎng)址協(xié)議 | http、https 或自定義協(xié)議 | 需要 http 或 https |
| intent 操作 | 任何操作 | 需要 android.intent.action.VIEW |
| intent 類別 | 任何類別 | 需要 android.intent.category.BROWSABLE 和 android.intent.category.DEFAULT |
| 鏈接驗證 | 無 | 需要通過 HTTPS 協(xié)議在您的網(wǎng)站上發(fā)布 Digital Asset Links 文件 |
| 用戶體驗 | 可能會顯示一個消除歧義對話框,以供用戶選擇用于打開鏈接的應(yīng)用 | 無對話框;您的應(yīng)用會打開以處理您的網(wǎng)站鏈接 |
| 兼容性 | 所有 Android 版本 | Android 6.0 及更高版本 |
配置
主要流程官網(wǎng)上都有,簡單說下在demo上的配置流程
在Androidmanifest配置activity
<activity android:name=".applink.AppLinkActivity">
<!--這個必須要有-->
<intent-filter android:autoVerify="true">
<!--這個action 和 這兩個category必須要有-->
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!--
系統(tǒng)會匹配
https://example.test.com
http://example.test.com
-->
<data
android:scheme="https" />
<data android:scheme="http" />
<data android:host="example.test.com" />
</intent-filter>
</activity>
處理Activity
public class AppLinkActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ATTENTION: This was auto-generated to handle app links.
Intent appLinkIntent = getIntent();
String appLinkAction = appLinkIntent.getAction();
Uri appLinkData = appLinkIntent.getData();
android.util.Log.i("AppLink","appLinkData is "+appLinkData);
}
}
配置生成assetlinks.json文件,注意文件名必須是這個,后面會解釋。

image.png
當(dāng)assetlinks.json已經(jīng)配置好了,需要把assetlinks.json上傳到設(shè)置的域名下的/.well-known/文件夾下,通過上面圖片中的Link and Verify按鈕驗證,驗證通過后就可以愉快的運行app了,注意app link只會在應(yīng)用安裝時校驗,注意官網(wǎng)有一句說明
確認要與您的應(yīng)用關(guān)聯(lián)的網(wǎng)站列表,并且確認托管的 JSON 文件有效后,請立即在您的設(shè)備上安裝應(yīng)用。等待至少 20 秒,讓系統(tǒng)完成異步驗證流程。
這一步就是在異步請求驗證app-link的合法性。
除了通過android studio自帶的工具驗證json文件外,還可以通過以下命令驗證
https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://yourhost&relation=delegate_permission/common.handle_all_urls
保證以上鏈接在公網(wǎng)能訪問就行,需要梯子
驗證
adb驗證
adb shell dumpsys package domain-preferred-apps //輸出當(dāng)前手機所有l(wèi)ink的包名
Package: com.vdian.android.lib.testforgradle // 關(guān)注自己的包名就行
Domains: example.test.com //之前設(shè)置的域名
Status: always : 200000017 //這里有4種狀態(tài)
undefined — app沒有在manifest中啟用鏈接自動驗證功能。
ask — app驗證失敗(會通過打開方式對話框詢問用戶)
always — app通過了驗證(點擊這個域名總是打開這個app)
never — app通過了驗證,但是系統(tǒng)設(shè)置關(guān)閉了此功能。
adb 直接喚起,如果是成功狀態(tài),可以直接喚起APP的指定頁面
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "https://example.test.com"
瀏覽器驗證
<a >點我link</a>
坑點
- app在安裝驗證時,會驗證Androidmanifest里配置的所有域名,而不是配置android:autoVerify="true"的Activity下的域名,既然這樣,autoVerify應(yīng)該配置在application標(biāo)簽比較合理。
- 校驗所有域名時,一旦有一個域名不符合正則,代碼會拋異常,流程就斷了,網(wǎng)上有很多說要翻墻什么的。其實根本不需要,真正的原因應(yīng)該就是域名校驗的問題。
- 瀏覽器喚起時,只有在chrome或者基于chrome的瀏覽器才可以,市面上大部分都不支持,例如小米自帶的瀏覽器,qq瀏覽器,百度瀏覽器等都不支持。
- 總的來說在國內(nèi)意義不大,像微信內(nèi)置瀏覽器攔截了系統(tǒng)的deeplink和applink,自己維護了一套(https://wiki.open.qq.com/index.php?title=mobile/%E5%BA%94%E7%94%A8%E5%AE%9D%E5%BE%AE%E4%B8%8B%E8%BD%BD),需要申請白名單才行。
源碼解析
PackageManagerService類
// 判斷是否需要驗證host
// If any filters need to be verified, then all need to be.
boolean needToVerify = false;
for (PackageParser.Activity a : pkg.activities) {
for (ActivityIntentInfo filter : a.intents) {
if (filter.needsVerification() && needsNetworkVerificationLPr(filter)) {
if (DEBUG_DOMAIN_VERIFICATION) {
Slog.d(TAG,
"Intent filter needs verification, so processing all filters");
}
needToVerify = true; //只要有1個Actvity帶有autoVerify,那就是true
break;
}
}
}
//如果需要驗證,那就獲取當(dāng)前的配置的host
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());
// 只要系統(tǒng)權(quán)限沒有關(guān)閉,這里都會把host加載進去
mIntentFilterVerifier.addOneIntentFilterVerification(
verifierUid, userId, verificationId, filter, packageName);
count++;
}
}
}
}
}
PackageManagerService類
//緊接著發(fā)送廣播
private void sendVerificationRequest(int verificationId, IntentFilterVerificationState ivs) {
//注意這里是廣播的action
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()); //所有的host
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME,
ivs.getPackageName());
verificationIntent.setComponent(mIntentFilterVerifierComponent);
verificationIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
final long whitelistTimeout = getVerificationTimeout();
final BroadcastOptions options = BroadcastOptions.makeBasic();
options.setTemporaryAppWhitelistDuration(whitelistTimeout);
DeviceIdleController.LocalService idleController = getDeviceIdleController();
idleController.addPowerSaveTempWhitelistApp(Process.myUid(),
mIntentFilterVerifierComponent.getPackageName(), whitelistTimeout,
UserHandle.USER_SYSTEM, true, "intent filter verifier");
mContext.sendBroadcastAsUser(verificationIntent, UserHandle.SYSTEM,
null, options.toBundle());
if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
"Sending IntentFilter verification broadcast");
}
這個廣播的接收在IntentFilterVerificationReceiver這個類
這里有第二個坑點,會驗證pms里搜集的所有host,一旦有一個host不符合正則校驗,整個流程就斷了
最合理的應(yīng)該是 只校驗配置android:autoVerify="true"的host。
try {
ArrayList<String> sourceAssets = new ArrayList<String>();
for (String host : hostList) {
// "*.example.tld" is validated via https://example.tld
if (host.startsWith("*.")) {
host = host.substring(2);
}
sourceAssets.add(createWebAssetString(scheme, host)); //這里正則校驗
finalHosts.add(host);
}
extras.putStringArrayList(DirectStatementService.EXTRA_SOURCE_ASSET_DESCRIPTORS,
sourceAssets);
} catch (MalformedURLException e) {
Log.w(TAG, "Error when processing input host: " + e.getMessage());
sendErrorToPackageManager(context.getPackageManager(), verificationId);
return;
}
private String createWebAssetString(String scheme, String host) throws MalformedURLException {
//校驗不通過就拋異常了。。
if (!Patterns.DOMAIN_NAME.matcher(host).matches()) {
throw new MalformedURLException("Input host is not valid.");
}
if (!scheme.equals("http") && !scheme.equals("https")) {
throw new MalformedURLException("Input scheme is not valid.");
}
return String.format(WEB_ASSET_FORMAT, new URL(scheme, host, "").toString());
}
如果以上校驗都通過了,會啟動DirectStatementService去驗證,最終走的邏輯是IsAssociatedCallable里的verifyOneSource方法。而verifyOneSource方法里調(diào)用了DirectStatementRetriever的retrieveStatements方法。
DirectStatementRetriever類
//注意這個常量,說明為什么文件名和路徑都必須按照官方的來
private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json";
@Override
public Result retrieveStatements(AbstractAsset source) throws AssociationServiceException {
if (source instanceof AndroidAppAsset) {
return retrieveFromAndroid((AndroidAppAsset) source);
} else if (source instanceof WebAsset) { //這里的source都是webAsset類型
return retrieveFromWeb((WebAsset) source);
} else {
throw new AssociationServiceException("Namespace is not supported.");
}
}
DirectStatementRetriever類
//通過http獲取服務(wù)端配置,和本地校驗,到這一步流程就走完了。
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);
}
}