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 層兼容,這使得我們推送服務的準確性也得到了一定的保障。未來,個推也將持續優化消息推送服務,並進一步提高推送的到達率與點擊率,以滿足一些實時性要求更高更復雜的業務場景需求。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章