天天看點

Deeplink實踐原理分析

目錄介紹

  • 01.先看一個場景
  • 02.什麼是DeepLink
  • 03.什麼是Deferred DeepLink
  • 04.什麼是AppLink
  • 05.DeepLink和AppLink核心技術
  • 06.DeepLink實踐方案
  • 07.AppLink實踐方案
  • 08.部分問題思考總結
  • 09.DeepLink原理分析
  • 10.AppLink原理分析

  • 假設一個場景:
    • 小明告訴小楊,一鹿有車APP上有一個很有創意的抽獎活動,小新想要參與這個活動
      • 如果小楊已經安裝了APP,他需要找到且打開APP,然後找到相應的活動,共計2步;
      • 如果小楊沒有安裝APP,他需要在應用市場搜尋一鹿有車APP、下載下傳、打開APP且找到相應的活動,共計4步;
    • 關于那些途徑實作
      • 通過短資訊,比如收到脈脈好友資訊,通過短資訊打開app跳轉制定頁面。
      • 通過短資訊,比如收到天貓推薦消息,通過短資訊打開浏覽器,然後通過浏覽器跳轉指定頁面。
      • 通過分享到微信中h5頁面,在微信中打開app(這個需要到微信開放平台做配置,其實是微信——>應用寶——>app指定頁面)。
  • 提出的需求:
    • 在浏覽器或者短信中喚起APP,如果安裝了就喚起,否則引導下載下傳。對于Android而言,這裡主要牽扯的技術就是deeplink,也可以簡單看成scheme,Android一直是支援scheme的,本文隻簡單分析下link的原理,包括deeplink,也包括Android6.0之後的AppLink。
    • 其實,AppLink就是特殊的deeplink,隻不過它多了一種類似于驗證機制,如果驗證通過,就設定預設打開,如果驗證不過,則退化為deeplink,如果單從APP端來看,差別主要在Manifest檔案中的android:autoVerify="true"。
    • 既而,在微信中,也可以作出這樣操作。如果使用者已經安裝app,點選跳轉app則會通過應用寶,打開該應用并且跳轉到相應的頁面。這種也是一種AppLink。
  • 然後看看下面截圖
    • Deeplink實踐原理分析
  • 提出的問題
    • 1.如何實作點選自己的網站跳到我們的App而不是任意的連結?
    • 2.通過連結跳轉到App中不同的頁面,應該怎麼做?某些頁面需要參數,如何攜帶參數?
    • 3.短信中,有時候看到的連結并非http或者https開頭,短資訊是如何識别這是一個連結,而不是一個字元串?具體看上面的短信截圖……
    • 4.出現了一個彈框讓我二次确認(一般是選擇浏覽器,隻要是浏覽器,都會相應http或者http開頭的shceme,如果你的APP安裝了多個浏覽器,都會出現在這個彈框的選項中),如何去掉這個惡心的選擇浏覽器的的彈框?
    • 5.短資訊中常見的非http或者https開頭的連結,究竟是如何生成的,是怎麼來的?
    • 6.scheme協定跳轉的原理是什麼?微信打開app的原理是什麼?
    • 7.跳轉指定頁面,有的需要傳遞參數,有的參數是url,如何避免被非法篡改?
    • 8.跳轉指定頁面,有的頁面需要登入才能進入,沒有登入則先跳轉登入頁面,登入了才跳轉指定頁面,這種如何操作?

  • 什麼是DeepLink
    • 移動端深度連結,簡稱deeplink。這是一種通過uri連結到app特定位置的一種跳轉技術,不單是簡單地通過網頁、app等打開目标app,還能達到利用傳遞辨別跳轉至不同頁面的效果。

  • 什麼是Deferred DeepLink
    • 相比DeepLink,它增加了判斷APP是否被安裝,使用者比對的2個功能;
      • 1.當使用者點選連結的時候判斷APP是否安裝,如果使用者沒有安裝時,引導使用者跳轉到應用商店下載下傳應用。
      • 2.使用者比對功能,當使用者點選連結時和使用者啟動APP時,分别将這兩次使用者Device Fingerprint(裝置指紋資訊)傳到伺服器進行模糊比對,使使用者下載下傳且啟動APP時,直接打開相應的指定頁面。

  • 什麼是AppLink
    • AppLink相對複雜,需要App與Web協作完成系統驗證,但可以保證直接喚起目标App,無需使用者二次選擇或确認。

  • DeepLink和AppLink不同點。下面這個總結很重要!
    不同點 DeepLink AppLink
    Intent scheme 任意 要求http或https
    Intent action 任意Action 要求配置andorid.intent.action.VIEW
    Intent category 任意Category 要求配置android.intent.category.BROWSABLE和android.intent.category.DEFAULT
    連結認證 無需驗證 要求進行Digital Asset Links檔案驗證
    使用者體驗 可能展示一個多選項彈窗或确認彈窗,使用者需要二次選擇或确認 無彈窗,直接由App處理連結
    相容性 所有版本 Android6.0及以上版本
  • DeepLink和AppLink用到的核心技術
    • URL SCHEMES。不論是IOS還是Android。
    • 比如微信:URL Schemes:weixin://dl/moments(打開微信朋友圈)
    • DeepLink與AppLink,本質上都是基于Intent架構,使App能夠識别并處理來自系統或其他App的某種特殊URL,在原生App之間互相跳轉,實作良好的使用者體驗

  • 1.指定scheme跳轉規則,關于scheme的協定規則,這裡不作過多解釋,[scheme]://[host]/[path]?[query]。比如暫時是這樣設定的:yilu://link/?page=main。
  • 2.被喚起方,用戶端需要配置清單檔案activity。關于SchemeActivity注意檢視下面代碼:
    • 為什麼要配置intent-filter,它是針對你跳轉的目标來講的,比如你要去某個朋友的家,就類似于門牌的修飾,他會在門牌上定義上述介紹的那些屬性,友善你定位。當有intent發送過來的時候,就會篩選出符合條件的app來。
    • action.VIEW是打開一個視圖,在Android 系統中點選連結會發送一條action=VIEW的隐式意圖,這個必須配置。
    • category.DEFAULT為預設,category.DEFAULT為設定該元件可以使用浏覽器啟動,這個是關鍵,從浏覽器跳轉,就要通過這個屬性。
    <!--用于DeepLink,html跳到此頁面  scheme_Adr: 'yilu://link/?page=main',-->
    <activity android:name=".activity.link.SchemeActivity"
        android:screenOrientation="portrait">
        <!--Android 接收外部跳轉過濾器-->
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <!-- 協定部配置設定置 ,要在web配置相同的-->
            <!--yilu://link/?page=main-->
            <data
                android:host="link"
                android:scheme="yilu" />
        </intent-filter>
    </activity>               
    • 解析資料的操作
    //解析資料
    @Override
    public void onCreate(Bundle savesInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    
        Intent intent=getIntent();
        String action=intent.getAction();
        Uri data=intent.getData();
    
        //解析data
        String scheme=data.getScheme();
        String host=data.getHost();
        String path=data.getPath();
        int port=data.getPort();
        Set<String> paramKeySet=data.getQueryParameterNames();
        //擷取指定參數值
        String page = uri.getQueryParameter("page");
        
        switch (page) {
            case "main":
                //喚起用戶端,進入首頁
                //https://yc.com?page=main
                Intent intent1 = new Intent(this, MainActivity.class);
                readGoActivity(intent1, this);
                break;
            case "full":
                //喚起用戶端,進入A頁面
                //https://yc.com?page=full
                Intent intent2 = new Intent(this, TestFullActivity.class);
                readGoActivity(intent2, this);
                break;
            case "list":
                //喚起用戶端,進入B頁面,攜帶參數
                //https://yc.com?page=list&id=520
                Intent intent3 = new Intent(this, TestListActivity.class);
                String id = getValueByName(url, "id");
                intent3.putExtra("id",id);
                readGoActivity(intent3, this);
                break;
            default:
                Intent intent = new Intent(this, MainActivity.class);
                readGoActivity(intent, this);
                break;
        }
    }           
  • 3.喚起方也需要操作
    Intent intent=new Intent();
    intent.setData(Uri.parse("yilu://link/?page=main"));
    startActivity(intent);           
  • 4.關于問題疑惑點解決方案
    • 配置了scheme協定,測試可以打開app,但是想跳到具體頁面,攜帶參數,又該如何實作呢?
    • 比如則可以配置:yilu://link/?page=car&id=520,則可以跳轉到汽車詳情頁面,然後傳遞的id參數是520。
  • 5.跳轉頁面後的優化
    • 通過以上規則比對上,你點選跳轉以後,如果使用者結束這個Activity的話,就直接回到桌面了,這個是比較奇怪的。參考一些其他app,發現不管是跳轉指定的幾級頁面,點選傳回是回到首頁,那麼這個是如何做到的呢?代碼如下所示
    public void readGoActivity(Intent intent, Context context) {
        // 如果app 運作中,直接打開頁面,沒有運作中就先打開主界面,在打開
        if (isAppRunning(context, context.getPackageName())) {
            openActivity(intent, context);
        } else {
            //先打開首頁,然後跳轉指定頁面
            reStartActivity(intent, context);
        }
    }
    
    public void openActivity(Intent intent, Context context) {
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }
    
    /**
     * 注意,為何要這樣跳轉,首先需要先跳轉首頁,然後在跳轉到指定頁面,那麼回來的時候始終是首頁Main頁面
     * @param intent                            intent
     * @param context                           上下文
     */
    public void reStartActivity(Intent intent, Context context) {
        Intent[] intents = new Intent[2];
        Intent mainIntent = new Intent(context, MainActivity.class);
        mainIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intents[0] = mainIntent;
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intents[1] = intent;
        context.startActivities(intents);
    }           
  • 6.短資訊竟無法識别scheme協定?
    • 把yilu://link/?page=main以短資訊發送出去,然後在短資訊裡點選連結,發現在短信裡面添加的連結自定義的scheme被認為不是一個scheme……可見終究跳不開的http/https通路。
  • 7.如何将一個http或https連結生成短連結
    • 這個很容易,直接找個短連結生成的網站,然後把連結轉化一下就可以。至于轉化的原理,我暫時也不清楚……

  • 1.Android App Links是一種特殊的Deep Links
    • 它使Android系統能夠直接通過網站位址打開應用程式對應的内容頁面,而不需要使用者選擇使用哪個應用來處理網站位址。
    • 要添加Android App Links到應用中,需要在應用裡定義通過Http(s)位址打開應用的intent filter,并驗證你确實擁有該應用和該網站。如果系統成功驗證到你擁有該網站,那麼系統會直接把URL對應的intent路由到你的應用。
  • 2.和Deep Link對比多些限制條件
    • APP Link 多了許多限制條件,比如scheme必須是http或者https的,但是體驗更好,沒有使用者選擇彈框,(實測下來,原生系統直接喚起來,大部分定制系統會提示是否打開連結,如果使用者确認以後,就直接跳到APP)調起APP之後邏輯都一樣,可以用同樣的方式取資料等。
  • 3.Manifest檔案中添加配置如下
    • 最關鍵的是這個:android:autoVerify="true"。那這個屬性是幹嘛的呢?是為了驗證我們點選的連結和我們的APP是否有關聯。具體如何驗證呢?接着往下看:
    • 當android:autoVerify="true"出現在你任意一個intent filter裡,在Android6.0及以上的系統上安裝應用的時候,會觸發系統對APP裡和URL有關的每一個域名的驗證。驗證過程設計以下步驟:
      • 系統會檢查所有包含以下特征的intent filter:Action為 android.intent.action.VIEW、Category為android.intent.category.BROWSABLE和android.intent.category.DEFAULT、Data scheme為http或https
      • 對于在上述intent filter裡找到的每一個唯一的域名,Android系統會到對應的域名下查找數字資産檔案,位址是: https:// 域名/.well-known/assetlinks.json
      • 隻有當系統為AndroidManifest裡找到的每一個域名找到對應的數字資産檔案,系統才會把你的應用設定為特定連結的預設處理器。
    <activity android:name=".SchemeActivity"
        android:screenOrientation="portrait">
        <!--Android 接收外部跳轉過濾器-->
        <intent-filter android:autoVerify="true">
            <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"/>
            <data android:scheme="https"/>
            <data android:host="yc.com"/>
        </intent-filter>
    </activity>           
  • 4.需要添加驗證操作
    • 為了驗證你對應用和網站的所有權,以下兩個步驟是必須的:
    • 1.在AndroidManifest裡要求系統自動進行App Links的所有權驗證。這個配置會告訴Android系統去驗證你的應用是否屬于在intent filter内指定的URL域名。
    • 2.在以下連結位址裡,放置一個數字資産連結的Json檔案,聲明你的網址和應用之間的關系。需要一個服務端檔案讓APP知道關聯關系,APP,在安裝的時候會去校驗這個檔案,校驗檔案上聲明的應用包名、檔案所在的域名、以及檔案聲明的APP密鑰,是否能和app中的配置比對上,如果比對上了,在點選該域名下的任何連結的時候,都會直接定向到我們的APP。
    • 關于json檔案的内容如下所示:
      • package_name:在build.gradle裡定義的application ID
      • sha256_cert_fingerprints:應用簽名的SHA256指紋資訊。你可以用下面的指令,通過Java keytool來生成指紋資訊:$ keytool -list -v -keystore my-release-key.keystore
      {
          relation: [
              "delegate_permission/common.handle_all_urls"
          ],
          target: {
              namespace: "android_app",
              package_name: "com.yc.video",
              sha256_cert_fingerprints: [
              "4D:8A:27:58:E2:00:2E:0B:E2:46:54:74:7D:3E:F2:27:CE:46:FE:08:8D:CF:F7:34:54:B8:36:6D:7B:32:58:A0"
              ]
          }
      }           
    • json檔案的注意點
      • 這個檔案的格式的content-type必須是application/json
      • 這個檔案隻能放在https的連結中,不管你之前在action中聲明的是http或者https
      • 這個檔案不能有任何重定向,并且必須是以/.well-known/assetlinks.json 字尾結尾
      • 你也可以在這個檔案上聲明多個APP,注意看它的格式,是一個list

  • deeplink的scheme相應分兩種:一種是隻有一個APP能相應,另一種是有多個APP可以相應,比如,如果為一個APP的Activity配置了http scheme類型的deepLink,如果通過短信或者其他方式喚起這種link的時候,一般會出現一個讓使用者選擇的彈窗,因為一般而言,系統會帶個浏覽器,也相應這類scheme。這裡就不舉例子了,因為上面已經已經提到呢。當然,如果私有scheme跟其他APP的重複了,還是會喚起APP選擇界面(其實是一個ResolverActivity)。下面就來看看scheme是如何比對并拉起對應APP的。
  • startActivity入口與ResolverActivity
    • 無論APPLink跟DeepLink其實都是通過喚起一個Activity來實作界面的跳轉,無論從APP外部:比如短信、浏覽器,還是APP内部。通過在APP内部模拟跳轉來看看具體實作,寫一個H5界面,然後通過Webview加載,不過Webview不進行任何設定,這樣跳轉就需要系統進行解析,走deeplink這一套:
    <html>
    <body> 
        <a href="yilu://link/?page=main">立即打開一鹿報價頁面(直接打開)&gt;&gt;</a>
    </body>
    </html>           
  • 點選Scheme跳轉,一般會喚起如下界面,讓使用者選擇打開方式:
    • 通過adb列印log,你會發現ActivityManagerService會列印這樣一條Log:
    ActivityManager: START u0 {act=android.intent.action.VIEW dat=yilu://link/... cmp=android/com.android.internal.app.ResolverActivity (has extras)} from uid 10067 on display 0           
  • 其實看到的選擇對話框就是ResolverActivity
    • 不過我們先來看看到底是走到ResolverActivity的,也就是這個scheme怎麼會喚起App選擇界面,在短信中,或者Webview中遇到scheme,他們一般會發出相應的Intent(當然第三方APP可能會屏蔽掉,比如微信就換不起APP),其實上面的作用跟下面的代碼結果一樣:
    Intent intent = new Intent()
    intent.setAction("android.intent.action.VIEW")
    intent.setData(Uri.parse("https://yc.com/history/520"))
    intent.addCategory("android.intent.category.DEFAULT")
    intent.addCategory("android.intent.category.BROWSABLE")
    startActivity(intent)           
  • 那剩下的就是看startActivity,在源碼中,startActivity最後會通過ActivityManagerService調用ActivityStatckSupervisor的startActivityMayWait
    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 
        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查詢
    @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資訊:
    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選擇界面。

  • 之前分析deeplink的時候提到了ResolveActivity這麼一個選擇過程,而AppLink就是自動幫使用者完成這個選擇過程,并且選擇的scheme是最适合它的scheme(開發者的角度)。是以對于AppLink要分析的就是如何完成了這個預設選擇的過程。
  • 目前Android源碼提供的是一個雙向認證的方案:在APP安裝的時候,用戶端根據APP配置像服務端請求,如果滿足條件,scheme跟服務端配置比對的上,就為APP設定預設啟動選項,是以這個方案很明顯,在安裝的時候需要聯網才行,否則就是完全不會驗證,那就是普通的deeplink,既然是在安裝的時候去驗證,那就看看PackageManagerService是如何處理這個流程的,具體找到installPackageLI方法:
    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);
        
        //重點看這裡,發送了一個handler消息
        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);
    }           
  • 可以看到發送了一個handler消息,那麼消息裡做了什麼呢?看一下startIntentFilterVerifications發送一個消息開啟驗證,随後調用verifyIntentFiltersIfNeeded進行驗證,代碼如下所示:
    • 以看出,驗證就三步:檢查、搜集、驗證。在檢查階段,首先看看是否有設定http/https scheme的Activity,并且是否滿足設定了Intent.ACTION_DEFAULT與Intent.ACTION_VIEW,如果沒有,則壓根不需要驗證
    //零碎代碼,handler接受消息的地方代碼
    case START_INTENT_FILTER_VERIFICATIONS: {
        IFVerificationParams params = (IFVerificationParams) msg.obj;
        verifyIntentFiltersIfNeeded(params.userId, params.verifierUid,
                params.replacing, params.pkg);
        break;
    }
    
    //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);
        } 
    }           
  • 具體看一下hasDomainURLs到底做了什麼?
    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自身的配置進行校驗,如果通過就設定預設啟動,并持久化,驗證成功的話可以通過。

01.關于部落格彙總連結

02.關于我的部落格

開源推薦: https://github.com/yangchong211/YCPhotoCover