Deeplink實踐原理分析

目錄介紹

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

01.先看一個場景

  • 假設一個場景:
    • 小明告訴小楊,一鹿有車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。
  • 而後看看下面截圖
  • 提出的問題
    • 1.如何實現點擊本身的網站跳到咱們的App而不是任意的連接?
    • 2.經過連接跳轉到App中不一樣的頁面,應該怎麼作?某些頁面須要參數,如何攜帶參數?
    • 3.短信中,有時候看到的連接並不是http或者https開頭,短信息是如何識別這是一個連接,而不是一個字符串?具體看上面的短信截圖……
    • 4.出現了一個彈框讓我二次確認(通常是選擇瀏覽器,只要是瀏覽器,都會相應http或者http開頭的shceme,若是你的APP安裝了多個瀏覽器,都會出如今這個彈框的選項中),如何去掉這個噁心的選擇瀏覽器的的彈框?
    • 5.短信息中常見的非http或者https開頭的連接,到底是如何生成的,是怎麼來的?
    • 6.scheme協議跳轉的原理是什麼?微信打開app的原理是什麼?
    • 7.跳轉指定頁面,有的須要傳遞參數,有的參數是url,如何避免被非法篡改?
    • 8.跳轉指定頁面,有的頁面須要登陸才能進入,沒有登陸則先跳轉登陸頁面,登陸了才跳轉指定頁面,這種如何操做?

02.什麼是DeepLink

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

03.什麼是Deferred DeepLink

  • 什麼是Deferred DeepLink
    • 相比DeepLink,它增長了判斷APP是否被安裝,用戶匹配的2個功能;
      • 1.當用戶點擊連接的時候判斷APP是否安裝,若是用戶沒有安裝時,引導用戶跳轉到應用商店下載應用。
      • 2.用戶匹配功能,當用戶點擊連接時和用戶啓動APP時,分別將這兩次用戶Device Fingerprint(設備指紋信息)傳到服務器進行模糊匹配,使用戶下載且啓動APP時,直接打開相應的指定頁面。

04.什麼是AppLink

  • 什麼是AppLink
    • AppLink相對複雜,須要App與Web協做完成系統驗證,但能夠保證直接喚起目標App,無需用戶二次選擇或確認。

05.DeepLink和AppLink核心技術

  • 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之間相互跳轉,實現良好的用戶體驗

06.DeepLink實踐方案

  • 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連接生成短連接
    • 這個很容易,直接找個短連接生成的網站,而後把連接轉化一下就能夠。至於轉化的原理,我暫時也不清楚……

07.AppLink實踐方案

  • 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

09.DeepLink原理分析

  • 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選擇界面。

10.AppLink原理分析

  • 以前分析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.關於個人博客

開源推薦:github.com/yangchong21…

相關文章
相關標籤/搜索