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 | "foo://example.com/deepLink/{id}", "foo://example.com/anotherDeepLink"}) ({ |
2 | public class MainActivity extends Activity { |
3 | |
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 | "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 | "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 的代理类,有一个构造方法,传进来的参数是SampleModuleLoader
和LibraryDeepLinkModuleLoader
,为下一步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
的实例,并把SampleModuleLoader
和LibraryDeepLinkModuleLoader
作为构造函数的参数传给该实例。第3节讲了SampleModuleLoader
和LibraryDeepLinkModuleLoader
的作用主要是提取注解@DeepLink
的信息并封装成DeepLinkEntry,所以这里其实是把DeepLinkEntry传递给DeepLinkDelegate。
1 | deepLinkDelegate.dispatchFrom(this); |
执行DeepLinkDelegate
的dispatchFrom
函数,找到和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 非常简便,解放双手,启迪大脑。