Android DeepLink的實現原理

前言

之前我們又是看源碼又是研究動畫,今天分享一個比較簡單的技術點:DeepLink。

DeepLink,深度鏈接技術,主要應用場景是通過Web頁面直接調用Android原生app,並且把需要的參數通過Uri的形式,直接傳遞給app,節省用戶的註冊成本。簡單的介紹DeepLink概念之後,我們看一個實際的例子:

朋友通過京東分享給我一個購物鏈接:

於是我通過微信打開了這條鏈接:

在微信中打開這個網址鏈接,提示我打開京東app,如果我點擊了允許,就會打開我手機中的京東app,並且跳轉到這個商品的詳情頁:

通過這種形式,我可以快速的找到需要查看的商品,並且完成購買相關的操作。是不是非常方便,這就是DeepLink。

正文

這麼流弊的DeepLink是不是非常的難?其實DeepLink的基本實現是簡單到不可思議,他的核心思想實際上是Android的隱式啓動。我們平時的隱式啓動主要是通過Action和Category配合啓動指定類型的Activity:

<activity
      android:name=".SecondActivity"
      android:exported="true">
      <intent-filter>
           <action android:name="com.lzp.deeplinkdemo.SECOND" />
           <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
</activity>
val intent = Intent("com.lzp.deeplinkdemo.SECOND")
intent.addCategory(Intent.CATEGORY_DEFAULT)
startActivity(intent)

除了action和category,還有一種隱式啓動的用法是配置data:

<data
     android:scheme="xxxx"
     android:host="xxxx"
     android:port="xxxx"
     android:path="xxxx"
     android:pathPattern="xxxx"
     android:pathPrefix="xxxx"
     android:mimeType="xxxx"/>

scheme:協議類型,我們可以自定義,一般是項目或公司縮寫,String

host:域名地址,String

port:端口,int。

path:訪問的路徑,String

pathPrefix:訪問的路徑的前綴,String

pathPattern:訪問路徑的匹配格式,相對於path和pathPrefix更爲靈活,String

mimeType:資源類型,例如常見的:video/*, image/png, text/plain。

通過這幾個配置項,我們發現data實際上爲當前的頁面綁定了一個Uri地址,這樣就可以通過Uri直接打開這個Activity。

複習一下Uri的結構:

<scheme> :// <host> : <port> / [ <path> | <pathPrefix> | <pathPattern> ]

示例:https://zhidao.baidu.com/question/2012197558423339788.html

scheme和host不可缺省,否則配置無效;path,pathPrefix,pathPattern一般指定一個就可以了,pathPattern與host不可同時使用;mimeType可以不設置,如果設置了,跳轉的時候必須加上mimeType,否則不能匹配到Activity。

現在我們修改SecondActivity的intent-filer:

<activity
    android:name=".SecondActivity"
    android:exported="true">

        <intent-filter>
            <action android:name="com.lzp.deeplinkdemo.SECOND" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>

        <intent-filter>              

            <data
                android:scheme="lzp"
                android:host="demo"
                android:port="8888"
                android:path="/second"
                android:pathPattern="/second"
                android:pathPrefix="/second"
                android:mimeType="text/plain"/>

        </intent-filter>
</activity>

打開SecondActivity:

val intent = Intent()
intent.setDataAndType(Uri.parse("lzp://demo:8888/second"), "text/plain")
startActivity(intent)

現在在App中已經可以打開頁面了,那麼用web能不能正常打開呢?首先配置MainActivity的intent-filter:

<activity
    android:name=".MainActivity"
    android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>

        <intent-filter>
            <data
                android:scheme="lzp"
                android:host="demo"
                android:path="/main"/>
        </intent-filter>

</activity>

Web需要打開url鏈接,所以我們不需要配置mimeType,

手寫一個簡單的Html頁面:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta http-equiv="Content-Style-Type" content="text/css">
  <title></title>
  <meta name="Generator" content="Cocoa HTML Writer">
  <meta name="CocoaVersion" content="1561.4">
  <style type="text/css">
    p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 17.0px; font: 12.0px 'Songti SC'; color: #000000; -webkit-text-stroke: #000000; min-height: 17.0px}
    span.s1 {font-kerning: none}
  </style>
</head>
<body>
<a href="lzp://demo/main">打開main</a>
</html>

Html頁面添加了一個鏈接,點擊打開lzp://demo/main這個地址。把html導入到手機中,用瀏覽器打開,點擊“打開app”,毫無反應!!!

沒錯,如果只是配置了data,Web還是沒辦法通過url地址打開我們的Activity,那怎麼解決這個問題呢?

/**
     * Activities that can be safely invoked from a browser must support this
     * category.  For example, if the user is viewing a web page or an e-mail
     * and clicks on a link in the text, the Intent generated execute that
     * link will require the BROWSABLE category, so that only activities
     * supporting this category will be considered as possible actions.  By
     * supporting this category, you are promising that there is nothing
     * damaging (without user intervention) that can happen by invoking any
     * matching Intent.
     */
    @SdkConstant(SdkConstantType.INTENT_CATEGORY)
    public static final String CATEGORY_BROWSABLE = "android.intent.category.BROWSABLE";

我們還需要配置:

<category android:name="android.intent.category.BROWSABLE" />

從官方的註釋上寫明:需要瀏覽器打開Activity,需要設置這個分類。例如郵件,只有設置了這個分類的Activity纔會考慮被打開。加上這個配置後,再次點擊看看有沒有效果。

如果你真的親自嘗試了,你會發現還是沒有效果。這個時候我們需要回顧一下action和category的用法:

首先需要嘗試匹配action,action匹配成功了之後,纔會繼續匹配設置的category,所以單獨匹配category是沒有任何效果的。

因爲我們要打開的僅僅是一個頁面,所以我們設置:

/**
     * Activity Action: Display the data to the user.  This is the most common
     * action performed on data -- it is the generic action you can use on
     * a piece of data to get the most reasonable thing to occur.  For example,
     * when used on a contacts entry it will view the entry; when used on a
     * mailto: URI it will bring up a compose window filled with the information
     * supplied by the URI; when used with a tel: URI it will invoke the
     * dialer.
     * <p>Input: {@link #getData} is URI from which to retrieve data.
     * <p>Output: nothing.
     */
    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
    public static final String ACTION_VIEW = "android.intent.action.VIEW";

官方的註釋說明ACTION_VIEW表示展示數據的頁面,系統默認的Action就是ACTION_VIEW。添加上ACTION_VIEW,再次點擊打開app。

還是不行,但是跟之前不同的是,這次出現了啓動app的提示窗口,但是app卻閃退了,看一下崩潰日誌:

09-06 14:35:15.459 1216-3270/? W/IntentResolver: resolveIntent failed: found match, but none with CATEGORY_DEFAULT
09-06 14:35:15.473 26708-26708/? E/AKAD: thread:Thread[main,5,main]
    android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW cat=[android.intent.category.BROWSABLE] dat=lzp://demo/main?id=111 flg=0x10000000 (has extras) }
        at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:1805)
        at android.app.Activity.startActivityIfNeeded(Activity.java:4420)
        at android.app.Activity.startActivityIfNeeded(Activity.java:4367)
        at org.chromium.chrome.browser.externalnav.ExternalNavigationDelegateImpl$3.onClick(ExternalNavigationDelegateImpl.java:239)
        at com.qihoo.browser.dialog.CustomDialog$3.onClick(CustomDialog.java:274)
        at android.view.View.performClick(View.java:5267)
        at android.view.View$PerformClick.run(View.java:21249)
        at android.os.Handler.handleCallback(Handler.java:739)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:148)
        at android.app.ActivityThread.main(ActivityThread.java:5541)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:745)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:635)

日誌上寫的很明確,雖然找到了匹配的頁面,但是沒有設置CATEGORY_DEFAULT。看來Web通過url來打開鏈接,必須要求設置CATEGORY_DEFAULT,添加上後,看一下我們完整的xml配置:

<activity
    android:name=".MainActivity"
            android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>

        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data
                android:scheme="lzp"
                android:host="demo"
                android:path="/main"/>
        </intent-filter>
</activity>

最後看一下效果:

 

那麼如何在通過url給app傳遞參數呢?要實現這個也很簡單,首先我們知道要想給url添加參數,直接在url後拼接key=value就可以了,例如:

http://www.baidu.com/s?wd=android

其中wd=android就是我們要添加的參數,現在假設我們需要爲Activity傳遞一個參數id,我們就可以修改uri爲:

lzp://demo/main?id=111

客戶端接收key爲id的參數的方法:

if (intent != null && intent.data != null) {
       Log.e("lzp", intent.data.getQueryParameter("id"))
}

如果只是接收參數的話,客戶端不需要進行任何修改,但是這裏有一種情況,如果我們Activity必須傳遞id,如果不傳遞id不允許跳轉怎麼辦呢?我們有兩種辦法解決這個問題:

1、在剛纔的if語句增加else判斷,當參數爲空的時候,進行finish操作。

2、通過pathPattern,通過通配符設置必須有參數。

我們看一下第二種的實現方式:

<data
    android:pathPattern="lzp://demo/main?id=*"
    android:scheme="lzp" />

之前已經說過,pathPattern不能和host同時使用,所以我們只能刪除host,pathPattern匹配的是整個Uri,這樣我們還可以指定多個參數。但是AndroidManifest.xml會報錯,我們忽略就可以了

總結

其實DeepLink的實現原理就是這麼簡單,只是我們對於隱式啓動理解的不夠。是不是也想給自己的App加上DeepLink呢?趕快嘗試一下吧~

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章