Deeplink實(shí)踐原理分析

目錄介紹

  • 01.先看一個(gè)場(chǎng)景
  • 02.什么是DeepLink
  • 03.什么是Deferred DeepLink
  • 04.什么是AppLink
  • 05.DeepLink和AppLink核心技術(shù)
  • 06.DeepLink實(shí)踐方案
  • 07.AppLink實(shí)踐方案
  • 08.部分問題思考總結(jié)
  • 09.DeepLink原理分析
  • 10.AppLink原理分析

01.先看一個(gè)場(chǎng)景

  • 假設(shè)一個(gè)場(chǎng)景:
    • 小明告訴小楊,一鹿有車APP上有一個(gè)很有創(chuàng)意的抽獎(jiǎng)活動(dòng),小新想要參與這個(gè)活動(dòng)
      • 如果小楊已經(jīng)安裝了APP,他需要找到且打開APP,然后找到相應(yīng)的活動(dòng),共計(jì)2步;
      • 如果小楊沒有安裝APP,他需要在應(yīng)用市場(chǎng)搜索一鹿有車APP、下載、打開APP且找到相應(yīng)的活動(dòng),共計(jì)4步;
    • 關(guān)于那些途徑實(shí)現(xiàn)
      • 通過短信息,比如收到脈脈好友信息,通過短信息打開app跳轉(zhuǎn)制定頁面。
      • 通過短信息,比如收到天貓推薦消息,通過短信息打開瀏覽器,然后通過瀏覽器跳轉(zhuǎn)指定頁面。
      • 通過分享到微信中h5頁面,在微信中打開app(這個(gè)需要到微信開放平臺(tái)做配置,其實(shí)是微信——>應(yīng)用寶——>app指定頁面)。
  • 提出的需求:
    • 在瀏覽器或者短信中喚起APP,如果安裝了就喚起,否則引導(dǎo)下載。對(duì)于Android而言,這里主要牽扯的技術(shù)就是deeplink,也可以簡(jiǎn)單看成scheme,Android一直是支持scheme的,本文只簡(jiǎn)單分析下link的原理,包括deeplink,也包括Android6.0之后的AppLink。
    • 其實(shí),AppLink就是特殊的deeplink,只不過它多了一種類似于驗(yàn)證機(jī)制,如果驗(yàn)證通過,就設(shè)置默認(rèn)打開,如果驗(yàn)證不過,則退化為deeplink,如果單從APP端來看,區(qū)別主要在Manifest文件中的android:autoVerify="true"。
    • 既而,在微信中,也可以作出這樣操作。如果用戶已經(jīng)安裝app,點(diǎn)擊跳轉(zhuǎn)app則會(huì)通過應(yīng)用寶,打開該應(yīng)用并且跳轉(zhuǎn)到相應(yīng)的頁面。這種也是一種AppLink。
  • 然后看看下面截圖
    • <img src="https://img-blog.csdnimg.cn/20191207141938713.png" width="200" hegiht="113" />
  • 提出的問題
    • 1.如何實(shí)現(xiàn)點(diǎn)擊自己的網(wǎng)站跳到我們的App而不是任意的鏈接?
    • 2.通過鏈接跳轉(zhuǎn)到App中不同的頁面,應(yīng)該怎么做?某些頁面需要參數(shù),如何攜帶參數(shù)?
    • 3.短信中,有時(shí)候看到的鏈接并非http或者h(yuǎn)ttps開頭,短信息是如何識(shí)別這是一個(gè)鏈接,而不是一個(gè)字符串?具體看上面的短信截圖……
    • 4.出現(xiàn)了一個(gè)彈框讓我二次確認(rèn)(一般是選擇瀏覽器,只要是瀏覽器,都會(huì)相應(yīng)http或者h(yuǎn)ttp開頭的shceme,如果你的APP安裝了多個(gè)瀏覽器,都會(huì)出現(xiàn)在這個(gè)彈框的選項(xiàng)中),如何去掉這個(gè)惡心的選擇瀏覽器的的彈框?
    • 5.短信息中常見的非http或者h(yuǎn)ttps開頭的鏈接,究竟是如何生成的,是怎么來的?
    • 6.scheme協(xié)議跳轉(zhuǎn)的原理是什么?微信打開app的原理是什么?
    • 7.跳轉(zhuǎn)指定頁面,有的需要傳遞參數(shù),有的參數(shù)是url,如何避免被非法篡改?
    • 8.跳轉(zhuǎn)指定頁面,有的頁面需要登錄才能進(jìn)入,沒有登錄則先跳轉(zhuǎn)登錄頁面,登錄了才跳轉(zhuǎn)指定頁面,這種如何操作?

02.什么是DeepLink

  • 什么是DeepLink
    • 移動(dòng)端深度鏈接,簡(jiǎn)稱deeplink。這是一種通過uri鏈接到app特定位置的一種跳轉(zhuǎn)技術(shù),不單是簡(jiǎn)單地通過網(wǎng)頁、app等打開目標(biāo)app,還能達(dá)到利用傳遞標(biāo)識(shí)跳轉(zhuǎn)至不同頁面的效果。

03.什么是Deferred DeepLink

  • 什么是Deferred DeepLink
    • 相比DeepLink,它增加了判斷APP是否被安裝,用戶匹配的2個(gè)功能;
      • 1.當(dāng)用戶點(diǎn)擊鏈接的時(shí)候判斷APP是否安裝,如果用戶沒有安裝時(shí),引導(dǎo)用戶跳轉(zhuǎn)到應(yīng)用商店下載應(yīng)用。
      • 2.用戶匹配功能,當(dāng)用戶點(diǎn)擊鏈接時(shí)和用戶啟動(dòng)APP時(shí),分別將這兩次用戶Device Fingerprint(設(shè)備指紋信息)傳到服務(wù)器進(jìn)行模糊匹配,使用戶下載且啟動(dòng)APP時(shí),直接打開相應(yīng)的指定頁面。

04.什么是AppLink

  • 什么是AppLink
    • AppLink相對(duì)復(fù)雜,需要App與Web協(xié)作完成系統(tǒng)驗(yàn)證,但可以保證直接喚起目標(biāo)App,無需用戶二次選擇或確認(rèn)。

05.DeepLink和AppLink核心技術(shù)

  • DeepLink和AppLink不同點(diǎn)。下面這個(gè)總結(jié)很重要!
    不同點(diǎn) DeepLink AppLink
    Intent scheme 任意 要求http或https
    Intent action 任意Action 要求配置andorid.intent.action.VIEW
    Intent category 任意Category 要求配置android.intent.category.BROWSABLE和android.intent.category.DEFAULT
    鏈接認(rèn)證 無需驗(yàn)證 要求進(jìn)行Digital Asset Links文件驗(yàn)證
    用戶體驗(yàn) 可能展示一個(gè)多選項(xiàng)彈窗或確認(rèn)彈窗,用戶需要二次選擇或確認(rèn) 無彈窗,直接由App處理鏈接
    兼容性 所有版本 Android6.0及以上版本
  • DeepLink和AppLink用到的核心技術(shù)
    • URL SCHEMES。不論是IOS還是Android。
    • 比如微信:URL Schemes:weixin://dl/moments(打開微信朋友圈)
    • DeepLink與AppLink,本質(zhì)上都是基于Intent框架,使App能夠識(shí)別并處理來自系統(tǒng)或其他App的某種特殊URL,在原生App之間相互跳轉(zhuǎn),實(shí)現(xiàn)良好的用戶體驗(yàn)

06.DeepLink實(shí)踐方案

  • 1.指定scheme跳轉(zhuǎn)規(guī)則,關(guān)于scheme的協(xié)議規(guī)則,這里不作過多解釋,[scheme]://[host]/[path]?[query]。比如暫時(shí)是這樣設(shè)定的:yilu://link/?page=main。
  • 2.被喚起方,客戶端需要配置清單文件activity。關(guān)于SchemeActivity注意查看下面代碼:
    • 為什么要配置intent-filter,它是針對(duì)你跳轉(zhuǎn)的目標(biāo)來講的,比如你要去某個(gè)朋友的家,就類似于門牌的修飾,他會(huì)在門牌上定義上述介紹的那些屬性,方便你定位。當(dāng)有intent發(fā)送過來的時(shí)候,就會(huì)篩選出符合條件的app來。
    • action.VIEW是打開一個(gè)視圖,在Android 系統(tǒng)中點(diǎn)擊鏈接會(huì)發(fā)送一條action=VIEW的隱式意圖,這個(gè)必須配置。
    • category.DEFAULT為默認(rèn),category.DEFAULT為設(shè)置該組件可以使用瀏覽器啟動(dòng),這個(gè)是關(guān)鍵,從瀏覽器跳轉(zhuǎn),就要通過這個(gè)屬性。
    <!--用于DeepLink,html跳到此頁面  scheme_Adr: 'yilu://link/?page=main',-->
    <activity android:name=".activity.link.SchemeActivity"
        android:screenOrientation="portrait">
        <!--Android 接收外部跳轉(zhuǎn)過濾器-->
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <!-- 協(xié)議部分配置 ,要在web配置相同的-->
            <!--yilu://link/?page=main-->
            <data
                android:host="link"
                android:scheme="yilu" />
        </intent-filter>
    </activity>    
    
    • 解析數(shù)據(jù)的操作
    //解析數(shù)據(jù)
    @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();
        //獲取指定參數(shù)值
        String page = uri.getQueryParameter("page");
        
        switch (page) {
            case "main":
                //喚起客戶端,進(jìn)入首頁
                //https://yc.com?page=main
                Intent intent1 = new Intent(this, MainActivity.class);
                readGoActivity(intent1, this);
                break;
            case "full":
                //喚起客戶端,進(jìn)入A頁面
                //https://yc.com?page=full
                Intent intent2 = new Intent(this, TestFullActivity.class);
                readGoActivity(intent2, this);
                break;
            case "list":
                //喚起客戶端,進(jìn)入B頁面,攜帶參數(shù)
                //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.關(guān)于問題疑惑點(diǎn)解決方案
    • 配置了scheme協(xié)議,測(cè)試可以打開app,但是想跳到具體頁面,攜帶參數(shù),又該如何實(shí)現(xiàn)呢?
    • 比如則可以配置:yilu://link/?page=car&id=520,則可以跳轉(zhuǎn)到汽車詳情頁面,然后傳遞的id參數(shù)是520。
  • 5.跳轉(zhuǎn)頁面后的優(yōu)化
    • 通過以上規(guī)則匹配上,你點(diǎn)擊跳轉(zhuǎn)以后,如果用戶結(jié)束這個(gè)Activity的話,就直接回到桌面了,這個(gè)是比較奇怪的。參考一些其他app,發(fā)現(xiàn)不管是跳轉(zhuǎn)指定的幾級(jí)頁面,點(diǎn)擊返回是回到首頁,那么這個(gè)是如何做到的呢?代碼如下所示
    public void readGoActivity(Intent intent, Context context) {
        // 如果app 運(yùn)行中,直接打開頁面,沒有運(yùn)行中就先打開主界面,在打開
        if (isAppRunning(context, context.getPackageName())) {
            openActivity(intent, context);
        } else {
            //先打開首頁,然后跳轉(zhuǎn)指定頁面
            reStartActivity(intent, context);
        }
    }
    
    public void openActivity(Intent intent, Context context) {
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }
    
    /**
     * 注意,為何要這樣跳轉(zhuǎn),首先需要先跳轉(zhuǎn)首頁,然后在跳轉(zhuǎn)到指定頁面,那么回來的時(shí)候始終是首頁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.短信息竟無法識(shí)別scheme協(xié)議?
    • yilu://link/?page=main以短信息發(fā)送出去,然后在短信息里點(diǎn)擊鏈接,發(fā)現(xiàn)在短信里面添加的鏈接自定義的scheme被認(rèn)為不是一個(gè)scheme……可見終究跳不開的http/https訪問。
  • 7.如何將一個(gè)http或https鏈接生成短鏈接
    • 這個(gè)很容易,直接找個(gè)短鏈接生成的網(wǎng)站,然后把鏈接轉(zhuǎn)化一下就可以。至于轉(zhuǎn)化的原理,我暫時(shí)也不清楚……

07.AppLink實(shí)踐方案

  • 1.Android App Links是一種特殊的Deep Links
    • 它使Android系統(tǒng)能夠直接通過網(wǎng)站地址打開應(yīng)用程序?qū)?yīng)的內(nèi)容頁面,而不需要用戶選擇使用哪個(gè)應(yīng)用來處理網(wǎng)站地址。
    • 要添加Android App Links到應(yīng)用中,需要在應(yīng)用里定義通過Http(s)地址打開應(yīng)用的intent filter,并驗(yàn)證你確實(shí)擁有該應(yīng)用和該網(wǎng)站。如果系統(tǒng)成功驗(yàn)證到你擁有該網(wǎng)站,那么系統(tǒng)會(huì)直接把URL對(duì)應(yīng)的intent路由到你的應(yīng)用。
  • 2.和Deep Link對(duì)比多些約束條件
    • APP Link 多了許多約束條件,比如scheme必須是http或者h(yuǎn)ttps的,但是體驗(yàn)更好,沒有用戶選擇彈框,(實(shí)測(cè)下來,原生系統(tǒng)直接喚起來,大部分定制系統(tǒng)會(huì)提示是否打開鏈接,如果用戶確認(rèn)以后,就直接跳到APP)調(diào)起APP之后邏輯都一樣,可以用同樣的方式取數(shù)據(jù)等。
  • 3.Manifest文件中添加配置如下
    • 最關(guān)鍵的是這個(gè):android:autoVerify="true"。那這個(gè)屬性是干嘛的呢?是為了驗(yàn)證我們點(diǎn)擊的鏈接和我們的APP是否有關(guān)聯(lián)。具體如何驗(yàn)證呢?接著往下看:
    • 當(dāng)android:autoVerify="true"出現(xiàn)在你任意一個(gè)intent filter里,在Android6.0及以上的系統(tǒng)上安裝應(yīng)用的時(shí)候,會(huì)觸發(fā)系統(tǒng)對(duì)APP里和URL有關(guān)的每一個(gè)域名的驗(yàn)證。驗(yàn)證過程設(shè)計(jì)以下步驟:
      • 系統(tǒng)會(huì)檢查所有包含以下特征的intent filter:Action為 android.intent.action.VIEW、Category為android.intent.category.BROWSABLE和android.intent.category.DEFAULT、Data scheme為http或https
      • 對(duì)于在上述intent filter里找到的每一個(gè)唯一的域名,Android系統(tǒng)會(huì)到對(duì)應(yīng)的域名下查找數(shù)字資產(chǎn)文件,地址是:https://域名/.well-known/assetlinks.json
      • 只有當(dāng)系統(tǒng)為AndroidManifest里找到的每一個(gè)域名找到對(duì)應(yīng)的數(shù)字資產(chǎn)文件,系統(tǒng)才會(huì)把你的應(yīng)用設(shè)置為特定鏈接的默認(rèn)處理器。
    <activity android:name=".SchemeActivity"
        android:screenOrientation="portrait">
        <!--Android 接收外部跳轉(zhuǎn)過濾器-->
        <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.需要添加驗(yàn)證操作
    • 為了驗(yàn)證你對(duì)應(yīng)用和網(wǎng)站的所有權(quán),以下兩個(gè)步驟是必須的:
    • 1.在AndroidManifest里要求系統(tǒng)自動(dòng)進(jìn)行App Links的所有權(quán)驗(yàn)證。這個(gè)配置會(huì)告訴Android系統(tǒng)去驗(yàn)證你的應(yīng)用是否屬于在intent filter內(nèi)指定的URL域名。
    • 2.在以下鏈接地址里,放置一個(gè)數(shù)字資產(chǎn)鏈接的Json文件,聲明你的網(wǎng)址和應(yīng)用之間的關(guān)系。需要一個(gè)服務(wù)端文件讓APP知道關(guān)聯(lián)關(guān)系,APP,在安裝的時(shí)候會(huì)去校驗(yàn)這個(gè)文件,校驗(yàn)文件上聲明的應(yīng)用包名、文件所在的域名、以及文件聲明的APP密鑰,是否能和app中的配置匹配上,如果匹配上了,在點(diǎn)擊該域名下的任何鏈接的時(shí)候,都會(huì)直接定向到我們的APP。
    • 關(guān)于json文件的內(nèi)容如下所示:
      • package_name:在build.gradle里定義的application ID
      • sha256_cert_fingerprints:應(yīng)用簽名的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文件的注意點(diǎn)
      • 這個(gè)文件的格式的content-type必須是application/json
      • 這個(gè)文件只能放在https的鏈接中,不管你之前在action中聲明的是http或者h(yuǎn)ttps
      • 這個(gè)文件不能有任何重定向,并且必須是以/.well-known/assetlinks.json 后綴結(jié)尾
      • 你也可以在這個(gè)文件上聲明多個(gè)APP,注意看它的格式,是一個(gè)list

09.DeepLink原理分析

  • deeplink的scheme相應(yīng)分兩種:一種是只有一個(gè)APP能相應(yīng),另一種是有多個(gè)APP可以相應(yīng),比如,如果為一個(gè)APP的Activity配置了http scheme類型的deepLink,如果通過短信或者其他方式喚起這種link的時(shí)候,一般會(huì)出現(xiàn)一個(gè)讓用戶選擇的彈窗,因?yàn)橐话愣裕到y(tǒng)會(huì)帶個(gè)瀏覽器,也相應(yīng)這類scheme。這里就不舉例子了,因?yàn)樯厦嬉呀?jīng)已經(jīng)提到呢。當(dāng)然,如果私有scheme跟其他APP的重復(fù)了,還是會(huì)喚起APP選擇界面(其實(shí)是一個(gè)ResolverActivity)。下面就來看看scheme是如何匹配并拉起對(duì)應(yīng)APP的。
  • startActivity入口與ResolverActivity
    • 無論APPLink跟DeepLink其實(shí)都是通過喚起一個(gè)Activity來實(shí)現(xiàn)界面的跳轉(zhuǎn),無論從APP外部:比如短信、瀏覽器,還是APP內(nèi)部。通過在APP內(nèi)部模擬跳轉(zhuǎn)來看看具體實(shí)現(xiàn),寫一個(gè)H5界面,然后通過Webview加載,不過Webview不進(jìn)行任何設(shè)置,這樣跳轉(zhuǎn)就需要系統(tǒng)進(jìn)行解析,走deeplink這一套:
    <html>
    <body> 
        <a href="yilu://link/?page=main">立即打開一鹿報(bào)價(jià)頁面(直接打開)&gt;&gt;</a>
    </body>
    </html>
    
  • 點(diǎn)擊Scheme跳轉(zhuǎn),一般會(huì)喚起如下界面,讓用戶選擇打開方式:
    • 通過adb打印log,你會(huì)發(fā)現(xiàn)ActivityManagerService會(huì)打印這樣一條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
    
  • 其實(shí)看到的選擇對(duì)話框就是ResolverActivity
    • 不過我們先來看看到底是走到ResolverActivity的,也就是這個(gè)scheme怎么會(huì)喚起App選擇界面,在短信中,或者Webview中遇到scheme,他們一般會(huì)發(fā)出相應(yīng)的Intent(當(dāng)然第三方APP可能會(huì)屏蔽掉,比如微信就換不起APP),其實(shí)上面的作用跟下面的代碼結(jié)果一樣:
    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最后會(huì)通過ActivityManagerService調(diào)用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;
        //創(chuàng)建新的Intent對(duì)象,即便intent被修改也不受影響
        intent = new Intent(intent);
         //收集Intent所指向的Activity信息, 當(dāng)存在多個(gè)可供選擇的Activity,則直接向用戶彈出resolveActivity 
        ActivityInfo aInfo = resolveActivity(intent, resolvedType, startFlags, profilerInfo, userId);
        ...
        
        }
    
  • startActivityMayWait會(huì)通過resolveActivity先找到目標(biāo)Activity,這個(gè)過程中,可能找到多個(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查詢
    @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ì)通過queryIntentActivities找到所有適合的Activity,再通過chooseBestActivity提供選擇的權(quán)利。這里分如下三種情況:
    • 僅僅找到一個(gè),直接啟動(dòng)
    • 找到了多個(gè),并且設(shè)置了其中一個(gè)為默認(rèn)啟動(dòng),則直接啟動(dòng)相應(yīng)Acitivity
    • 找到了多個(gè),切沒有設(shè)置默認(rèn)啟動(dòng),則啟動(dòng)ResolveActivity供用戶選擇
  • 關(guān)于如何查詢,匹配的這里不詳述,僅僅簡(jiǎn)單看看如何喚起選擇頁面,或者默認(rèn)打開,比較關(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)選擇過默認(rèn)打開的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后,無論是真的目標(biāo)Acitiviy,還是ResolveActivity,都會(huì)通過startActivityLocked繼續(xù)走啟動(dòng)流程,這里就會(huì)看到之前打印的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還會(huì)根據(jù)用戶選擇的信息將一些設(shè)置持久化到本地,這樣下次就可以直接啟動(dòng)用戶的偏好App。其實(shí)以上就是deeplink的原理,說白了一句話:scheme就是隱式啟動(dòng)Activity,如果能找到唯一或者設(shè)置的目標(biāo)Acitivity則直接啟動(dòng),如果找到多個(gè),則提供APP選擇界面。

10.AppLink原理分析

  • 之前分析deeplink的時(shí)候提到了ResolveActivity這么一個(gè)選擇過程,而AppLink就是自動(dòng)幫用戶完成這個(gè)選擇過程,并且選擇的scheme是最適合它的scheme(開發(fā)者的角度)。因此對(duì)于AppLink要分析的就是如何完成了這個(gè)默認(rèn)選擇的過程。
  • 目前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è)流程的,具體找到installPackageLI方法:
    private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
        final int installFlags = args.installFlags;
        <!--開始驗(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);
        
        //重點(diǎn)看這里,發(fā)送了一個(gè)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);
    }
    
  • 可以看到發(fā)送了一個(gè)handler消息,那么消息里做了什么呢?看一下startIntentFilterVerifications發(fā)送一個(gè)消息開啟驗(yàn)證,隨后調(diào)用verifyIntentFiltersIfNeeded進(jìn)行驗(yàn)證,代碼如下所示:
    • 以看出,驗(yàn)證就三步:檢查、搜集、驗(yàn)證。在檢查階段,首先看看是否有設(shè)置http/https scheme的Activity,并且是否滿足設(shè)置了Intent.ACTION_DEFAULT與Intent.ACTION_VIEW,如果沒有,則壓根不需要驗(yàn)證
    //零碎代碼,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設(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++;
                        }    }   } }  }
       <!--開始驗(yàn)證-->
        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);
                // 必須設(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è)置過,用戶是否選擇過的操作,比較復(fù)雜,不分析,不過不影響對(duì)流程的理解:
    public final boolean needsVerification() {
        return getAutoVerify() && handlesWebUris(true);
    }
    
    public final boolean getAutoVerify() {
        return ((mVerifyState & STATE_VERIFY_AUTO) == STATE_VERIFY_AUTO);
    }
    
  • 只要找到一個(gè)滿足以上條件的Activity,就開始驗(yàn)證。如果想要開啟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>
    
  • 搜集其實(shí)就是搜集intentfilter信息,下面直接看驗(yàn)證過程
    @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)是通過發(fā)送一個(gè)廣播來進(jìn)行驗(yàn)證的,也就是說,這是個(gè)異步的過程,驗(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)證消息后,通過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)證,具體是通過DirectStatementRetriever的retrieveStatements來實(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),如果通過了,那就是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);
            }
            <!--通過網(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ì)多的情況,或者說設(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í)就是通過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),如果通過就設(shè)置默認(rèn)啟動(dòng),并持久化,驗(yàn)證成功的話可以通過。

01.關(guān)于博客匯總鏈接

02.關(guān)于我的博客

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容