code小生 一個專注大前端領域的技術平臺 公衆號回覆 Android 加入安卓技術羣

作者:看書的小蝸牛

鏈接:juejin.im/post/5f17e0515188252e974f0b70

聲明:本文已獲 看書的小蝸牛 授權發表,轉發等請聯繫原作者授權

APP 開發中經常會有這種需求:在瀏覽器或者短信中喚起 APP,如果安裝了就喚起,否則引導下載。對於 Android 而言,這裏主要牽扯的技術就是 deeplink,也可以簡單看成 scheme,Android一直是支持scheme的,但是由於 Android的開源特性,不同手機廠商或者不同瀏覽器廠家處理的千奇百怪,有些能拉起,有些不行,本文只簡單分析下link的原理,包括 deeplink,也包括 Android6.0之後的 AppLink。 其實個人認爲,AppLink就是特殊的deeplink,只不過它多了一種類似於驗證機制,如果驗證通過,就設置默認打開,如果驗證不過,則退化爲deeplink ,如果單從APP端來看,區別主要在 Manifest 文件中的android:autoVerify="true",如下,

APPLINK只是在安裝時候多了一個驗證,其他跟之前deeplink一樣,如果沒聯網,驗證失敗,那就跟之前的deeplink表現一樣

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" />
     <!-- 下面這幾行也必須得設置 -->
     <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實現中,需要APP跟服務端雙向驗證才能讓APPLink生效,如果如果APPLink驗證失敗,APPLink會完全退化成deepLink,這也是爲什麼說APPLINK是一種特殊的deepLink,所以先分析下deepLink,deepLink理解了,APPLink就很容易理解。

deepLink原理分析

deeplink的scheme相應分兩種:一種是隻有一個APP能相應,另一種是有多個APP可以相應,比如,如果爲一個APP的Activity配置了http scheme類型的deepLink,如果通過短信或者其他方式喚起這種link的時候,一般會出現一個讓用戶選擇的彈窗,因爲一般而言,系統會帶個瀏覽器,也相應這類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>

如果是設置了一個私用的,並且沒有跟其他app重複的,那麼會直接打開,比如下面的:

>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" />
     <!-- 下面這幾行也必須得設置 -->
     <category android:name="android.intent.category.DEFAULT" />
     <action android:name="android.intent.action.VIEW" />
     <category android:name="android.intent.category.BROWSABLE" />
</intent-filter>

當然,如果私有scheme跟其他APP的重複了,還是會喚起APP選擇界面(其實是一個ResolverActivity)。下面就來看看scheme是如何匹配並拉起對應APP的。

startActivity入口與ResolverActivity

無論APPLink跟DeepLink其實都是通過喚起一個Activity來實現界面的跳轉,無論從APP外部:比如短信、瀏覽器,還是APP內部。通過在APP內部模擬跳轉來看看具體實現,寫一個H5界面,然後通過Webview加載,不過Webview不進行任何設置,這樣跳轉就需要系統進行解析,走deeplink這一套:

<html>
<body> 
 <a href="https://test.example.com/a/g">Scheme跳轉</a>
</body>
</html>

點擊Scheme跳轉,一般會喚起如下界面,讓用戶選擇打開方式:

如果通過adb打印log,你會發現ActivityManagerService會打印這樣一條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

其實看到的選擇對話框就是 ResolverActivity,不過我們先來看看到底是走到ResolverActivity 的,也就是這個scheme怎麼會喚起 App 選擇界面,在短信中,或者 Webview 中遇到 scheme,他們一般會發出相應的 Intent(當然第三方APP可能會屏蔽掉,比如微信就換不起APP),其實上面的作用跟下面的代碼結果一樣:

   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最後會通過ActivityManagerService調用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;
    //創建新的Intent對象,即便intent被修改也不受影響
    intent = new Intent(intent);
  //收集Intent所指向的Activity信息, 當存在多個可供選擇的Activity,則直接向用戶彈出resolveActivity [見2.7.1]
    ActivityInfo aInfo = resolveActivity(intent, resolvedType, startFlags, profilerInfo, userId);
    ...
    
    }

startActivityMayWait 會通過 resolveActivity 先找到目標 Activity,這個過程中,可能找到多個匹配的 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;
}

所有的四大組件的信息都在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 會通過 queryIntentActivities 找到所有適合的Activity,再通過 chooseBestActivity 提供選擇的權利。這裏分如下三種情況:

  • 僅僅找到一個,直接啓動

  • 找到了多個,並且設置了其中一個爲默認啓動,則直接啓動相應Acitivity

  • 找到了多個,切沒有設置默認啓動,則啓動ResolveActivity供用戶選擇

關於如何查詢,匹配的這裏不詳述,僅僅簡單看看如何喚起選擇頁面,或者默認打開,比較關鍵的就是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();
        }
         
        <!--如果用戶已經選擇過默認打開的APP,則這裏返回的就是相對應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) {
        ...
        <!--弄一個ResolveActivity的ActivityInfo-->
        if (mResolveComponentName.equals(component)) {
            return PackageParser.generateActivityInfo(mResolveActivity, flags,
                    new PackageUserState(), userId);
        }
    }
    return null;
}

其實上述流程比較複雜,這裏只是自己簡單猜想下流程,找到目標Activity後,無論是真的目標Acitiviy,還是ResolveActivity,都會通過startActivityLocked繼續走啓動流程,這裏就會看到之前打印的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 還會根據用戶選擇的信息將一些設置持久化到本地,這樣下次就可以直接啓動用戶的偏好App。其實以上就是 deeplink 的原理,說白了一句話: scheme就是隱式啓動Activity,如果能找到唯一或者設置的目標Acitivity則直接啓動,如果找到多個,則提供APP選擇界面

AppLink原理

一般而言,每個APP都希望被自己制定的scheme喚起,這就是Applink,之前分析deeplink的時候提到了ResolveActivity這麼一個選擇過程,而AppLink就是自動幫用戶完成這個選擇過程,並且選擇的scheme是最適合它的scheme(開發者的角度)。因此對於AppLink要分析的就是如何完成了這個默認選擇的過程。

目前Android源碼提供的是一個雙向認證的方案: 在APP安裝的時候,客戶端根據 APP 配置像服務端請求,如果滿足條件,scheme 跟服務端配置匹配的上,就爲APP設置默認啓動選項 ,所以這個方案很明顯,在安裝的時候需要聯網纔行,否則就是完全不會驗證,那就是普通的deeplink,既然是在安裝的時候去驗證,那就看看PackageManagerService是如何處理這個流程的:

PackageManagerService
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) {
    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發送一個消息開啓驗證,隨後調用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);
    } 
}

可以看出,驗證就三步:檢查、蒐集、驗證。在檢查階段,首先看看是否有設置http/https scheme的Activity,並且是否滿足設置了Intent.ACTION_DEFAULT與Intent.ACTION_VIEW,如果沒有,則壓根不需要驗證,

 * 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);
            // 必須設置Intent.ACTION_VIEW 必須設置有ACTION_DEFAULT 必須要有SCHEME_HTTPS或者SCHEME_HTTP,查到一個就可以
            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;
}

檢查的第二步試看看是否設置了autoverify,當然中間還有些是否設置過,用戶是否選擇過的操作,比較複雜,不分析,不過不影響對流程的理解:

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

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

只要找到一個滿足以上條件的Activity,就開始驗證。如果想要開啓applink,Manifest中配置必須像下面這樣

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

蒐集其實就是蒐集intentfilter信息,下面直接看驗證過程,

@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的實現是通過發送一個廣播來進行驗證的,也就是說,這是個異步的過程,驗證是需要耗時的(網絡請求),所以安裝後,一般要等個幾秒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收到驗證消息後,通過start一個DirectStatementService進行驗證,兜兜轉轉最終調用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會逐一對需要驗證的intentfilter進行驗證,具體是通過DirectStatementRetriever的retrieveStatements來實現:

@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:從名字就能看出,這是獲取服務端Applink的配置,獲取後跟本地校驗,如果通過了,那就是applink啓動成功:

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

其實就是通過 UrlFetcher 獲取服務端配置,然後發給之前的 receiver 進行驗證:

    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需在安裝時聯網纔有效,到這裏其實就可以理解的差不多,後面其實就是針對配置跟App自身的配置進行校驗,如果通過就設置默認啓動,並持久化,驗證成功的話可以通過

adb shell dumpsys package d   

查看結果:

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

驗證後再通過PackageManagerService持久化到設置信息,如此就完成了Applink驗證流程。

Chrome瀏覽器對於自定義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"> . 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中不能通過iframe跳轉自定義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);
}

但是仍然可以通過window.location.href喚起:

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

或者通過跳轉標籤喚起

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

當然,如果自定義了https/http的也是可以的。總的來說Chrome除了Iframe,其他的好像都沒問題。

<a href="https://xxx.com/a/g">  https 跳轉</a>

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

  • 360瀏覽器,可以通過iframe、、 方式調用scheme,除了不支持https/http,其他都支持

  • UC瀏覽器可以通過iframe、、 方式調用scheme(即便如此,也可能被屏蔽(域名)) ,無法通過https/http/intent

  • QQ瀏覽器可以通過iframe、、 、intent 方式調用scheme,(也可能被屏蔽(域名),目前看沒屏蔽) ,但是無法通過https/http

前端需要根據不同的瀏覽器選擇合適的策略。

總結

其實關於applink有幾個比較特殊的點:

  • applink第一它只驗證一次,在安裝的時候,爲什麼不每次啓動動檢測呢?可能是爲了給用戶自己選怎留後門。

  • applink驗證的時候需要聯網,不聯網的方案行嗎?個人理解,不聯網應該也可以,只要在安裝的時候,只本地驗證好了,但是這樣明顯沒有雙向驗證安全,因爲雙向驗證證明了網站跟app是一對一應的,這樣才能保證安全,防止第三方打包篡改。

參考文檔

https://developer.android.com/training/app-links/verify-site-associations

相關文章