前言

最近airbnb开源了DeepLinkDispatch项目,DeepLinkDispatch是一个基于注解的链接跳转库,简单了解完其实现后,想起了Facebook今年5月公布的另一个项目AppLink,于是有了这篇文章。

AppLink

与其说AppLink是一个框架,更不如说他是一个规范。当app内嵌WebView遇到自定义的Schema时,app只能简单的将url转交给系统,或直接显示页面无法加载。AppLink规范旨在解决各个平台的app跳转的问题。第三方网页或者app接入applink后,跳转方可以根据AppLink规范进行精确的目标跳转以及数据传输。 引用官方文档中的例子,example.hmtl:

<html>
<head>
    <meta property="al:ios:url" content="applinks://docs" />
    <meta property="al:ios:app_store_id" content="12345" />
    <meta property="al:ios:app_name" content="App Links" />
    <meta property="al:android:url" content="applinks://docs" />
    <meta property="al:android:app_name" content="App Links" />
    <meta property="al:android:class" content="org.applinks.DocsActivity" />
    <meta property="al:android:package" content="org.applinks" />
    <meta property="al:web:url"
          content="http://applinks.org/documentation" />
</head>
<body>
Hello, world!
</body>
</html>

第三方网页在meta中加以上信息,ios或android的webview通过以上信息可以向系统询问第三方app是否安装,进而生成跳转路由。第三方app未安装的情况下,利用al:web:url进行降级跳转。除例子的属性之外,AppLink还提供了一个重要的标签属性:al_applink_data,他的结构:

{
  "target_url": "http://example.com/docs",
  "extras": {
    "myapp_token": "t0kEn"
  },
  "user_agent": "Bolts iOS 1.1",
  "version": "1.0"
}

al_applink_data用于数据传递,以android的intent为例,al:android:package和al:android:class分别标识app的包名以及activity的类名,al:android:url作为intent.setData的参数,al_applink_data的内容放在Bundle数据中。

AppLink使用BoltsFramework工具库中的WebViewAppLinkResolver来进行html的解析,WebViewAppLinkResolver需先从网络中下载到完整的html文本再进行元标签的解析。

AppLink的目标很高大上,然后在国内没什么卵用, 做为Applink的发起者,facebook的app装机量巨大且开放,第三方app愿意进入其生态圈。而国内的app霸主微信,正忙着为自家的各个产品撕逼,封锁竞争对手的app跳转。可想到的场景只剩有多个产品线的几个大厂,利用AppLink进行自家兄弟应用的跳转。

做为小型app的开发者,与其期待facebook的AppLink规范在国内得到普及,不如等待Android M的AppLink可以尽快完善。

DeepLinkDispatch

AppLink无卵用,但我们将AppLink结合其他框架。去年info的架构师大会中,淘宝提到了UI总线的概念,使用跨平台统一的URL来进行Web、Android、IOS的寻址。在android平台上,当本地没有activity来匹配URL时,自动降级成WebView加载。

DeepLinkDispatch是使用url的跳转框架,activity可以通过注解来描述activity的资源定位符,参考一个DeepLinkDispatch的一个示例:

@DeepLink("airbnb://example.com/deepLink/{id}")
public class MainActivity extends AppCompatActivity {
    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (getIntent().getBooleanExtra(DeepLink.IS_DEEP_LINK, false)) {
            showToast("secondactivity");
        }
     }
     ...
}

DeepLink注解表示MainActivity接受airbnb://example.com/deepLink/{id}规则的跳转。 在编译期,所有activity的DeepLinks注解都会被解析生成实际的跳转规则,生成在DeepLinkActivity类中,DeepLinkActivity是一个预先声明在manifest中的不可见activity, DeepLinkActivity.onCreate():

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Loader loader = new com.airbnb.deeplinkdispatch.DeepLinkLoader();
    DeepLinkRegistry registry = new DeepLinkRegistry(loader);
    Uri uri = getIntent().getData();
    String uriString = uri.toString();
    DeepLinkEntry entry = registry.parseUri(uriString);
    ...
      try {
        Class<?> c = entry.getActivityClass();
        Intent intent;
        if (entry.getType() == DeepLinkEntry.Type.CLASS) {
          intent = new Intent(this, c);
        } else {
          Method method = c.getMethod(entry.getMethod(), Context.class);
          intent = (Intent) method.invoke(c, this);
        }
        if (intent.getAction() == null) {
          intent.setAction(getIntent().getAction());
        }
        if (intent.getData() == null) {
          intent.setData(getIntent().getData());
        }
        Bundle parameters;
        if (getIntent().getExtras() != null) {
          parameters = new Bundle(getIntent().getExtras());
        } else {
          parameters = new Bundle();
        }
         ...
        intent.putExtras(parameters);
        intent.putExtra(DeepLink.IS_DEEP_LINK, true);
        startActivity(intent);
        notifyListener(false, uri, null);
      } catch (NoSuchMethodException exception) {
        ...
        } 
      ...
      } finally {
        finish();
      }
}

DeepLinkActivity作为url的统一入口以及路由,定位并启动目标activity,之后立即销毁。

在DeepLinkDispatch下,启动MainActivity的代码就变成了:

  Intent intent = new Intent();
  intent.setData(Uri.parse("airbnb://example.com/deepLink/1"));
  intent.setAction(Intent.ACTION_VIEW);
  startActivity(intent);

回到UI总线的问题,在DeepLinkDispatch的基础上,我们只需要为DeepLinkActivity做一些简单的改造。在DeepLinkActivity中加上activity启动失败后的工作流,即可完成降级跳转webview的逻辑。

App跳转

解决完nativie跳转webview的问题,我们回头看一下facebook的AppLink规范,惊喜的发现它与DeepLinkActivity意外的搭配,AppLink中需要为跳转指定al:android:class,一旦APK的包结构发生了调整,已经发布的网页需做一次重新部署,引入DeepLinkDispatch后,al:android:class的入口统一成了DeepLinkActivity,由DeepLinkDispatch进行二次寻址。额外还带来一个好处,app内嵌webview经常会有拦截自定义url跳转的需求,为了运营活动,不得不为某个url拦截专门发一个版本,采用UI总线的设计,此类需求可以通过动态调整网页AppLink完成。

最后

AppLink以及DeepLinkDispatch各自还是有一些缺陷,比如AppLink的BoltsFramework不能和WebView很好的结合,导致html下载多次。App跳转中若需要携带复杂的数据格式,DeepLinkDispatch的url会变得过于冗长。但UI总线的概念可以为native和webview之间的连接提供一些好的思路。

Comments