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

簡介

經過 Link這個單詞咱們能夠看出這個是一種連接,使用此連接能夠直接跳轉到 APP,經常使用於應用拉活,跨應用啓動,推送通知啓動等場景。html

流程

在AS 上其實已經有詳細的使用步驟解析了,這裏給你們普及下android


快速點擊 shift 兩次,輸入 APPLink 便可找到 AS 提供的集成教程。
在 AS 中已經有詳細的使用步驟了,總共分爲 4 步web

add URL intent filters

建立一個 URL shell


或者也能夠點擊 「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-... 的時候看到這個文件(替換成本身的域名),那麼說明服務端的配置是成功的。目前能夠經過 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頁面。

  1. 要使用App Links開發者必須維護一個與app相關聯的網站

對於小的開發者來講這個有點困難,由於他們沒有能力爲app維護一個網站,可是它們仍然但願經過web連接得到流量。

  1. 對 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...

做者:哈哈將

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

相關文章
相關標籤/搜索