DeepLinkDispatch源码分析

     随着android系统里app之间的联通性越来越强,很多产品意识到增加app的入口能获得更多的流量,比如你在刷FaceBook,看到一条广告显示Tubi新上了电影Barnyard,点击广告链接就能打开tubi app,进到电影的详情页。或者为了在移动端给用户更好的体验,一些产品会把手机web里的流量导到app端。比如打开手机浏览器,输入一个问题 “我为什么这么帅?” 搜索结果前几条有知乎的一个高票回答,点开这条结果会离开浏览器进到知乎app。这些操作的幕后就是deep link在工作,它根据链接进来的uri,决定打开哪个app,进到哪个页面。
开发者怎么处理deep link的情况呢?可能事情是这样的,初期公司只在FB和Twitter上投了广告,要求打开详情页和类别页,uri格式也就三五种,写个if-else语句能搞定,后来公司又在Ins上投了一批广告,还要打开播放页,再后来除了手机端, 电视端也要用deep link,这一下uri的格式就有了七八十来种,再加if-else代码就是一团乱麻。

     Airbnb的开源项目DeepLinkDispatch给出了批量管理deepLink url的很好方案,利用注解配置 deepLink url,看起来省心,写起来省力。很值得学习和借鉴,继续往下看之前建议先花20分钟时间看明白这个框架怎么用的,再揣测下作者的意图和框架背后可能的工作原理,下面就根据源码看这个框架是怎么工作的。

如果做一个简单的概括,可以把它的工作流程分成四个步骤:

     1.项目清单文件里注册activity,接收DeepLink的uri;
     2.给要接收deep link 的类或者方法添加注解;
     3.项目编译期提取并保存注解信息;
     4.接收到Deep link后, 根据uri匹配并且调用对应的类或者方法。

结合代码一步步分析这四个步骤:

1.项目清单文件里注册activity,接收DeepLink的uri

     无论哪个Android 项目要想接收deepLink,都要有这个配置,它的作用是告诉android系统收到某种格式的uri后,请开启Activity。例如源码里DeepLinkActivity在签单文件中的配置:

1
2
<activity
3
  android:name="com.airbnb.deeplinkdispatch.sample.DeepLinkActivity"
4
  android:theme="@android:style/Theme.NoDisplay">
5
  
6
  <intent-filter> 
7
    <action android:name="android.intent.action.VIEW" />
8
    <category android:name="android.intent.category.DEFAULT" />
9
    <category android:name="android.intent.category.BROWSABLE" />
10
    <data android:scheme="http" android:host="example.com" />
11
  </intent-filter>   
12
 
13
 <intent-filter> 
14
   <action android:name="android.intent.action.VIEW" />
15
   <category android:name="android.intent.category.DEFAULT" />
16
   <category android:name="android.intent.category.BROWSABLE" />
17
   <data android:scheme="dld" />
18
 </intent-filter>   
19
 
20
 <intent-filter> 
21
   <action android:name="android.intent.action.VIEW" />
22
   <category android:name="android.intent.category.DEFAULT" />
23
   <category android:name="android.intent.category.BROWSABLE" />
24
   <data android:scheme="http"/>
25
   <data android:scheme="https"/>
26
   <data android:host="airbnb.com"/>
27
 </intent-filter>   
28
 
29
 <intent-filter> 
30
   <action android:name="android.intent.action.VIEW" />
31
   <category android:name="android.intent.category.DEFAULT" />
32
   <category android:name="android.intent.category.BROWSABLE" />
33
   <data android:scheme="app" android:host="airbnb"/>
34
 </intent-filter> 
35
 
36
</activity>

声明了它接收的deep link uri 有下面几种格式:

1
http://example.com
2
dld://
3
http://airbnb.com
4
https://airbnb.com
5
app://airbnb

2.给要接收deep link 的类或者方法添加注解
     deep link dispatch库使用注解 @DeepLink 把 link 分发给对应的类(Activity) 或者函数(Method)

activity 接收deep link 的配置如下:

1
@DeepLink({"foo://example.com/deepLink/{id}", "foo://example.com/anotherDeepLink"})
2
public class MainActivity extends Activity {
3
  @Override 
4
  protected void onCreate(Bundle savedInstanceState) {
5
    super.onCreate(savedInstanceState);
6
    Intent intent = getIntent();
7
    if (intent.getBooleanExtra(DeepLink.IS_DEEP_LINK, false)) {
8
           Bundle parameters = intent.getExtras();
9
          String idString = parameters.getString("id");
10
       // Do something with idString
11
    }
12
  }
13
}

如果使用method 接收 deep link,需要满足三个条件:
     1.方法类型必须是公共静态(public static)的;
     2.方法必须返回Intent 类型;
     3.方法的签名只能有两种情况:Context , 或者Context 和 Bundle

方法签名只有一个参数 Context的情况

1
@DeepLink("foo://example.com/methodDeepLink/{param1}")
2
public static Intent intentForDeepLinkMethod(Context context) {
3
  return new Intent(context, MainActivity.class)
4
      .setAction(ACTION_DEEP_LINK_METHOD);
5
}

如果要获取Intent的Extra里的参数,需要在方法签名里添加另一个参数 Bundle:

1
@DeepLink("foo://example.com/methodDeepLink/{param1}")
2
public static Intent intentForDeepLinkMethod(Context context, Bundle extras) {
3
  Uri.Builder uri = Uri.parse(extras.getString(DeepLink.URI)).buildUpon();
4
  return new Intent(context, MainActivity.class)
5
      .setData(uri.appendQueryParameter("bar", "baz").build())
6
      .setAction(ACTION_DEEP_LINK_METHOD);
7
}

3.项目编译期提取并保存注解信息
     这一步是承上启下的操作,只有提取了注解标注的信息,才能分发deep link uri到正确的类或者方法。
那么注解是怎么提取的?
DeepLinkProcessor这一节里起到了核心的作用,因为它在项目编译期生成辅助类和代码,把所有被注解@DeepLink 标注的信息封装成DeepLinkEntity,还生成其它类为下一步做铺垫。你可能会继续问:
怎么生成辅助代码?
生成了哪些类,要他们做什么?
下文一一解答:

3.1怎么生成辅助代码
     借助javax.annotation.processing 包下的AbstractProcessor类,可以按给定模版生成java代码,刚开始接触这个类觉得它在java世界里算是一个神奇的Api,能在编译期获取信息并生成代码,给程序增加很多可能性,很多优秀的注解类开源项目也是以它为依托。

3.2 生成了哪些类
DeepLinkProcessor里有两个方法负责生成代码:

1
private void generateDeepLinkLoader(String packageName, String className,
2
    List<DeepLinkAnnotatedElement> elements)
3
4
private void generateDeepLinkDelegate(String packageName, List<TypeElement> loaderClasses)

被注解@DeepLinkModule标注的类生成相应的Loader类,
     例如源码里的class SampleModule 被添加了注解@DeepLinkModule,编译期生成类 SampleModuleLoader。以SampleModuleLoader为例,它提取了sample module里所有被注解@DeepLink标注的方法或者类的相关信息,封装成DeepLinkEntry,存进list 集合 REGISTRY中,以便在第4节分发deep link时查找这些信息。

DeepLinkEntry都保存了哪些信息?
代码里找答案:

1
new DeepLinkEntry("http://example.com/deepLink/{id}/{name}/{place}", DeepLinkEntry.Type.METHOD, MainActivity.class, "intentForTaskStackBuilderMethods")
2
3
new DeepLinkEntry("http://airbnb.com/user/{id}", DeepLinkEntry.Type.CLASS, CustomPrefixesActivity.class, null)

     第2节里提到,注解 @DeepLink可以添加到类或者函数上面, 相应的DeepLinkEntry按照Type也可以分为两种。应用在类上面的注解提取的DeepLinkEntry包含匹配deep link uri的字符串,DeepLinkEntry类型(CLASS 或者METHOD),和类的Class。应用在函数上面的注解提取的DeepLinkEntry会包含匹配deep link uri的字符串,DeepLinkEntry类型,方法所在类的Class,和方法名。这些信息会在第4节分发deep link 的时候用到

DeepLinkDelegate
     使用到注解@DeepLinkHandler的地方会生成类 DeepLinkDelegate,从名字可以看出它是处理 deep link 的代理类,有一个构造方法,传进来的参数是SampleModuleLoaderLibraryDeepLinkModuleLoader,为下一步deep link的分发做准备。

4. 接收到Deep link后, 根据uri匹配并且调用对应的类或者方法
用adb命令模拟一个deep link的场景:

1
am start -W -a android.intent.action.VIEW -d "dld://methodDeepLink/abc123" com.airbnb.deeplinkdispatch.sample

android 系统会解析这条命令包含的uri dld://methodDeepLink/abc123, 查询到清淡文件里注册的类 DeepLinkActivity声明的uri格式和它匹配,就会调起 DeepLinkActivity 并且把参数封装在Intent里一起传递过去。

DeepLinkActivity被调用后,在onCreate()函数中做了两部操作:

1
DeepLinkDelegate deepLinkDelegate = new DeepLinkDelegate(
2
    new SampleModuleLoader(), new LibraryDeepLinkModuleLoader());

     创建DeepLinkDelegate的实例,并把SampleModuleLoaderLibraryDeepLinkModuleLoader作为构造函数的参数传给该实例。第3节讲了SampleModuleLoaderLibraryDeepLinkModuleLoader的作用主要是提取注解@DeepLink的信息并封装成DeepLinkEntry,所以这里其实是把DeepLinkEntry传递给DeepLinkDelegate。

1
deepLinkDelegate.dispatchFrom(this);

     执行DeepLinkDelegatedispatchFrom函数,找到和deep link uri匹配的类或者函数,并用反射调用该类或者函数。
DeepLink的分发逻辑就在函数dispatchFrom()中。可以把之前的注册uri, 添加注解,编译期生成代码比作撒网的操作,这一步就算是收网操作了。为了直观展示主流程,剔除了一些错误校验和细节代码后的关键代码如下:

1
public class BaseDeepLinkDelegate {
2
  //构造方法传进来的SampleModuleLoader和LibraryDeepLinkModuleLoader
3
  protected final List<? extends Parser> loaders;
4
5
  public DeepLinkResult dispatchFrom(Activity activity, Intent sourceIntent) {
6
    //...省略细节代码
7
    
8
    Uri uri = sourceIntent.getData();
9
    String uriString = uri.toString();
10
    DeepLinkEntry entry = findEntry(uriString);
11
12
    Class<?> c = entry.getActivityClass();
13
    Intent newIntent;
14
    Bundle parameters = new Bundle();
15
16
    //...省略获取deep link uri 参数的步骤
17
18
    if (entry.getType() == DeepLinkEntry.Type.CLASS) {
19
     //deep link 分发给entry对应的activity
20
      newIntent = new Intent(activity, c);
21
    } else {
22
      Method method;
23
      try{
24
        //尝试获取签名只有一个参数Context的方法
25
        method = c.getMethod(entry.getMethod(), Context.class);
26
        
27
        //调用一个参数签名的方法
28
        newIntent = (Intent) method.invoke(c, activity);
29
      } catch (NoSuchMethodException exception) {
30
        //调用一个签名参数的方法失败时,尝试获取两个参数签名的方法,参数分别是Context和Bundle
31
        method = c.getMethod(entry.getMethod(), Context.class, Bundle.class);
32
33
        //调用两个参数签名的方法
34
        newIntent = (Intent) method.invoke(c, activity, parameters);
35
      }
36
      
37
      activity.startActivity(newIntent);
38
    }
39
    
40
    //...省略细节代码
41
  }
42
43
  private DeepLinkEntry findEntry(String uriString) {
44
    for (Parser loader : loaders) {
45
      DeepLinkEntry entry = loader.parseUri(uriString);
46
      if (entry != null) {
47
        return entry;
48
      }
49
    }
50
    return null;
51
  }
52
53
}

     其中 findEntry的方法是一个非常巧妙的设计,代码跟进去会发现它利用正则表达式匹配uri,并且提取除了uri里包含的参数,详情可以在类DeepLinkEntry里找到。

     同时吐槽下源码里的一个操作:调用一个参数签名的方法失败后,后续在try catch 代码块里调用了两个参数签名的方法,这种操作不够优雅,如果出现了有三个参数签名的方法该怎么办,再增加一个try catch 代码块吗。Bob大叔在《Clean Code》 里介绍一些反面的编程习惯时提到了这点,应该避免在try catch代码块里做一些逻辑操作。如果把所有的方法签名参数封装成一个数组,使用起来就灵活很多,能很方便的支持方法签名有多个的情况。

     deep link从创建到分发的过程讲到这就结束了,如果把开篇提到的4个步骤进一步压缩,介绍DeepLinkDispatcher的工作原理就是:
代码里配置deep link注解,编译期提取注解信息并封装成DeepLinkEntry, deep link到来后根据uri找到匹配的DeepLinkEntry, 反射调用对应的类或者函数。

     除了支撑框架正常工作的基本注解外,DeepLinkDispatcher还提供了可以避免重复配置uri schema的注解@DeepLinkSpec,它是一个只能用在注解上的注解,想法和实现思路非常值得学习。

原理和流程梳理完以后,说下DeepLinkDispatcher库的使用感受:
可以提高的地方:一个方面不够灵活,给方法添加注解时,要求方法必须是静态且返回Intent类型,实际项目里,会有很多接收到deep link后,异步请求服务端数据,根据服务端返回数据决定跳转页面的情况,这种情况下DeepLinkDispatche就爱莫能助了;
受益匪浅的地方:利用注解管理 deep link,代码直观又清晰,后期扩展和更改deep link 非常简便,解放双手,启迪大脑。