Android deeplink原理解析

一、什么是DeepLink

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

二、原理分析

deeplink的scheme相应分两种:一种是只有一个APP能相应,另一种是有多个APP可以相应,比如,如果为一个APP的Activity配置了http scheme类型的deepLink,如果通过短信或者其他方式唤起这种link的时候,一般会出现一个让用户选择的弹窗,因为一般而言,系统会带个浏览器,也相应这类scheme。当然,如果私有scheme跟其他APP的重复了,还是会唤起APP选择界面。下面就来看看scheme是如何匹配并拉起对应APP的。

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选择界面。

上一篇:一文带你做一个震动APP,7年老Android一次操蛋的面试经历


下一篇:记油耗App