Deeplink实践原理分析

作者: 杨充211 | 来源:发表于2019-12-09 18:52 被阅读0次

    目录介绍

    • 01.先看一个场景
    • 02.什么是DeepLink
    • 03.什么是Deferred DeepLink
    • 04.什么是AppLink
    • 05.DeepLink和AppLink核心技术
    • 06.DeepLink实践方案
    • 07.AppLink实践方案
    • 08.部分问题思考总结
    • 09.DeepLink原理分析
    • 10.AppLink原理分析

    01.先看一个场景

    • 假设一个场景:
      • 小明告诉小杨,一鹿有车APP上有一个很有创意的抽奖活动,小新想要参与这个活动
        • 如果小杨已经安装了APP,他需要找到且打开APP,然后找到相应的活动,共计2步;
        • 如果小杨没有安装APP,他需要在应用市场搜索一鹿有车APP、下载、打开APP且找到相应的活动,共计4步;
      • 关于那些途径实现
        • 通过短信息,比如收到脉脉好友信息,通过短信息打开app跳转制定页面。
        • 通过短信息,比如收到天猫推荐消息,通过短信息打开浏览器,然后通过浏览器跳转指定页面。
        • 通过分享到微信中h5页面,在微信中打开app(这个需要到微信开放平台做配置,其实是微信——>应用宝——>app指定页面)。
    • 提出的需求:
      • 在浏览器或者短信中唤起APP,如果安装了就唤起,否则引导下载。对于Android而言,这里主要牵扯的技术就是deeplink,也可以简单看成scheme,Android一直是支持scheme的,本文只简单分析下link的原理,包括deeplink,也包括Android6.0之后的AppLink。
      • 其实,AppLink就是特殊的deeplink,只不过它多了一种类似于验证机制,如果验证通过,就设置默认打开,如果验证不过,则退化为deeplink,如果单从APP端来看,区别主要在Manifest文件中的android:autoVerify="true"。
      • 既而,在微信中,也可以作出这样操作。如果用户已经安装app,点击跳转app则会通过应用宝,打开该应用并且跳转到相应的页面。这种也是一种AppLink。
    • 然后看看下面截图
      • <img src="https://img-blog.csdnimg.cn/20191207141938713.png" width="200" hegiht="113" />
    • 提出的问题
      • 1.如何实现点击自己的网站跳到我们的App而不是任意的链接?
      • 2.通过链接跳转到App中不同的页面,应该怎么做?某些页面需要参数,如何携带参数?
      • 3.短信中,有时候看到的链接并非http或者https开头,短信息是如何识别这是一个链接,而不是一个字符串?具体看上面的短信截图……
      • 4.出现了一个弹框让我二次确认(一般是选择浏览器,只要是浏览器,都会相应http或者http开头的shceme,如果你的APP安装了多个浏览器,都会出现在这个弹框的选项中),如何去掉这个恶心的选择浏览器的的弹框?
      • 5.短信息中常见的非http或者https开头的链接,究竟是如何生成的,是怎么来的?
      • 6.scheme协议跳转的原理是什么?微信打开app的原理是什么?
      • 7.跳转指定页面,有的需要传递参数,有的参数是url,如何避免被非法篡改?
      • 8.跳转指定页面,有的页面需要登录才能进入,没有登录则先跳转登录页面,登录了才跳转指定页面,这种如何操作?

    02.什么是DeepLink

    • 什么是DeepLink
      • 移动端深度链接,简称deeplink。这是一种通过uri链接到app特定位置的一种跳转技术,不单是简单地通过网页、app等打开目标app,还能达到利用传递标识跳转至不同页面的效果。

    03.什么是Deferred DeepLink

    • 什么是Deferred DeepLink
      • 相比DeepLink,它增加了判断APP是否被安装,用户匹配的2个功能;
        • 1.当用户点击链接的时候判断APP是否安装,如果用户没有安装时,引导用户跳转到应用商店下载应用。
        • 2.用户匹配功能,当用户点击链接时和用户启动APP时,分别将这两次用户Device Fingerprint(设备指纹信息)传到服务器进行模糊匹配,使用户下载且启动APP时,直接打开相应的指定页面。

    04.什么是AppLink

    • 什么是AppLink
      • AppLink相对复杂,需要App与Web协作完成系统验证,但可以保证直接唤起目标App,无需用户二次选择或确认。

    05.DeepLink和AppLink核心技术

    • DeepLink和AppLink不同点。下面这个总结很重要!
      不同点 DeepLink AppLink
      Intent scheme 任意 要求http或https
      Intent action 任意Action 要求配置andorid.intent.action.VIEW
      Intent category 任意Category 要求配置android.intent.category.BROWSABLE和android.intent.category.DEFAULT
      链接认证 无需验证 要求进行Digital Asset Links文件验证
      用户体验 可能展示一个多选项弹窗或确认弹窗,用户需要二次选择或确认 无弹窗,直接由App处理链接
      兼容性 所有版本 Android6.0及以上版本
    • DeepLink和AppLink用到的核心技术
      • URL SCHEMES。不论是IOS还是Android。
      • 比如微信:URL Schemes:weixin://dl/moments(打开微信朋友圈)
      • DeepLink与AppLink,本质上都是基于Intent框架,使App能够识别并处理来自系统或其他App的某种特殊URL,在原生App之间相互跳转,实现良好的用户体验

    06.DeepLink实践方案

    • 1.指定scheme跳转规则,关于scheme的协议规则,这里不作过多解释,[scheme]://[host]/[path]?[query]。比如暂时是这样设定的:yilu://link/?page=main
    • 2.被唤起方,客户端需要配置清单文件activity。关于SchemeActivity注意查看下面代码:
      • 为什么要配置intent-filter,它是针对你跳转的目标来讲的,比如你要去某个朋友的家,就类似于门牌的修饰,他会在门牌上定义上述介绍的那些属性,方便你定位。当有intent发送过来的时候,就会筛选出符合条件的app来。
      • action.VIEW是打开一个视图,在Android 系统中点击链接会发送一条action=VIEW的隐式意图,这个必须配置。
      • category.DEFAULT为默认,category.DEFAULT为设置该组件可以使用浏览器启动,这个是关键,从浏览器跳转,就要通过这个属性。
      <!--用于DeepLink,html跳到此页面  scheme_Adr: 'yilu://link/?page=main',-->
      <activity android:name=".activity.link.SchemeActivity"
          android:screenOrientation="portrait">
          <!--Android 接收外部跳转过滤器-->
          <intent-filter>
              <action android:name="android.intent.action.VIEW" />
              <category android:name="android.intent.category.DEFAULT" />
              <category android:name="android.intent.category.BROWSABLE" />
              <!-- 协议部分配置 ,要在web配置相同的-->
              <!--yilu://link/?page=main-->
              <data
                  android:host="link"
                  android:scheme="yilu" />
          </intent-filter>
      </activity>    
      
      • 解析数据的操作
      //解析数据
      @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();
          //获取指定参数值
          String page = uri.getQueryParameter("page");
          
          switch (page) {
              case "main":
                  //唤起客户端,进入首页
                  //https://yc.com?page=main
                  Intent intent1 = new Intent(this, MainActivity.class);
                  readGoActivity(intent1, this);
                  break;
              case "full":
                  //唤起客户端,进入A页面
                  //https://yc.com?page=full
                  Intent intent2 = new Intent(this, TestFullActivity.class);
                  readGoActivity(intent2, this);
                  break;
              case "list":
                  //唤起客户端,进入B页面,携带参数
                  //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.关于问题疑惑点解决方案
      • 配置了scheme协议,测试可以打开app,但是想跳到具体页面,携带参数,又该如何实现呢?
      • 比如则可以配置:yilu://link/?page=car&id=520,则可以跳转到汽车详情页面,然后传递的id参数是520。
    • 5.跳转页面后的优化
      • 通过以上规则匹配上,你点击跳转以后,如果用户结束这个Activity的话,就直接回到桌面了,这个是比较奇怪的。参考一些其他app,发现不管是跳转指定的几级页面,点击返回是回到首页,那么这个是如何做到的呢?代码如下所示
      public void readGoActivity(Intent intent, Context context) {
          // 如果app 运行中,直接打开页面,没有运行中就先打开主界面,在打开
          if (isAppRunning(context, context.getPackageName())) {
              openActivity(intent, context);
          } else {
              //先打开首页,然后跳转指定页面
              reStartActivity(intent, context);
          }
      }
      
      public void openActivity(Intent intent, Context context) {
          intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
          context.startActivity(intent);
      }
      
      /**
       * 注意,为何要这样跳转,首先需要先跳转首页,然后在跳转到指定页面,那么回来的时候始终是首页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.短信息竟无法识别scheme协议?
      • yilu://link/?page=main以短信息发送出去,然后在短信息里点击链接,发现在短信里面添加的链接自定义的scheme被认为不是一个scheme……可见终究跳不开的http/https访问。
    • 7.如何将一个http或https链接生成短链接
      • 这个很容易,直接找个短链接生成的网站,然后把链接转化一下就可以。至于转化的原理,我暂时也不清楚……

    07.AppLink实践方案

    • 1.Android App Links是一种特殊的Deep Links
      • 它使Android系统能够直接通过网站地址打开应用程序对应的内容页面,而不需要用户选择使用哪个应用来处理网站地址。
      • 要添加Android App Links到应用中,需要在应用里定义通过Http(s)地址打开应用的intent filter,并验证你确实拥有该应用和该网站。如果系统成功验证到你拥有该网站,那么系统会直接把URL对应的intent路由到你的应用。
    • 2.和Deep Link对比多些约束条件
      • APP Link 多了许多约束条件,比如scheme必须是http或者https的,但是体验更好,没有用户选择弹框,(实测下来,原生系统直接唤起来,大部分定制系统会提示是否打开链接,如果用户确认以后,就直接跳到APP)调起APP之后逻辑都一样,可以用同样的方式取数据等。
    • 3.Manifest文件中添加配置如下
      • 最关键的是这个:android:autoVerify="true"。那这个属性是干嘛的呢?是为了验证我们点击的链接和我们的APP是否有关联。具体如何验证呢?接着往下看:
      • 当android:autoVerify="true"出现在你任意一个intent filter里,在Android6.0及以上的系统上安装应用的时候,会触发系统对APP里和URL有关的每一个域名的验证。验证过程设计以下步骤:
        • 系统会检查所有包含以下特征的intent filter:Action为 android.intent.action.VIEW、Category为android.intent.category.BROWSABLE和android.intent.category.DEFAULT、Data scheme为http或https
        • 对于在上述intent filter里找到的每一个唯一的域名,Android系统会到对应的域名下查找数字资产文件,地址是:https://域名/.well-known/assetlinks.json
        • 只有当系统为AndroidManifest里找到的每一个域名找到对应的数字资产文件,系统才会把你的应用设置为特定链接的默认处理器。
      <activity android:name=".SchemeActivity"
          android:screenOrientation="portrait">
          <!--Android 接收外部跳转过滤器-->
          <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.需要添加验证操作
      • 为了验证你对应用和网站的所有权,以下两个步骤是必须的:
      • 1.在AndroidManifest里要求系统自动进行App Links的所有权验证。这个配置会告诉Android系统去验证你的应用是否属于在intent filter内指定的URL域名。
      • 2.在以下链接地址里,放置一个数字资产链接的Json文件,声明你的网址和应用之间的关系。需要一个服务端文件让APP知道关联关系,APP,在安装的时候会去校验这个文件,校验文件上声明的应用包名、文件所在的域名、以及文件声明的APP密钥,是否能和app中的配置匹配上,如果匹配上了,在点击该域名下的任何链接的时候,都会直接定向到我们的APP。
      • 关于json文件的内容如下所示:
        • package_name:在build.gradle里定义的application ID
        • sha256_cert_fingerprints:应用签名的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文件的注意点
        • 这个文件的格式的content-type必须是application/json
        • 这个文件只能放在https的链接中,不管你之前在action中声明的是http或者https
        • 这个文件不能有任何重定向,并且必须是以/.well-known/assetlinks.json 后缀结尾
        • 你也可以在这个文件上声明多个APP,注意看它的格式,是一个list

    09.DeepLink原理分析

    • deeplink的scheme相应分两种:一种是只有一个APP能相应,另一种是有多个APP可以相应,比如,如果为一个APP的Activity配置了http scheme类型的deepLink,如果通过短信或者其他方式唤起这种link的时候,一般会出现一个让用户选择的弹窗,因为一般而言,系统会带个浏览器,也相应这类scheme。这里就不举例子了,因为上面已经已经提到呢。当然,如果私有scheme跟其他APP的重复了,还是会唤起APP选择界面(其实是一个ResolverActivity)。下面就来看看scheme是如何匹配并拉起对应APP的。
    • startActivity入口与ResolverActivity
      • 无论APPLink跟DeepLink其实都是通过唤起一个Activity来实现界面的跳转,无论从APP外部:比如短信、浏览器,还是APP内部。通过在APP内部模拟跳转来看看具体实现,写一个H5界面,然后通过Webview加载,不过Webview不进行任何设置,这样跳转就需要系统进行解析,走deeplink这一套:
      <html>
      <body> 
          <a href="yilu://link/?page=main">立即打开一鹿报价页面(直接打开)&gt;&gt;</a>
      </body>
      </html>
      
    • 点击Scheme跳转,一般会唤起如下界面,让用户选择打开方式:
      • 通过adb打印log,你会发现ActivityManagerService会打印这样一条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
      
    • 其实看到的选择对话框就是ResolverActivity
      • 不过我们先来看看到底是走到ResolverActivity的,也就是这个scheme怎么会唤起App选择界面,在短信中,或者Webview中遇到scheme,他们一般会发出相应的Intent(当然第三方APP可能会屏蔽掉,比如微信就换不起APP),其实上面的作用跟下面的代码结果一样:
      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最后会通过ActivityManagerService调用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;
          //创建新的Intent对象,即便intent被修改也不受影响
          intent = new Intent(intent);
           //收集Intent所指向的Activity信息, 当存在多个可供选择的Activity,则直接向用户弹出resolveActivity 
          ActivityInfo aInfo = resolveActivity(intent, resolvedType, startFlags, profilerInfo, userId);
          ...
          
          }
      
    • startActivityMayWait会通过resolveActivity先找到目标Activity,这个过程中,可能找到多个匹配的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;
          }
      
    • 可以认为,所有的四大组件的信息都在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会通过queryIntentActivities找到所有适合的Activity,再通过chooseBestActivity提供选择的权利。这里分如下三种情况:
      • 仅仅找到一个,直接启动
      • 找到了多个,并且设置了其中一个为默认启动,则直接启动相应Acitivity
      • 找到了多个,切没有设置默认启动,则启动ResolveActivity供用户选择
    • 关于如何查询,匹配的这里不详述,仅仅简单看看如何唤起选择页面,或者默认打开,比较关键的就是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();
              }
               
              <!--如果用户已经选择过默认打开的APP,则这里返回的就是相对应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) {
              ...
              <!--弄一个ResolveActivity的ActivityInfo-->
              if (mResolveComponentName.equals(component)) {
                  return PackageParser.generateActivityInfo(mResolveActivity, flags,
                          new PackageUserState(), userId);
              }
          }
          return null;
      }
      
    • 其实上述流程比较复杂,这里只是自己简单猜想下流程,找到目标Activity后,无论是真的目标Acitiviy,还是ResolveActivity,都会通过startActivityLocked继续走启动流程,这里就会看到之前打印的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还会根据用户选择的信息将一些设置持久化到本地,这样下次就可以直接启动用户的偏好App。其实以上就是deeplink的原理,说白了一句话:scheme就是隐式启动Activity,如果能找到唯一或者设置的目标Acitivity则直接启动,如果找到多个,则提供APP选择界面。

    10.AppLink原理分析

    • 之前分析deeplink的时候提到了ResolveActivity这么一个选择过程,而AppLink就是自动帮用户完成这个选择过程,并且选择的scheme是最适合它的scheme(开发者的角度)。因此对于AppLink要分析的就是如何完成了这个默认选择的过程。
    • 目前Android源码提供的是一个双向认证的方案:在APP安装的时候,客户端根据APP配置像服务端请求,如果满足条件,scheme跟服务端配置匹配的上,就为APP设置默认启动选项,所以这个方案很明显,在安装的时候需要联网才行,否则就是完全不会验证,那就是普通的deeplink,既然是在安装的时候去验证,那就看看PackageManagerService是如何处理这个流程的,具体找到installPackageLI方法:
      private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
          final int installFlags = args.installFlags;
          <!--开始验证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);
          
          //重点看这里,发送了一个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);
      }
      
    • 可以看到发送了一个handler消息,那么消息里做了什么呢?看一下startIntentFilterVerifications发送一个消息开启验证,随后调用verifyIntentFiltersIfNeeded进行验证,代码如下所示:
      • 以看出,验证就三步:检查、搜集、验证。在检查阶段,首先看看是否有设置http/https scheme的Activity,并且是否满足设置了Intent.ACTION_DEFAULT与Intent.ACTION_VIEW,如果没有,则压根不需要验证
      //零碎代码,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设置了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是否设置autoverify -->
                      if (filter.needsVerification() && needsNetworkVerificationLPr(filter)) {
                          needToVerify = true;
                          break;
                      }
                  }
              }
            <!--如果有搜集需要验证的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++;
                          }    }   } }  }
         <!--开始验证-->
          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);
                  // 必须设置Intent.ACTION_VIEW 必须设置有ACTION_DEFAULT 必须要有SCHEME_HTTPS或者SCHEME_HTTP,查到一个就可以
                  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;
      }
      
    • 检查的第二步试看看是否设置了autoverify,当然中间还有些是否设置过,用户是否选择过的操作,比较复杂,不分析,不过不影响对流程的理解:
      public final boolean needsVerification() {
          return getAutoVerify() && handlesWebUris(true);
      }
      
      public final boolean getAutoVerify() {
          return ((mVerifyState & STATE_VERIFY_AUTO) == STATE_VERIFY_AUTO);
      }
      
    • 只要找到一个满足以上条件的Activity,就开始验证。如果想要开启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>
      
    • 搜集其实就是搜集intentfilter信息,下面直接看验证过程
      @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的实现是通过发送一个广播来进行验证的,也就是说,这是个异步的过程,验证是需要耗时的(网络请求),所以安装后,一般要等个几秒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收到验证消息后,通过start一个DirectStatementService进行验证,兜兜转转最终调用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会逐一对需要验证的intentfilter进行验证,具体是通过DirectStatementRetriever的retrieveStatements来实现:
      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:从名字就能看出,这是获取服务端Applink的配置,获取后跟本地校验,如果通过了,那就是applink启动成功:
      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);
              }
              <!--通过网络请求获取配置-->
              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());
              <!--如果有一对多的情况,或者说设置了“代理”,则循环获取配置-->
              for (String delegate : result.getDelegates()) {
                  statements.addAll(
                          retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source)
                                  .getStatements());
              }
              <!--发送结果-->
              return Result.create(statements, webContent.getExpireTimeMillis());
          } catch (JSONException | IOException e) {
              return Result.create(statements, DO_NOT_CACHE_RESULT);
          }
      }
      
    • 其实就是通过UrlFetcher获取服务端配置,然后发给之前的receiver进行验证:
      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需在安装时联网才有效,到这里其实就可以理解的差不多,后面其实就是针对配置跟App自身的配置进行校验,如果通过就设置默认启动,并持久化,验证成功的话可以通过。

    01.关于博客汇总链接

    02.关于我的博客

    开源推荐:https://github.com/yangchong211/YCPhotoCover

    相关文章

      网友评论

        本文标题:Deeplink实践原理分析

        本文链接:https://www.haomeiwen.com/subject/yrjigctx.html