applink_facebook applink 跳转到应用参数携带问题 - CSDN
精华内容
参与话题
  • openinstall将这个功能免费开放出来了,与腾讯的applink相比仅在iOS环境下小有差异。 腾讯的applink服务对使用其的App有一些硬性要求,部分中小型App较难达到这些标准,现在openinstall免费开放了这个功能,对于...

    openinstall将这个功能免费开放出来了,与腾讯的applink相比仅在iOS环境下小有差异。

    腾讯的applink服务对使用其的App有一些硬性要求,部分中小型App较难达到这些标准,现在openinstall免费开放了这个功能,对于移动开发者来说无疑是个好消息。腾讯的applink无疑品质是很好的,并且很多分享链接也是在微信、QQ平台上传播,但用applink无疑是很好的选择,但在大家尚未达到使用腾讯applink标准之前,使用openinstall的免费服务也是一个不错的选择,openinstall的服务品质也是很好和非常稳定的。

    openinstall不仅实现了在微信中唤醒App,还适配了大量社交平台,如钉钉、支付宝、QQ,同时也适配了大量浏览器,能够在社交分享和浏览器链接中流畅拉起(唤醒)App并直达App内指定场景(指定页面),对于用户体验的提升,有很大帮助。

    这个服务过去openinstall是收费的,目前已经开放给移动开发者免费使用了

     

    openinstall网址:https://www.openinstall.io
    --------------------- 
     

    展开全文
  • applink,一种基于android方案的简单路由器.zip
  • code小生 一个专注大前端领域的技术平台公众号回复Android加入安卓技术群作者:看书的小蜗牛链接:juejin.im/post/5f17e0515188252e974f0b70声明...

    code小生 一个专注大前端领域的技术平台公众号回复Android加入安卓技术群

    作者:看书的小蜗牛
    链接:juejin.im/post/5f17e0515188252e974f0b70
    声明:本文已获看书的小蜗牛授权发表,转发等请联系原作者授权

    APP 开发中经常会有这种需求:在浏览器或者短信中唤起 APP,如果安装了就唤起,否则引导下载。对于 Android 而言,这里主要牵扯的技术就是 deeplink,也可以简单看成 scheme,Android一直是支持scheme的,但是由于 Android的开源特性,不同手机厂商或者不同浏览器厂家处理的千奇百怪,有些能拉起,有些不行,本文只简单分析下link的原理,包括 deeplink,也包括 Android6.0之后的 AppLink。其实个人认为,AppLink就是特殊的deeplink,只不过它多了一种类似于验证机制,如果验证通过,就设置默认打开,如果验证不过,则退化为deeplink,如果单从APP端来看,区别主要在 Manifest 文件中的android:autoVerify="true",如下,

    APPLINK只是在安装时候多了一个验证,其他跟之前deeplink一样,如果没联网,验证失败,那就跟之前的deeplink表现一样

    deeplink配置(不限http/https)

    <intent-filter>
        <data android:scheme="https" android:host="test.example.com"  />
        <category android:name="android.intent.category.DEFAULT" />
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
    </intent-filter>
    
    (不限http/https)
     <intent-filter>
         <data android:scheme="example" />
         <!-- 下面这几行也必须得设置 -->
         <category android:name="android.intent.category.DEFAULT" />
         <action android:name="android.intent.action.VIEW" />
         <category android:name="android.intent.category.BROWSABLE" />
    </intent-filter>
    

    applink配置(只能http/https)

    <intent-filter android:autoVerify="true">
        <data android:scheme="https" android:host="test.example.com"  />
        <category android:name="android.intent.category.DEFAULT" />
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
    </intent-filter>
    

    在Android原生的APPLink实现中,需要APP跟服务端双向验证才能让APPLink生效,如果如果APPLink验证失败,APPLink会完全退化成deepLink,这也是为什么说APPLINK是一种特殊的deepLink,所以先分析下deepLink,deepLink理解了,APPLink就很容易理解。

    deepLink原理分析

    deeplink的scheme相应分两种:一种是只有一个APP能相应,另一种是有多个APP可以相应,比如,如果为一个APP的Activity配置了http scheme类型的deepLink,如果通过短信或者其他方式唤起这种link的时候,一般会出现一个让用户选择的弹窗,因为一般而言,系统会带个浏览器,也相应这类scheme,比如下面的例子:

    >adb shell am start -a android.intent.action.VIEW   -c android.intent.category.BROWSABLE  -d "https://test.example.com/b/g"
    
    <intent-filter>
        <data android:scheme="https" android:host="test.example.com"  />
        <category android:name="android.intent.category.DEFAULT" />
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
    </intent-filter>
    

    如果是设置了一个私用的,并且没有跟其他app重复的,那么会直接打开,比如下面的:

    >adb shell am start -a android.intent.action.VIEW   -c android.intent.category.BROWSABLE  -d "example://test.example.com/b/g"
    
     <intent-filter>
         <data android:scheme="example" />
         <!-- 下面这几行也必须得设置 -->
         <category android:name="android.intent.category.DEFAULT" />
         <action android:name="android.intent.action.VIEW" />
         <category android:name="android.intent.category.BROWSABLE" />
    </intent-filter>
    

    当然,如果私有scheme跟其他APP的重复了,还是会唤起APP选择界面(其实是一个ResolverActivity)。下面就来看看scheme是如何匹配并拉起对应APP的。

    startActivity入口与ResolverActivity

    无论APPLink跟DeepLink其实都是通过唤起一个Activity来实现界面的跳转,无论从APP外部:比如短信、浏览器,还是APP内部。通过在APP内部模拟跳转来看看具体实现,写一个H5界面,然后通过Webview加载,不过Webview不进行任何设置,这样跳转就需要系统进行解析,走deeplink这一套:

    <html>
    <body> 
     <a href="https://test.example.com/a/g">Scheme跳转</a>
    </body>
    </html>
    

    点击Scheme跳转,一般会唤起如下界面,让用户选择打开方式:

    如果通过adb打印log,你会发现ActivityManagerService会打印这样一条Log:

    > 12-04 20:32:04.367   887  9064 I ActivityManager: START u0 {act=android.intent.action.VIEW dat=https://test.example.com/... cmp=android/com.android.internal.app.ResolverActivity (has extras)} from uid 10067 on display 0
    

    其实看到的选择对话框就是 ResolverActivity,不过我们先来看看到底是走到ResolverActivity 的,也就是这个scheme怎么会唤起 App 选择界面,在短信中,或者 Webview 中遇到 scheme,他们一般会发出相应的 Intent(当然第三方APP可能会屏蔽掉,比如微信就换不起APP),其实上面的作用跟下面的代码结果一样:

       val intent = Intent()
        intent.setAction("android.intent.action.VIEW")
        intent.setData(Uri.parse("https://test.example.com/a/g"))
        intent.addCategory("android.intent.category.DEFAULT")
        intent.addCategory("android.intent.category.BROWSABLE")
        startActivity(intent)
    

    那剩下的就是看startActivity,在6.0的源码中,startActivity最后会通过ActivityManagerService调用ActivityStatckSupervisor的startActivityMayWait

    ActivityStatckSUpervisor

    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 [见2.7.1]
        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查询,

    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信息:

    ActivityStatckSUpervisor

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

    AppLink原理

    一般而言,每个APP都希望被自己制定的scheme唤起,这就是Applink,之前分析deeplink的时候提到了ResolveActivity这么一个选择过程,而AppLink就是自动帮用户完成这个选择过程,并且选择的scheme是最适合它的scheme(开发者的角度)。因此对于AppLink要分析的就是如何完成了这个默认选择的过程。

    目前Android源码提供的是一个双向认证的方案:在APP安装的时候,客户端根据 APP 配置像服务端请求,如果满足条件,scheme 跟服务端配置匹配的上,就为APP设置默认启动选项,所以这个方案很明显,在安装的时候需要联网才行,否则就是完全不会验证,那就是普通的deeplink,既然是在安装的时候去验证,那就看看PackageManagerService是如何处理这个流程的:

    PackageManagerService

    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);
    
        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);
    }
    

    startIntentFilterVerifications发送一个消息开启验证,随后调用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);
        } 
    }
    

    可以看出,验证就三步:检查、搜集、验证。在检查阶段,首先看看是否有设置http/https scheme的Activity,并且是否满足设置了Intent.ACTION_DEFAULT与Intent.ACTION_VIEW,如果没有,则压根不需要验证,

     * Check if one of the IntentFilter as both actions DEFAULT / VIEW and a HTTP/HTTPS data URI
     */
    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自身的配置进行校验,如果通过就设置默认启动,并持久化,验证成功的话可以通过

    adb shell dumpsys package d   
    

    查看结果:

      Package: com.xxx
      Domains: xxxx.com
      Status: always : 200000002
    

    验证后再通过PackageManagerService持久化到设置信息,如此就完成了Applink验证流程。

    Chrome浏览器对于自定义scheme的拦截

    https://developer.chrome.com/multidevice/android/intents

    A little known feature in Android lets you launch apps directly from a web page via an Android Intent. One scenario is launching an app when the user lands on a page, which you can achieve by embedding an iframe in the page with a custom URI-scheme set as the src, as follows:   <  iframe src="paulsawesomeapp://page1"> . This works in the Chrome for Android browser, version 18 and earlier. It also works in the Android browser, of course.

    The functionality has changed slightly in Chrome for Android, versions 25 and later. It is no longer possible to launch an Android app by setting an iframe's src attribute. For example, navigating an iframe to a URI with a custom scheme such as paulsawesomeapp:// will not work even if the user has the appropriate app installed. Instead, you should implement a user gesture to launch the app via a custom scheme, or use the “intent:” syntax described in this article.

    也就是在chrome中不能通过iframe跳转自定义scheme唤起APP了,直接被block,如下图:

    function userIframJump() {
     var url = 'yanxuan://lab/u.you.com';
     var iframe = document.createElement('iframe');
     iframe.style.width = '100px';
     iframe.style.height = '100px';
     iframe.style.display = 'none';
     iframe.src = url;
     document.body.appendChild(iframe);
     setTimeout(function() {
      iframe.remove();
     }, 1000);
    }
    

    但是仍然可以通过window.location.href唤起:

    function clickAndroid1(){
           window.location.href="yaxxxuan://lab/u.xx.com";
    }
    

    或者通过跳转标签唤起

    <a href="yauan://lab/u.you.com">测试</a>
    

    当然,如果自定义了https/http的也是可以的。总的来说Chrome除了Iframe,其他的好像都没问题。

    <a href="https://xxx.com/a/g">  https 跳转</a>
    

    国内乱七八糟的浏览器(观察日期2019-6-11)

    • 360浏览器,可以通过iframe、、 方式调用scheme,除了不支持https/http,其他都支持

    • UC浏览器可以通过iframe、、 方式调用scheme(即便如此,也可能被屏蔽(域名)) ,无法通过https/http/intent

    • QQ浏览器可以通过iframe、、 、intent 方式调用scheme,(也可能被屏蔽(域名),目前看没屏蔽) ,但是无法通过https/http

    前端需要根据不同的浏览器选择合适的策略。

    总结

    其实关于applink有几个比较特殊的点:

    • applink第一它只验证一次,在安装的时候,为什么不每次启动动检测呢?可能是为了给用户自己选怎留后门。

    • applink验证的时候需要联网,不联网的方案行吗?个人理解,不联网应该也可以,只要在安装的时候,只本地验证好了,但是这样明显没有双向验证安全,因为双向验证证明了网站跟app是一对一应的,这样才能保证安全,防止第三方打包篡改。

    参考文档

    https://developer.android.com/training/app-links/verify-site-associations

    相关阅读

    1 "DeepNight-in-kotlin"一个纯看妹纸的 Kotlin 开源项目
    2 代码洁癖症的我,学习 Lint 学到心态爆炸
    3 Android APP架构思考
    4 Android 进阶之网络协议及网络知识
    5 Android Glide缓存策略分析

    
    
    
    
    
    如果你有写博客的好习惯
    欢迎投稿
    赞+在看,小生感恩❤️
    
    展开全文
  • 简介 通过 Link这个单词我们可以看出这个是一种链接,使用此链接可以直接...快速点击shift两次,输入 APPLink 即可找到 AS 提供的集成教程。 在 AS 中已经有详细的使用步骤了,总共分为 4 步 add URL intent filter...

    简介

    通过 Link这个单词我们可以看出这个是一种链接,使用此链接可以直接跳转到 APP,常用于应用拉活,跨应用启动,推送通知启动等场景。

    流程

    在AS 上其实已经有详细的使用步骤解析了,这里给大家普及下

    腾讯面试官:同学,说说 Applink 的使用以及原理

    快速点击 shift 两次,输入 APPLink 即可找到 AS 提供的集成教程。
    在 AS 中已经有详细的使用步骤了,总共分为 4 步

    add URL intent filters

    创建一个 URL

    腾讯面试官:同学,说说 Applink 的使用以及原理

    或者也可以点击 “How it works” 按钮

    Add logic to handle the intent

    选择通过 applink 启动的入口 activity。
    点击完成后,AS 会自动在两个地方进行修改,一个是 AndroidManifest

     <activity android:name=".TestActivity">
                <intent-filter>
                    <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"
                        android:host="geyan.getui.com" />
                </intent-filter>
            </activity>

    此处多了一个 data,看到这个 data 标签,我们可以大胆的猜测,也许这个 applink 的是一个隐式启动。
    另外一个改动点是

        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_test);
            // ATTENTION: This was auto-generated to handle app links.
            Intent appLinkIntent = getIntent();
            String appLinkAction = appLinkIntent.getAction();
            Uri appLinkData = appLinkIntent.getData();
        }

    applink 的值即为之前配置的 url 链接,此处是为了接收数据用的,不再多说了。

    Associate website

    这一步最关键了,需要根据 APP 的证书生成一个 json 文件, APP 安装的时候会去联网进行校验。选择你的线上证书,然后点击生成会得到一个 assetlinks.json 的文件,需要把这个文件放到服务器指定的目录下

    腾讯面试官:同学,说说 Applink 的使用以及原理

    基于安全原因,这个文件必须通过 SSL 的 GET 请求获取,JSON 格式如下:

    [{
      "relation": ["delegate_permission/common.handle_all_urls"],
      "target": {
        "namespace": "android_app",
        "package_name": "com.lenny.myapplication",
        "sha256_cert_fingerprints":
        ["E7:E8:47:2A:E1:BF:63:F7:A3:F8:D1:A5:E1:A3:4A:47:88:0F:B5:F3:EA:68:3F:5C:D8:BC:0B:BA:3E:C2:D2:61"]
      }
    }]

    sha256_cert_fingerprints 这个参数可以通过 keytool 命令获取,这里不再多说了。

    最后把这个文件上传到 你配置的地址/.well-know/statements/json,为了避免今后每个 app 链接请求都访问网络,安卓只会在 app 安装的时候检查这个文件。,如果你能在请求 https://yourdomain.com/.well-... 的时候看到这个文件(替换成自己的域名),那么说明服务端的配置是成功的。目前可以通过 http 获得这个文件,但是在M最终版里则只能通过 HTTPS 验证。确保你的 web 站点支持 HTTPS 请求。

    若一个host需要配置多个app,assetlinks.json添加多个app的信息。
    若一个 app 需要配置多个 host,每个 host 的 .well-known 下都要配置assetlinks.json

    有没有想过 url 的后缀是不是一定要写成 /.well-know/statements/json 的?
    后续讲原理的时候会涉及到,这里先不细说。

    Test device

    最后我们本质仅是拿到一个 URL,大多数的情况下,我们会在 url 中拼接一些参数,比如

    https://yourdomain.com/products/123?coupon=save90

    其中 ./products/123?coupon=save90 是我们之前在第二步填写的 path。
    那测试方法多种多样,可以使用通知,也可以使用短信,或者使用 adb 直接模拟,我这边图省事就直接用 adb 模拟了

    adb shell am start
    -W -a android.intent.action.VIEW
    -d "https://yourdomain.com/products/123?coupon=save90"
    [包名]

    使用这个命令就会自动打开 APP。前提是 yourdomain.com 网站上存在了 web-app 关联文件。

    原理

    上述这些都简单的啦,依葫芦画瓢就行,下面讲些深层次的东西,不仅要知道会用,还得知道为什么可以这么用,不然和咸鱼有啥区别。

     

    上诉也说了,我们配置的域名是在 activity 的 data 标签的,那是否是可以认为 applink 是一种隐式启动,应用安装的时候根据 data 的内容到这个网页下面去获取 assetlinks.json 进行校验,如果符合条件则把 这个 url 保存在本地,当点击 webview 或者短信里面的 url的时候,系统会自动与本地库中的域名相匹配, 如果匹配失败则会被自动认为是 deeplink 的连接。确认过眼神对吧~~~

    也就说在第一次安装 APP 的时候是会去请求 data 标签下面的域名的,并且去请求所获得的域名,那 安装->初次启动 的体验自然会想到是在源码中 PackageManagerService 实现。

    一个 APk 的安装过程是极其复杂的,涉及到非常多的底层知识,这里不细说,直接找到校验 APPLink 的入口 PackageManagerService 的 installPackageLI 方法。

    PackageMmanagerService.class

    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) {
        ...
    
        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);
    }

    可以看到这边发送了一个 message 为 START_INTENT_FILTER_VERIFICATIONS 的 handler 消息,在 handle 的 run 方法里又会接着调用 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);
        } 
    }

    对 APPLink 进行了检查,搜集,验证,主要是对 scheme 的校验是否是 http/https,以及是否有 flag 为 Intent.ACTION_DEFAULT与Intent.ACTION_VIEW 的参数,接着是开启验证

    PMS#IntentVerifierProxy.class

    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 的实现是通过发送一个广播来进行验证的,也就是说,这是个异步的过程,验证是需要耗时的(网络请求),发出去的广播会被 IntentFilterVerificationReceiver 接收到。这个类又会再次 start DirectStatementService,在这个 service 里面又会去调用 DirectStatementRetriever 类。在此类的 retrieveStatementFromUrl 方法中才是真正请求网络的地方

    DirectStatementRetriever.class

      @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 {
                throw new AssociationServiceException("Namespace is not supported.");
            }
        }
      private Result retrieveFromWeb(WebAsset asset)
                throws AssociationServiceException {
            return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset);
        }
        private String computeAssociationJsonUrl(WebAsset asset) {
            try {
                return new URL(asset.getScheme(), asset.getDomain(), asset.getPort(),
                        WELL_KNOWN_STATEMENT_PATH)
                        .toExternalForm();
            } catch (MalformedURLException e) {
                throw new AssertionError("Invalid domain name in database.");
            }
        }
    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);
        }
    }

    到了这里差不多就全部讲完了,本质就是通过 HTTPURLConnection 去发起来一个请求。之前还留了个问题,是不是一定要要 /.well-known/assetlinks.json,到这里是不是可以完全明白了,就是 WELL_KNOWN_STATEMENT_PATH 参数

        private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json";
    

    缺点

    1. 只能在 Android M 系统上支持

    在配置好了app对App Links的支持之后,只有运行Android M的用户才能正常工作。之前安卓版本的用户无法直接点击链接进入app,而是回到浏览器的web页面。

    1. 要使用App Links开发者必须维护一个与app相关联的网站

    对于小的开发者来说这个有点困难,因为他们没有能力为app维护一个网站,但是它们仍然希望通过web链接获得流量。

    1. 对 ink 域名不太友善
      在测试中发现,国内各大厂商对 .ink 域名不太友善,很多的是被支持了 .com 域名,但是不支持 .ink 域名。
    机型 版本 是否识别ink 是否识别com
    小米 MI6 Android 8.0 MIUI 9.5
    小米 MI5 Android 7.0 MIUI 9.5
    魅族 PRO 7 Android 7.0 Flyme 6.1.3.1A
    三星 S8 Android 7.0 是,弹框
    华为 HonorV10 Android 8.0 EMUI 8.0
    oppo R11s Android 7.1.1 ColorOS 3.2
    oppo A59s Android 5.1 ColorOS 3.0 是,不能跳转到app 是,不能跳转到app
    vivo X6Plus A Android 5.0.2 Funtouch OS_2.5
    vivo 767 Android 6.0 Funtouch OS_2.6 是,不能跳转到app 是,不能跳转到app
    vivo X9 Android 7.1.1 Funtouch OS_3.1 是,不能跳转到app 是,不能跳转到app

    参考

    官方文档

     

    最后对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

    展开全文
  • Applink使用教程及原理解析

    千次阅读 2019-11-14 14:49:40
    个推作为大促期间的消息推送服务商,为蘑菇街等电商APP在消息的稳定下发环节提供着强大支撑和保障。今年的11.11个推全球消息下发总量再创新高,超过274亿条。而2017年和2018年11.11当天个推推送的总下发量分别是超过...

    今年11.11大促期间,各大电商平台都使出了浑身解数,吸引剁手族买买买。个推作为大促期间的消息推送服务商,为蘑菇街等电商APP在消息的稳定下发环节提供着强大支撑和保障。今年的11.11个推全球消息下发总量再创新高,超过274亿条。而2017年和2018年11.11当天个推推送的总下发量分别是超过110亿条和232亿条。

     

    那么个推是如何在11.11期间支撑起数百亿级别的推送量,且使消息推送稳定率达到了99.9%的呢?这背后离不开个推强大智能的技术服务。而Applink 在推送中也发挥了一定的做用。它使消息不再局限于手机通知栏。开发者可以通过AppLink技术,让用户在点击短信、信息流或Banner后,直接跳转到APP指定页面,在打造流畅用户体验的同时实现了高效的转化,提升了消息推送的到达率与点击率。本文将着重分析一下个推Applink的技术原理和使用方式。

     

     

    简介

    通过 Link这个单词我们可以看出这是一种链接,使用此链接可以直接跳转到 APP。Applink常用于应用拉活、跨应用启动、推送通知启动等场景。

     

    流程

    在AS 上其实已经有详细的使用步骤解析了,这里给大家普及下 。

     

    快速点击 shift 两次,输入 APPLink 即可找到 AS 提供的集成教程。详细教程可参加AS,总共分为 4 步:

     

    add URL intent filters

    创建一个 URL 

    或者也可以点击 “How it works” 按钮

    Add logic to handle the intent

    选择通过 applink 启动的入口 activity。点击完成后,AS 会自动在两个地方进行修改,详情如下:

    (一)

     

     <activity android:name=".TestActivity">            <intent-filter>                <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"                    android:host="geyan.getui.com" />            </intent-filter>        </activity>

     

     

    此处多了一个 data,看到这个 data 标签,我们可以大胆的猜测,这个 applink可能是一种隐式的APP启动方式。

     

    (二)

     

        protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_test);        // ATTENTION: This was auto-generated to handle app links.        Intent appLinkIntent = getIntent();        String appLinkAction = appLinkIntent.getAction();        Uri appLinkData = appLinkIntent.getData();    }

     

     

    applink 的值即为之前配置的 url 链接,此处配置是为接收数据所用,不再予以赘述。

     

    Associate website

    这一步最为关键:开发者需要根据 APP 证书生成一个 json 文件,这样可以保证用户在 APP 安装的时候,通过安卓系统的校验。选择你的线上证书,然后点击生成会得到一个 assetlinks.json 的文件,需要把这个文件放到服务器指定的目录下。

    基于安全原因,这个文件必须通过 SSL 的 GET 请求获取,JSON 格式如下:

    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    [{  "relation": ["delegate_permission/common.handle_all_urls"],  "target": {    "namespace": "android_app",    "package_name": "com.lenny.myapplication",    "sha256_cert_fingerprints":    ["E7:E8:47:2A:E1:BF:63:F7:A3:F8:D1:A5:E1:A3:4A:47:88:0F:B5:F3:EA:68:3F:5C:D8:BC:0B:BA:3E:C2:D2:61"]  }}]

    sha256_cert_fingerprints 这个参数可以通过 keytool 命令获取。最后把这个文件上传到 你配置的地址/.well-know/statements/json。为了避免今后每个 app 链接请求都需要访问网络,安卓只会在 app 安装的时候检查这个文件。如果你能在请求 https://yourdomain.com/.well-known/statements.json 的时候看到这个文件(替换成自己的域名),则说明服务端的配置是成功的。目前我们可以通过 http 获得这个文件,但是在M最终版里只能通过 HTTPS 验证。确保你的 web 站点支持 HTTPS 请求。 若一个host需要配置多个app,那么assetlinks.json需要添加多个app的信息。若一个 app 需要配置多个 host,每个 host 的 .well-known 下都要配置assetlinks.json。url 的后缀是不是一定要写成 /.well-know/statements/json 格式呢?后续讲原理的时候我们会涉及到,这里先不展开。

     

    ###Test device我们操作的最终目的是为了拿到一个 URL。大多数情况下,我们会在 url 中拼接一些参数,比如

     

    •  
    https://yourdomain.com/products/123?coupon=save90

    其中 ./products/123?coupon=save90 是我们之前在第二步填写的 path。测试方法比较多样,可以使用通知、短信来进行测试,也可以使用 adb 进行直接模拟。我这边选择 adb 模拟。

     

    •  
    •  
    •  
    •  
    adb shell am start-W -a android.intent.action.VIEW-d "https://yourdomain.com/products/123?coupon=save90"[包名]

    使用这个命令就会自动打开 APP。前提是 yourdomain.com 网站上存在了 web-app 关联文件。

     

     

    原理

    上述操作相对比较简单,依葫芦画瓢就行。下面讲些深层次的东西:不仅要知道要会用,还得知道为什么可以这么用,不然和咸鱼有啥区别?

    上文也提到了我们配置的域名是在 activity 的 data 标签里面,因此我们可以认为 applink 是一种隐式启动方式,应用安装的时候根据 data 的内容到这个网页下面去获取 assetlinks.json 进行校验,如果符合条件则把 这个 url 保存在本地,当点击 webview 或者短信里面的 url的时候,系统会自动与本地库中的域名相匹配, 如果匹配失败则会被自动认为是 deeplink 的连接。也就说在第一次安装 APP 的时候安卓系统是会去验证data 标签下面的域名,那我们可以推理出安装APP的底层实现其实是在源码中 PackageManagerService进行的。以下方法可以帮助你快速找到校验 APPLink 的入口 PackageManagerService 的 installPackageLI。

     

    PackageMmanagerService.class

    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    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) {    ...
        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);}

    可以看到这边发送了一个 message 为 START_INTENT_FILTER_VERIFICATIONS 的 handler 消息,在 handle 的 run 方法里又会接着调用 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);    } }

    安卓底层在安装APP时会对 APPLink 进行检查、搜集、验证,判断其是否为http/https,以及是否有 flag 为 Intent.ACTION_DEFAULT与Intent.ACTION_VIEW 的参数,接着便开启验证。

    PMS#IntentVerifierProxy.class​​​​​​​

    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是通过发送一个广播来进行验证的,也就是说,这是个异步的过程,验证是需要耗时的(网络请求),发出去的广播会被 IntentFilterVerificationReceiver 接收到。这个类又会再次 start DirectStatementService,在这个 service 里面又会去调用 DirectStatementRetriever 类。在此类的 retrieveStatementFromUrl 方法中才是真正请求网络的地方。

    DirectStatementRetriever.class​​​​​​​

      @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 {            throw new AssociationServiceException("Namespace is not supported.");        }    }  private Result retrieveFromWeb(WebAsset asset)            throws AssociationServiceException {        return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset);    }    private String computeAssociationJsonUrl(WebAsset asset) {        try {            return new URL(asset.getScheme(), asset.getDomain(), asset.getPort(),                    WELL_KNOWN_STATEMENT_PATH)                    .toExternalForm();        } catch (MalformedURLException e) {            throw new AssertionError("Invalid domain name in database.");        }    }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);    }}

    以上讲解我们可以得出一个结论,即Applink的本质是通过 HTTPURLConnection 去发起请求。前文还留了个问题,url 的后缀是不是一定要写成是不是一定要写成/.well-known/assetlinks.json 格式呢?看到这里相信大家都已经明白了,格式一定要这么写!!格式就是 WELL_KNOWN_STATEMENT_PATH 参数!

     

        private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json";

     

     

    缺点

    1. 只能在 Android M 系统上支持 在配置好了app对Applink的支持之后,只有运行Android M的用户才能使Applink正常工作。Android M之前版本的用户点击链接无法直接进入app,而是回到浏览器的web页面。

     

    2. 要使用App Links开发者必须维护一个与app相关联的网站 对于小的开发者来说这个有点困难,因为他们没有能力为app维护一个网站,但是它们仍然希望通过web链接获得流量。

     

    3. 对 ink 域名不太友善 在测试中发现,国内各大厂商对 .ink 域名不太友善,很多厂商仅支持 .com 域名,却不支持 .ink 域名。

     

    (调研结果仅供参考)

    机型

    版本

     

         是否识别ink

     

     

    是否识别

    com

     

    小米

    MI6 Android 8.0 MIUI 9.5

    小米

    MI5 Android 7.0 MIUI 9.5

    魅族

    PRO 7 Android 7.0 Flyme 6.1.3.1A

    三星

    S8 Android 7.0

     

    是,弹框    

     

    华为

    HonorV10 Android 8.0 EMUI 8.0

    oppo

    R11s Android 7.1.1 ColorOS 3.2

    oppo

    A59s Android 5.1 ColorOS 3.0

    是,不能跳转到app

    是,不能跳转到app

    vivo

    X6Plus A Android 5.0.2 Funtouch OS_2.5

     

     

    vivo

    767 Android 6.0 Funtouch OS_2.6

    是,不能跳转到app

    是,不能跳转到app

    vivo

    X9 Android 7.1.1 Funtouch OS_3.1

    是,不能跳转到app

    是,不能跳转到app

     

    总结

    通过使用Applink,我们个推拓宽了交互场景,对App的启动有了一个更多元化的选择,缩短了App的启动路径,能够使用户更快地启动App;同时,因为Applink的内在逻辑已经在 Android framework 层兼容,这使得我们推送服务的准确性也得到了一定的保障。未来,个推也将持续优化消息推送服务,并进一步提高推送的到达率与点击率,以满足一些实时性要求更高更复杂的业务场景需求。

     

    展开全文
  • AppLink简单来说就是你的app注册一个scheme,当安卓系统发现有用户点击可以识别的scheme,并且这个scheme在你的app中注册过,则会拉起你app的指定activity,并且可以附带参数。 不过由于Android只能识别http开头的...
  • 最全的 APPLink 的使用与源码

    千次阅读 2019-09-27 08:50:28
    简介 通过 Link这个单词我们可以看出这个是一种链接,使用此链接...快速点击 shift 两次,输入 APPLink 即可找到 AS 提供的集成教程。 在 AS 中已经有详细的使用步骤了,总共分为 4 步 ###add URL intent filters ...
  • 深度链接applink探索

    2019-06-13 06:24:33
    为什么80%的码农都做不了架构师?>>> ...
  • Android DEPPLINK、APPLink原理简析

    千次阅读 2019-01-09 09:52:14
    APP开发中经常会有这种需求:在浏览器或者短信中唤起APP,如果安装了就唤起,否则引导下载。对于Android而言,这里主要牵扯的技术就是deeplink,也可以简单看成scheme,Android一直是支持scheme的,但是由于Android...
  • UniversalLink通用链接

    万次阅读 2018-01-02 11:23:23
    什么是Universal Link iOS9 新引入的一个功能,是通过传统HTTP链接来启动App的技术,可以使用相同的网址打开网站和App。通过唯一的网址,就可以链接到App中具体的视图,不需要特殊的scheme。如果用户没有安装App则...
  • ios app使用微信登录现在都需要填写一个universal link,本文介绍1.怎么生成universal link,2.提示错误时怎么检查 生成universal link 准备工作:需要有可以使用https访问的网址,网站需要通过苹果ATS测试:...
  • 移动互联网时代,信息的分享传播无疑是 App 引流增长的关键,与其花费大量精力和成本找渠道、硬推广,不如从细节下手,用最快最简便的方法实现 Deeplink(深度链接)技术,打破信息孤岛、缩短分享路径、优化用户体验...
  • iOS 唤起APP之Universal Link(通用链接)

    万次阅读 2020-07-02 15:14:08
    iOS 9之前,一直使用的是URL Schemes技术来从外部对App进行跳转,但是iOS系统中进行URL Schemes跳转的时候如果没有安装App,会提示Cannot open Page的提示,而且当注册有多个scheme相同的时候,目前没有办法区分,...
  • 在使用openssl里面的test目录下ecdhtest.c运行时出错,如下: 百度到解决办法http://www.bubuko.com/infodetail-1082732.html,但是对我来说直接这么写错误还是没解决,原来是extern “C”语法写错了,语法参照这...
  • no OPENSSL_Applink 的错误

    千次阅读 2012-06-08 13:51:00
    OPENSSL_Uplink(006E9000,08): no OPENSSL_Applink  在网上查了一下,发现下面这个方法是对的。 在自己的程序中,加上这样的源代码: extern "C" { #include } 直接将applink中的代码,链接...
  • 应用跳转(Deep Link

    万次阅读 2016-08-16 15:53:53
    前言: 接触到这个是因为项目需求。产品给的需求是这样的:在微信或者任何分享的链接中,打开我们自己...但是当用户没安装我们的app,那么此时app下载安装的来源是第三方的市场(这里假设是应用宝),那么应用宝安装完
  • iOS 通用链接(Universal Link)唤起app的实现方法准备工作开发者账号设置支持进入开发者中心 选择 Certificates, IDs &amp; Profiles选择对应的AppId, 编辑其属性,让它能支持Associated Domains,编辑完成之后...
  • http://fokkezb.nl/2013/08/26/url-schemes-for-ios-and-android-1/
  • iOS 通用链接(Universal Link)配置

    千次阅读 2019-09-30 14:57:12
    通用链接是iOS9推出的一项新功能,如果你的应用(app)支持通用链接(Universal Link)之后,iOS用户能够在点击网页的链接的时候无缝的重定向到已经安装的app,不需要额外的任何操作。如果没有安装对应的app,那么...
  • deepLink技术打开app内的某个页面

    千次阅读 2017-10-16 10:02:36
    现在很火的app上的deeplink技术,到底是什么? 2016-12-27 11:39技术 来源:魔窗合作伙伴[ 互联网数据官],第一家聚焦于基于数据和大数据的互联网营销、运营、产品和管理的中立平台。 原创好文授权发布于...
1 2 3 4 5 ... 20
收藏数 180,876
精华内容 72,350
关键字:

applink