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

個推開發者發表於2019-09-26

簡介

通過 Link這個單詞我們可以看出這個是一種連結,使用此連結可以直接跳轉到 APP,常用於應用拉活,跨應用啟動,推送通知啟動等場景。

流程

在AS 上其實已經有詳細的使用步驟解析了,這裡給大家普及下

面試官:同學,說說 Applink 的使用以及原理
快速點選 shift 兩次,輸入 APPLink 即可找到 AS 提供的整合教程。 在 AS 中已經有詳細的使用步驟了,總共分為 4 步

add URL intent filters

建立一個 URL

面試官:同學,說說 Applink 的使用以及原理
或者也可以點選 “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 的檔案,需要把這個檔案放到伺服器指定的目錄下

面試官:同學,說說 Applink 的使用以及原理
基於安全原因,這個檔案必須通過 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 安裝的時候檢查這個檔案。,如果你能在請求 yourdomain.com/.well-known… 的時候看到這個檔案(替換成自己的域名),那麼說明服務端的配置是成功的。目前可以通過 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 關聯檔案。

原理

上述這些都簡單的啦,依葫蘆畫瓢就行,下面講些深層次的東西,不僅要知道會用,還得知道為什麼可以這麼用,不然和鹹魚有啥區別。

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

上訴也說了,我們配置的域名是在 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.官方文件: developer.android.com/studio/writ…

作者:哈哈將

行業前沿、移動開發、資料建模等乾貨內容,盡在公眾號:個推技術學院

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

相關文章