- 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();
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":
Intent intent1 = new Intent(this, MainActivity.class);
readGoActivity(intent1, this);
break;
case "full":
Intent intent2 = new Intent(this, TestFullActivity.class);
readGoActivity(intent2, this);
break;
case "list":
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) {
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);
}
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连接生成短连接
- 这个很容易,直接找个短连接生成的网站,而后把连接转化一下就能够。至于转化的原理,我暂时也不清楚……
- 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">当即打开一鹿报价页面(直接打开)>></a>
</body>
</html>
复制代码
- 点击Scheme跳转,通常会唤起以下界面,让用户选择打开方式:
- 经过adb打印log,你会发现ActivityManagerService会打印这样一条Log:
ActivityManager: START u0 {act=android.intent.action.VIEW dat=yilu:
复制代码
- 其实看到的选择对话框就是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 = new Intent(intent);
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) {
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;
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选择界面。
- 以前分析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);
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,若是没有,则压根不须要验证
case START_INTENT_FILTER_VERIFICATIONS: {
IFVerificationParams params = (IFVerificationParams) msg.obj;
verifyIntentFiltersIfNeeded(params.userId, params.verifierUid,
params.replacing, params.pkg);
break;
}
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);
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自身的配置进行校验,若是经过就设置默认启动,并持久化,验证成功的话能够经过。