面試官:同學,說說 Applink 的使用以及原理

  • 2019 年 12 月 24 日
  • 筆記

簡介

通過 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

作者:哈哈將