Android DEPPLINK兵多、APPLink原理簡(jiǎn)析

APP開(kāi)發(fā)中經(jīng)常會(huì)有這種需求:在瀏覽器或者短信中喚起APP尖啡,如果安裝了就喚起,否則引導(dǎo)下載剩膘。對(duì)于Android而言衅斩,這里主要牽扯的技術(shù)就是deeplink,也可以簡(jiǎn)單看成scheme援雇,Android一直是支持scheme的矛渴,但是由于Android的開(kāi)源特性,不同手機(jī)廠商或者不同瀏覽器廠家處理的千奇百怪惫搏,有些能拉起具温,有些不行,本文只簡(jiǎn)單分析下link的原理筐赔,包括deeplink铣猩,也包括Android6.0之后的AppLink。其實(shí)個(gè)人認(rèn)為茴丰,AppLink就是特殊的deeplink达皿,只不過(guò)它多了一種類似于驗(yàn)證機(jī)制,如果驗(yàn)證通過(guò)贿肩,就設(shè)置默認(rèn)打開(kāi)峦椰,如果驗(yàn)證不過(guò),則退化為deeplink汰规,如果單從APP端來(lái)看汤功,區(qū)別主要在Manifest文件中的android:autoVerify="true",如下溜哮,

APPLINK只是在安裝時(shí)候多了一個(gè)驗(yàn)證滔金,其他跟之前deeplink一樣色解,如果沒(méi)聯(lián)網(wǎng),驗(yàn)證失敗餐茵,那就跟之前的deeplink表現(xiàn)一樣

deeplink配置(不限http/https)

<intent-filter>
    <data android:scheme="https" android:host="test.example.com"  />
    <category android:name="android.intent.category.DEFAULT" />
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.BROWSABLE" />
</intent-filter>

 (不限http/https)
 <intent-filter>
        <data android:scheme="example" />
        <!-- 下面這幾行也必須得設(shè)置 -->
        <category android:name="android.intent.category.DEFAULT" />
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
</intent-filter>

applink配置(只能http/https)

<intent-filter android:autoVerify="true">
    <data android:scheme="https" android:host="test.example.com"  />
    <category android:name="android.intent.category.DEFAULT" />
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.BROWSABLE" />
</intent-filter>

在Android原生的APPLink實(shí)現(xiàn)中科阎,需要APP跟服務(wù)端雙向驗(yàn)證才能讓APPLink生效,如果如果APPLink驗(yàn)證失敗忿族,APPLink會(huì)完全退化成deepLink锣笨,這也是為什么說(shuō)APPLINK是一種特殊的deepLink,所以先分析下deepLink肠阱,deepLink理解了票唆,APPLink就很容易理解。

deepLink原理分析

deeplink的scheme相應(yīng)分兩種:一種是只有一個(gè)APP能相應(yīng)屹徘,另一種是有多個(gè)APP可以相應(yīng)走趋,比如,如果為一個(gè)APP的Activity配置了http scheme類型的deepLink噪伊,如果通過(guò)短信或者其他方式喚起這種link的時(shí)候簿煌,一般會(huì)出現(xiàn)一個(gè)讓用戶選擇的彈窗,因?yàn)橐话愣约担到y(tǒng)會(huì)帶個(gè)瀏覽器姨伟,也相應(yīng)這類scheme,比如下面的例子:

>adb shell am start -a android.intent.action.VIEW   -c android.intent.category.BROWSABLE  -d "https://test.example.com/b/g"

<intent-filter>
    <data android:scheme="https" android:host="test.example.com"  />
    <category android:name="android.intent.category.DEFAULT" />
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
image.png

如果是設(shè)置了一個(gè)私用的豆励,并且沒(méi)有跟其他app重復(fù)的夺荒,那么會(huì)直接打開(kāi),比如下面的:

>adb shell am start -a android.intent.action.VIEW   -c android.intent.category.BROWSABLE  -d "example://test.example.com/b/g"

 <intent-filter>
        <data android:scheme="example" />
        <!-- 下面這幾行也必須得設(shè)置 -->
        <category android:name="android.intent.category.DEFAULT" />
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
</intent-filter>

當(dāng)然良蒸,如果私有scheme跟其他APP的重復(fù)了技扼,還是會(huì)喚起APP選擇界面(其實(shí)是一個(gè)ResolverActivity)。下面就來(lái)看看scheme是如何匹配并拉起對(duì)應(yīng)APP的嫩痰。

startActivity入口與ResolverActivity

無(wú)論APPLink跟DeepLink其實(shí)都是通過(guò)喚起一個(gè)Activity來(lái)實(shí)現(xiàn)界面的跳轉(zhuǎn)剿吻,無(wú)論從APP外部:比如短信、瀏覽器串纺,還是APP內(nèi)部丽旅。通過(guò)在APP內(nèi)部模擬跳轉(zhuǎn)來(lái)看看具體實(shí)現(xiàn),寫(xiě)一個(gè)H5界面纺棺,然后通過(guò)Webview加載榄笙,不過(guò)Webview不進(jìn)行任何設(shè)置,這樣跳轉(zhuǎn)就需要系統(tǒng)進(jìn)行解析祷蝌,走deeplink這一套:

<html>
<body> 
    <a >Scheme跳轉(zhuǎn)</a>
</body>
</html>

點(diǎn)擊Scheme跳轉(zhuǎn)办斑,一般會(huì)喚起如下界面,讓用戶選擇打開(kāi)方式:

image.png

如果通過(guò)adb打印log,你會(huì)發(fā)現(xiàn)ActivityManagerService會(huì)打印這樣一條Log:

> 12-04 20:32:04.367   887  9064 I ActivityManager: START u0 {act=android.intent.action.VIEW dat=https://test.example.com/... cmp=android/com.android.internal.app.ResolverActivity (has extras)} from uid 10067 on display 0

其實(shí)看到的選擇對(duì)話框就是ResolverActivity乡翅,不過(guò)我們先來(lái)看看到底是走到ResolverActivity的,也就是這個(gè)scheme怎么會(huì)喚起App選擇界面罪郊,在短信中蠕蚜,或者Webview中遇到scheme,他們一般會(huì)發(fā)出相應(yīng)的Intent(當(dāng)然第三方APP可能會(huì)屏蔽掉悔橄,比如微信就換不起APP)靶累,其實(shí)上面的作用跟下面的代碼結(jié)果一樣:

    val intent = Intent()
    intent.setAction("android.intent.action.VIEW")
    intent.setData(Uri.parse("https://test.example.com/a/g"))
    intent.addCategory("android.intent.category.DEFAULT")
    intent.addCategory("android.intent.category.BROWSABLE")
    startActivity(intent)

那剩下的就是看startActivity,在6.0的源碼中癣疟,startActivity最后會(huì)通過(guò)ActivityManagerService調(diào)用ActivityStatckSupervisor的startActivityMayWait

ActivityStatckSUpervisor

 final int startActivityMayWait(IApplicationThread caller, int callingUid, String callingPackage, Intent intent, String resolvedType, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, IBinder resultTo, String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo, WaitResult outResult, Configuration config, Bundle options, boolean ignoreTargetSecurity, int userId, IActivityContainer iContainer, TaskRecord inTask) {
    ...
    boolean componentSpecified = intent.getComponent() != null;
    //創(chuàng)建新的Intent對(duì)象挣柬,即便intent被修改也不受影響
    intent = new Intent(intent);
     //收集Intent所指向的Activity信息, 當(dāng)存在多個(gè)可供選擇的Activity,則直接向用戶彈出resolveActivity [見(jiàn)2.7.1]
    ActivityInfo aInfo = resolveActivity(intent, resolvedType, startFlags, profilerInfo, userId);
    ...
    
    }

startActivityMayWait會(huì)通過(guò)resolveActivity先找到目標(biāo)Activity,這個(gè)過(guò)程中睛挚,可能找到多個(gè)匹配的Activity邪蛔,這就是ResolverActivity的入口:

  ActivityInfo resolveActivity(Intent intent, String resolvedType, int startFlags,
        ProfilerInfo profilerInfo, int userId) {
    // Collect information about the target of the Intent.
    ActivityInfo aInfo;
    try {
        ResolveInfo rInfo =
            AppGlobals.getPackageManager().resolveIntent(
                    intent, resolvedType,
                    PackageManager.MATCH_DEFAULT_ONLY
                                | ActivityManagerService.STOCK_PM_FLAGS, userId);
        aInfo = rInfo != null ? rInfo.activityInfo : null;
    } catch (RemoteException e) {
        aInfo = null;
    }

可以認(rèn)為,所有的四大組件的信息都在PackageManagerService中有登記扎狱,想要找到這些類侧到,就必須向PackagemanagerService查詢,

PackageManagerService

@Override
public ResolveInfo resolveIntent(Intent intent, String resolvedType,
        int flags, int userId) {
    if (!sUserManager.exists(userId)) return null;
    enforceCrossUserPermission(Binder.getCallingUid(), userId, false, false, "resolve intent");
    List<ResolveInfo> query = queryIntentActivities(intent, resolvedType, flags, userId);
    return chooseBestActivity(intent, resolvedType, flags, query, userId);
}

PackageManagerService會(huì)通過(guò)queryIntentActivities找到所有適合的Activity淤击,再通過(guò)chooseBestActivity提供選擇的權(quán)利匠抗。這里分如下三種情況:

  • 僅僅找到一個(gè),直接啟動(dòng)
  • 找到了多個(gè)污抬,并且設(shè)置了其中一個(gè)為默認(rèn)啟動(dòng)汞贸,則直接啟動(dòng)相應(yīng)Acitivity
  • 找到了多個(gè),切沒(méi)有設(shè)置默認(rèn)啟動(dòng)印机,則啟動(dòng)ResolveActivity供用戶選擇

關(guān)于如何查詢矢腻,匹配的這里不詳述,僅僅簡(jiǎn)單看看如何喚起選擇頁(yè)面耳贬,或者默認(rèn)打開(kāi)踏堡,比較關(guān)鍵的就是chooseBestActivity,

private ResolveInfo chooseBestActivity(Intent intent, String resolvedType,
        int flags, List<ResolveInfo> query, int userId) {
             <!--查詢最好的Activity-->
            ResolveInfo ri = findPreferredActivity(intent, resolvedType,
                    flags, query, r0.priority, true, false, debug, userId);
            if (ri != null) {
                return ri;
            }
            ...
}
        
    ResolveInfo findPreferredActivity(Intent intent, String resolvedType, int flags,
        List<ResolveInfo> query, int priority, boolean always,
        boolean removeMatches, boolean debug, int userId) {
    if (!sUserManager.exists(userId)) return null;
    // writer
    synchronized (mPackages) {
        if (intent.getSelector() != null) {
            intent = intent.getSelector();
        }
         
        <!--如果用戶已經(jīng)選擇過(guò)默認(rèn)打開(kāi)的APP咒劲,則這里返回的就是相對(duì)應(yīng)APP中的Activity-->
        ResolveInfo pri = findPersistentPreferredActivityLP(intent, resolvedType, flags, query,
                debug, userId);
        if (pri != null) {
            return pri;
        }
        <!--找Activity-->
        PreferredIntentResolver pir = mSettings.mPreferredActivities.get(userId);
        ...
                    final ActivityInfo ai = getActivityInfo(pa.mPref.mComponent,
                            flags | PackageManager.GET_DISABLED_COMPONENTS, userId);
        ...
}


@Override
public ActivityInfo getActivityInfo(ComponentName component, int flags, int userId) {
    if (!sUserManager.exists(userId)) return null;
    enforceCrossUserPermission(Binder.getCallingUid(), userId, false, false, "get activity info");
    synchronized (mPackages) {
        ...
        <!--弄一個(gè)ResolveActivity的ActivityInfo-->
        if (mResolveComponentName.equals(component)) {
            return PackageParser.generateActivityInfo(mResolveActivity, flags,
                    new PackageUserState(), userId);
        }
    }
    return null;
}

其實(shí)上述流程比較復(fù)雜顷蟆,這里只是自己簡(jiǎn)單猜想下流程,找到目標(biāo)Activity后腐魂,無(wú)論是真的目標(biāo)Acitiviy帐偎,還是ResolveActivity,都會(huì)通過(guò)startActivityLocked繼續(xù)走啟動(dòng)流程蛔屹,這里就會(huì)看到之前打印的Log信息:

ActivityStatckSUpervisor

final int startActivityLocked(IApplicationThread caller...{
    if (err == ActivityManager.START_SUCCESS) {
        Slog.i(TAG, "START u" + userId + " {" + intent.toShortString(true, true, true, false)
                + "} from uid " + callingUid
                + " on display " + (container == null ? (mFocusedStack == null ?
                        Display.DEFAULT_DISPLAY : mFocusedStack.mDisplayId) :
                        (container.mActivityDisplay == null ? Display.DEFAULT_DISPLAY :
                                container.mActivityDisplay.mDisplayId)));
    }

如果是ResolveActivity還會(huì)根據(jù)用戶選擇的信息將一些設(shè)置持久化到本地削樊,這樣下次就可以直接啟動(dòng)用戶的偏好App。其實(shí)以上就是deeplink的原理,說(shuō)白了一句話:scheme就是隱式啟動(dòng)Activity漫贞,如果能找到唯一或者設(shè)置的目標(biāo)Acitivity則直接啟動(dòng)甸箱,如果找到多個(gè),則提供APP選擇界面迅脐。

AppLink原理

一般而言芍殖,每個(gè)APP都希望被自己制定的scheme喚起,這就是Applink谴蔑,之前分析deeplink的時(shí)候提到了ResolveActivity這么一個(gè)選擇過(guò)程豌骏,而AppLink就是自動(dòng)幫用戶完成這個(gè)選擇過(guò)程,并且選擇的scheme是最適合它的scheme(開(kāi)發(fā)者的角度)隐锭。因此對(duì)于AppLink要分析的就是如何完成了這個(gè)默認(rèn)選擇的過(guò)程窃躲。

目前Android源碼提供的是一個(gè)雙向認(rèn)證的方案:在APP安裝的時(shí)候,客戶端根據(jù)APP配置像服務(wù)端請(qǐng)求钦睡,如果滿足條件蒂窒,scheme跟服務(wù)端配置匹配的上,就為APP設(shè)置默認(rèn)啟動(dòng)選項(xiàng)赎婚,所以這個(gè)方案很明顯刘绣,在安裝的時(shí)候需要聯(lián)網(wǎng)才行,否則就是完全不會(huì)驗(yàn)證挣输,那就是普通的deeplink纬凤,既然是在安裝的時(shí)候去驗(yàn)證,那就看看PackageManagerService是如何處理這個(gè)流程的:

PackageManagerService

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
    final int installFlags = args.installFlags;
    <!--開(kāi)始驗(yàn)證applink-->
    startIntentFilterVerifications(args.user.getIdentifier(), replace, pkg);
    ...
    
    }

private void startIntentFilterVerifications(int userId, boolean replacing,
        PackageParser.Package pkg) {
    if (mIntentFilterVerifierComponent == null) {
        return;
    }

    final int verifierUid = getPackageUid(
            mIntentFilterVerifierComponent.getPackageName(),
            (userId == UserHandle.USER_ALL) ? UserHandle.USER_OWNER : userId);

    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);
}

startIntentFilterVerifications發(fā)送一個(gè)消息開(kāi)啟驗(yàn)證撩嚼,隨后調(diào)用verifyIntentFiltersIfNeeded進(jìn)行驗(yàn)證

 private void verifyIntentFiltersIfNeeded(int userId, int verifierUid, boolean replacing,
        PackageParser.Package pkg) {
        ...
        <!--檢查是否有Activity設(shè)置了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是否設(shè)置autoverify -->
                if (filter.needsVerification() && needsNetworkVerificationLPr(filter)) {
                    needToVerify = true;
                    break;
                }
            }
        }
      <!--如果有搜集需要驗(yàn)證的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++;
                    }    }   } }  }
   <!--開(kāi)始驗(yàn)證-->
    if (count > 0) {
        mIntentFilterVerifier.startVerifications(userId);
    } 
}

可以看出停士,驗(yàn)證就三步:檢查、搜集完丽、驗(yàn)證恋技。在檢查階段,首先看看是否有設(shè)置http/https scheme的Activity逻族,并且是否滿足設(shè)置了Intent.ACTION_DEFAULT與Intent.ACTION_VIEW蜻底,如果沒(méi)有,則壓根不需要驗(yàn)證聘鳞,

 * Check if one of the IntentFilter as both actions DEFAULT / VIEW and a HTTP/HTTPS data URI
 */
private static boolean hasDomainURLs(Package pkg) {
    if (pkg == null || pkg.activities == null) return false;
    final ArrayList<Activity> activities = pkg.activities;
    final int countActivities = activities.size();
    for (int n=0; n<countActivities; n++) {
        Activity activity = activities.get(n);
        ArrayList<ActivityIntentInfo> filters = activity.intents;
        if (filters == null) continue;
        final int countFilters = filters.size();
        for (int m=0; m<countFilters; m++) {
            ActivityIntentInfo aii = filters.get(m);
            // 必須設(shè)置Intent.ACTION_VIEW 必須設(shè)置有ACTION_DEFAULT 必須要有SCHEME_HTTPS或者SCHEME_HTTP薄辅,查到一個(gè)就可以
            if (!aii.hasAction(Intent.ACTION_VIEW)) continue;
            if (!aii.hasAction(Intent.ACTION_DEFAULT)) continue;
            if (aii.hasDataScheme(IntentFilter.SCHEME_HTTP) ||
                    aii.hasDataScheme(IntentFilter.SCHEME_HTTPS)) {
                return true;
            }
        }
    }
    return false;
}

檢查的第二步試看看是否設(shè)置了autoverify,當(dāng)然中間還有些是否設(shè)置過(guò)抠璃,用戶是否選擇過(guò)的操作站楚,比較復(fù)雜,不分析搏嗡,不過(guò)不影響對(duì)流程的理解:

public final boolean needsVerification() {
    return getAutoVerify() && handlesWebUris(true);
}

public final boolean getAutoVerify() {
    return ((mVerifyState & STATE_VERIFY_AUTO) == STATE_VERIFY_AUTO);
}

只要找到一個(gè)滿足以上條件的Activity窿春,就開(kāi)始驗(yàn)證拉一。如果想要開(kāi)啟applink,Manifest中配置必須像下面這樣

    <intent-filter android:autoVerify="true">
        <data android:scheme="https" android:host="xxx.com" />
        <data android:scheme="http" android:host="xxx.com" />
        <!--外部intent打開(kāi)旧乞,比如短信蔚润,文本編輯等-->
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>

搜集其實(shí)就是搜集intentfilter信息,下面直接看驗(yàn)證過(guò)程良蛮,

@Override
    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的實(shí)現(xiàn)是通過(guò)發(fā)送一個(gè)廣播來(lái)進(jìn)行驗(yàn)證的抽碌,也就是說(shuō),這是個(gè)異步的過(guò)程决瞳,驗(yàn)證是需要耗時(shí)的(網(wǎng)絡(luò)請(qǐng)求),所以安裝后左权,一般要等個(gè)幾秒Applink才能生效皮胡,廣播的接受處理者是:IntentFilterVerificationReceiver

public final class IntentFilterVerificationReceiver extends BroadcastReceiver {
    private static final String TAG = IntentFilterVerificationReceiver.class.getSimpleName();
...

    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        if (Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION.equals(action)) {
            Bundle inputExtras = intent.getExtras();
            if (inputExtras != null) {
                Intent serviceIntent = new Intent(context, DirectStatementService.class);
                serviceIntent.setAction(DirectStatementService.CHECK_ALL_ACTION);
               ...
                serviceIntent.putExtras(extras);
                context.startService(serviceIntent);
            }

IntentFilterVerificationReceiver收到驗(yàn)證消息后,通過(guò)start一個(gè)DirectStatementService進(jìn)行驗(yàn)證赏迟,兜兜轉(zhuǎn)轉(zhuǎn)最終調(diào)用IsAssociatedCallable的verifyOneSource屡贺,

private class IsAssociatedCallable implements Callable<Void> {

     ...
    private boolean verifyOneSource(AbstractAsset source, AbstractAssetMatcher target,
            Relation relation) throws AssociationServiceException {
        Result statements = mStatementRetriever.retrieveStatements(source);
        for (Statement statement : statements.getStatements()) {
            if (relation.matches(statement.getRelation())
                    && target.matches(statement.getTarget())) {
                return true;
            }
        }
        return false;
    }

IsAssociatedCallable會(huì)逐一對(duì)需要驗(yàn)證的intentfilter進(jìn)行驗(yàn)證,具體是通過(guò)DirectStatementRetriever的retrieveStatements來(lái)實(shí)現(xiàn):

@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 {
       ..
               }
}

AndroidAppAsset好像是Google的另一套assetlink類的東西锌杀,好像用在APP web登陸信息共享之類的地方 甩栈,不看,直接看retrieveFromWeb:從名字就能看出糕再,這是獲取服務(wù)端Applink的配置量没,獲取后跟本地校驗(yàn),如果通過(guò)了突想,那就是applink啟動(dòng)成功:

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);
        }
        <!--通過(guò)網(wǎng)絡(luò)請(qǐng)求獲取配置-->
        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());
        <!--如果有一對(duì)多的情況殴蹄,或者說(shuō)設(shè)置了“代理”,則循環(huán)獲取配置-->
        for (String delegate : result.getDelegates()) {
            statements.addAll(
                    retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source)
                            .getStatements());
        }
        <!--發(fā)送結(jié)果-->
        return Result.create(statements, webContent.getExpireTimeMillis());
    } catch (JSONException | IOException e) {
        return Result.create(statements, DO_NOT_CACHE_RESULT);
    }
}

其實(shí)就是通過(guò)UrlFetcher獲取服務(wù)端配置猾担,然后發(fā)給之前的receiver進(jìn)行驗(yàn)證:

    public WebContent getWebContentFromUrl(URL url, long fileSizeLimit, int connectionTimeoutMillis)
        throws AssociationServiceException, IOException {
    final String scheme = url.getProtocol().toLowerCase(Locale.US);
    if (!scheme.equals("http") && !scheme.equals("https")) {
        throw new IllegalArgumentException("The url protocol should be on http or https.");
    }

    HttpURLConnection connection = null;
    try {
        connection = (HttpURLConnection) url.openConnection();
        connection.setInstanceFollowRedirects(true);
        connection.setConnectTimeout(connectionTimeoutMillis);
        connection.setReadTimeout(connectionTimeoutMillis);
        connection.setUseCaches(true);
        connection.setInstanceFollowRedirects(false);
        connection.addRequestProperty("Cache-Control", "max-stale=60");
         ...
        return new WebContent(inputStreamToString(
                connection.getInputStream(), connection.getContentLength(), fileSizeLimit),
            expireTimeMillis);
    } 

看到這里的HttpURLConnection就知道為什么Applink需在安裝時(shí)聯(lián)網(wǎng)才有效袭灯,到這里其實(shí)就可以理解的差不多,后面其實(shí)就是針對(duì)配置跟App自身的配置進(jìn)行校驗(yàn)绑嘹,如果通過(guò)就設(shè)置默認(rèn)啟動(dòng)稽荧,并持久化,驗(yàn)證成功的話可以通過(guò)

adb shell dumpsys package d   

查看結(jié)果:

  Package: com.xxx
  Domains: xxxx.com
  Status: always : 200000002

驗(yàn)證后再通過(guò)PackageManagerService持久化到設(shè)置信息工腋,如此就完成了Applink驗(yàn)證流程姨丈。

Chrome瀏覽器對(duì)于自定義scheme的攔截

https://developer.chrome.com/multidevice/android/intents

A little known feature in Android lets you launch apps directly from a web page via an Android Intent. One scenario is launching an app when the user lands on a page, which you can achieve by embedding an iframe in the page with a custom URI-scheme set as the src, as follows: < iframe src="paulsawesomeapp://page1"> </iframe>. This works in the Chrome for Android browser, version 18 and earlier. It also works in the Android browser, of course.

The functionality has changed slightly in Chrome for Android, versions 25 and later. It is no longer possible to launch an Android app by setting an iframe's src attribute. For example, navigating an iframe to a URI with a custom scheme such as paulsawesomeapp:// will not work even if the user has the appropriate app installed. Instead, you should implement a user gesture to launch the app via a custom scheme, or use the “intent:” syntax described in this article.
參考文檔

也就是在chrome中不能通過(guò)iframe跳轉(zhuǎn)自定義scheme喚起APP了,直接被block夷蚊,如下圖:

function userIframJump() {
    var url = 'yanxuan://lab/u.you.com';
    var iframe = document.createElement('iframe');
    iframe.style.width = '100px';
    iframe.style.height = '100px';
    iframe.style.display = 'none';
    iframe.src = url;
    document.body.appendChild(iframe);
    setTimeout(function() {
        iframe.remove();
    }, 1000);
}
image.png

但是仍然可以通過(guò)window.location.href喚起:

function clickAndroid1(){
       window.location.href="yaxxxuan://lab/u.xx.com";
}

或者通過(guò)<a>跳轉(zhuǎn)標(biāo)簽喚起

<a href="yauan://lab/u.you.com">測(cè)試</a>

當(dāng)然构挤,如果自定義了https/http的也是可以的√韫模總的來(lái)說(shuō)Chrome除了Iframe筋现,其他的好像都沒(méi)問(wèn)題傍菇。

<a >  https 跳轉(zhuǎn)</a>

國(guó)內(nèi)亂七八糟的瀏覽器(觀察日期2019-6-11)

  • 360瀏覽器坯汤,可以通過(guò)iframe、<a>、<ref> 方式調(diào)用scheme督暂,除了不支持https/http,其他都支持
  • UC瀏覽器可以通過(guò)iframe赁温、<a>挑势、<ref> 方式調(diào)用scheme(即便如此,也可能被屏蔽(域名)) 申眼,無(wú)法通過(guò)https/http/intent
  • QQ瀏覽器可以通過(guò)iframe瞒津、<a>、<ref> 括尸、intent 方式調(diào)用scheme巷蚪,(也可能被屏蔽(域名),目前看沒(méi)屏蔽) 濒翻,但是無(wú)法通過(guò)https/http

前端需要根據(jù)不同的瀏覽器選擇合適的策略屁柏。

總結(jié)

其實(shí)關(guān)于applink有幾個(gè)比較特殊的點(diǎn):

  • applink第一它只驗(yàn)證一次,在安裝的時(shí)候有送,為什么不每次啟動(dòng)動(dòng)檢測(cè)呢淌喻?可能是為了給用戶自己選怎留后門(mén)。
  • applink驗(yàn)證的時(shí)候需要聯(lián)網(wǎng)雀摘,不聯(lián)網(wǎng)的方案行嗎裸删?個(gè)人理解,不聯(lián)網(wǎng)應(yīng)該也可以届宠,只要在安裝的時(shí)候烁落,只本地驗(yàn)證好了,但是這樣明顯沒(méi)有雙向驗(yàn)證安全豌注,因?yàn)殡p向驗(yàn)證證明了網(wǎng)站跟app是一對(duì)一應(yīng)的伤塌,這樣才能保證安全,防止第三方打包篡改轧铁。

參考文檔

Verify Android App Links

作者:看書(shū)的小蝸牛
Android DEPPLINK及APPLink原理簡(jiǎn)析

僅供參考每聪,歡迎指正

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市齿风,隨后出現(xiàn)的幾起案子药薯,更是在濱河造成了極大的恐慌,老刑警劉巖救斑,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件童本,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡脸候,警方通過(guò)查閱死者的電腦和手機(jī)穷娱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)绑蔫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人泵额,你說(shuō)我怎么就攤上這事配深。” “怎么了嫁盲?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵篓叶,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我羞秤,道長(zhǎng)缸托,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任瘾蛋,我火速辦了婚禮嗦董,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瘦黑。我一直安慰自己,他們只是感情好奇唤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布幸斥。 她就那樣靜靜地躺著,像睡著了一般咬扇。 火紅的嫁衣襯著肌膚如雪甲葬。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天懈贺,我揣著相機(jī)與錄音经窖,去河邊找鬼。 笑死梭灿,一個(gè)胖子當(dāng)著我的面吹牛画侣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播堡妒,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼配乱,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了皮迟?” 一聲冷哼從身側(cè)響起搬泥,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎伏尼,沒(méi)想到半個(gè)月后忿檩,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡爆阶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年燥透,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了沙咏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡兽掰,死狀恐怖芭碍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情孽尽,我是刑警寧澤窖壕,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站杉女,受9級(jí)特大地震影響瞻讽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜熏挎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一速勇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坎拐,春花似錦、人聲如沸哼勇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)积担。三九已至陨晶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間帝璧,已是汗流浹背先誉。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留的烁,地道東北人褐耳。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像撮躁,于是被迫代替她去往敵國(guó)和親漱病。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • APK安裝流程系列文章整體內(nèi)容如下: APK安裝流程詳解0——前言APK安裝流程詳解1——有關(guān)"安裝ing"的實(shí)體...
    隔壁老李頭閱讀 9,025評(píng)論 4 15
  • APK安裝流程系列文章整體內(nèi)容如下: APK安裝流程詳解0——前言APK安裝流程詳解1——有關(guān)"安裝ing"的實(shí)體...
    隔壁老李頭閱讀 14,205評(píng)論 15 59
  • 【Android Activity】 什么是 Activity? 四大組件之一,通常一個(gè)用戶交互界面對(duì)應(yīng)一個(gè) ac...
    Rtia閱讀 3,805評(píng)論 3 18
  • 今天一天除了上課把曼,滿腦子都是宣言計(jì)劃杨帽。感召了四個(gè),還是之前對(duì)課程有興趣的嗤军,結(jié)果都石沉大海注盈,大概唯恐躲我不及。想想完...
    顧鳴芬閱讀 329評(píng)論 0 1
  • 初冬的夜僚饭,在農(nóng)村,萬(wàn)籟俱寂胧砰。耳邊只有母親均勻的熟睡的呼吸聲鳍鸵。 這個(gè)堅(jiān)強(qiáng)的倔強(qiáng)的曾經(jīng)那么干凈利落的老說(shuō)夜晚睡不著的老...
    暖意人生1閱讀 301評(píng)論 4 4