疑问的开端
大家有没有想过一个问题:在浏览器里打开某个网页,网页上有一个按钮点击可以唤起App。
这样的效果是怎么实现的呢?浏览器是一个app;为什么一个app可以调起其他app的页面?
说到跨app的页面调用,大家是不是能够想到一个机制:Activity的隐式调用?
隐式启动原理
当我们有需要调起其他app的页面时,使用的API就是隐式调用。
- <activityandroid:name=\”.OtherActivity\”
- android:screenOrientation=\”portrait\”>
- <intent-filter>
- <actionandroid:name=\”mdove\”/>
- <categoryandroid:name=\”android.intent.category.DEFAULT\”/>
- </intent-filter>
- </activity>
其他App想启动上边这个Activity如下的调用就好:
- valintent=Intent()
- intent.action=\”mdove\”
- startActivity(intent)
我们没有主动声明Activity的class,那么系统是怎么为我们找到对应的Activity的呢?其实这里和正常的Activity启动流程是一样的,无非是if / else的实现不同而已。
接下来咱们就回顾一下Activity的启动流程,为了避免陷入细节,这里只展开和大家相对“耳熟能详”的类和调用栈,以串流程为主。
跨进程
首先我们必须明确一点:无论是隐式启动还是显示启动;无论是启动App内Activity还是启动App外的Activity都是跨进程的。比如我们上述的例子,一个App想要启动另一个App的页面,至少涉及3个进程。
注意没有root的手机,是看不到系统孵化出来的进程的。也就是我们常见的为什么有些代码打不上断点。
追过startActivity()的同学,应该很熟悉下边这个调用流程,跟进几个方法之后就发现进到了一个叫做ActivityTread的类里边。
ActivityTread这个类有什么特点?有main函数,就是我们的主线程。
很快我们能看到一个比较常见类的调用:Instrumentation:
- //Activity.java
- publicvoidstartActivityForResult(@RequiresPermissionIntentintent,intrequestCode,@NullableBundleoptions){
- mInstrumentation.execStartActivity(this,mMainThread.getApplicationThread(),mToken,this,intent,requestCode,options);
- //省略
- }
注意mInstrumentation#execStartActivity()有一个标黄的入参,它是ActivityThread中的内部类ApplicationThread。
ApplicationThread这个类有什么特点,它实现了IApplicationThread.Stub,也就是aidl的“跨进程调用的客户端回调”。
此外mInstrumentation#execStartActivity()中又会看到一个大名鼎鼎的调用:
- publicActivityResultexecStartActivity(Contextwho,IBindercontextThread,IBindertoken,Activitytarget,Intentintent,intrequestCode,Bundleoptions){
- //省略…
- ActivityManager.getService()
- .startActivity(whoThread,who.getBasePackageName(),intent,
- intent.resolveTypeIfNeeded(who.getContentResolver()),
- token,target!=null?target.mEmbeddedID:null,
- requestCode,0,null,options);
- returnnull;
- }
我们点击去getService()会看到一个标红的IActivityManager的类。
它并不是一个.java文件,而是aidl文件。
所以ActivityManager.getService()本质返回的是“进程的服务端”接口实例,也就是:
ActivityManagerService
public class ActivityManagerService extends IActivityManager.Stub
所以执行到这就转到了系统进程(system_process进程)。省略一下代码细节,看一下调用栈:
从上述debug截图,看一看到此时已经拿到了我们的目标Activitiy的相关信息。
这里简化一些获取目标类的源码,直接引入结论:
PackageManagerService
这里类相当于解析手机内的所有apk,将其信息构造到内存之中,比如下图这样:
小tips:手机目录中/data/system/packages.xml,可以看到所有apk的path、进程名、权限等信息。
启动新进程
打开目标Activity的前提是:目标Activity的进程启动了。所以第一次想要打开目标Activity,就意味着要启动进程。
启动进程的代码就在启动Activity的方法中:
resumeTopActivityInnerLocked->startProcessLocked。
这里便引入了另一个另一个大名鼎鼎的类:ZygoteInit。这里简单来说会通过ZygoteInit来进行App进程启动的。
ApplicationThread
进程启动后,继续回到目标Activity的启动流程。这里依旧是一系列的system_process进行的转来转去,然后IApplicationThread进入目标进程。
注意看,在这里再次通过IApplicationThread回调到ActivityThread。
- classHextendsHandler{
- //省略
- publicvoidhandleMessage(Messagemsg){
- switch(msg.what){
- caseEXECUTE_TRANSACTION:
- finalClientTransactiontransaction=(ClientTransaction)msg.obj;
- mTransactionExecutor.execute(transaction);
- //省略
- break;
- caseRELAUNCH_ACTIVITY:
- handleRelaunchActivityLocally((IBinder)msg.obj);
- break;
- }
- //省略…
- }
- }
- //执行Callback
- publicvoidexecute(ClientTransactiontransaction){
- finalIBindertoken=transaction.getActivityToken();
- executeCallbacks(transaction);
- }
这里所谓的CallBack的实现是LaunchActivityItem#execute(),对应的实现:
- publicvoidexecute(ClientTransactionHandlerclient,IBindertoken,
- PendingTransactionActionspendingActions){
- ActivityClientRecordr=newActivityClientRecord(token,mIntent,mIdent,mInfo,
- mOverrideConfig,mCompatInfo,mReferrer,mVoiceInteractor,mState,mPersistentState,
- mPendingResults,mPendingNewIntents,mIsForward,
- mProfilerInfo,client);
- client.handleLaunchActivity(r,pendingActions,null);
- }
此时就转到了ActivityThread#handleLaunchActivity(),也就转到了咱们日常的生命周期里边,调用栈如下:
上述截图的调用链中暗含了Activity实例化的过程(反射):
- public@NonNullActivityinstantiateActivity(@NonNullClassLoadercl,@NonNullStringclassName,@NullableIntentintent)throwsInstantiationException,IllegalAccessException,ClassNotFoundException{
- return(Activity)cl.loadClass(className).newInstance();
- }
浏览器启动原理
Helo站内的回流页就是一个标准的,浏览器唤起另一个App的实例。
交互流程
html标签有一个属性href,比如:。
我们常见的一种用法:。也就是点击之后跳转到百度。
因为这个是前端的标签,依托于浏览器及其内核的实现,跳转到一个网页似乎很“顺其自然”(不然叫什么浏览器)。
当然这里和android交互的流程基本一致:用隐式调用的方式,声明需要启动的Activity;然后传入对应的协议(scheme)即可。比如:
前端页面:
- <head>
- <metacharset=\”UTF-8\”>
- <metaname=\”viewport\”content=\”width=device-width,initial-scale=1.0\”>
- </head>
- <body>
- <ahref=\”mdove1://haha\”>启动OtherActivity</a>
- </body>
android声明:
- <activity
- android:name=\”.OtherActivity\”
- android:screenOrientation=\”portrait\”>
- <intent-filter>
- <data
- android:host=\”haha\”
- android:scheme=\”mdove1\”/>
- <actionandroid:name=\”android.intent.action.VIEW\”/>
- <categoryandroid:name=\”android.intent.category.BROWSABLE\”/>
- <categoryandroid:name=\”android.intent.category.DEFAULT\”/>
- </intent-filter>
- </activity>
推理实现
浏览器能够加载scheme,可以理解为是浏览器内核做了封装。那么想要让android也能支持对scheme的解析,难道是由浏览器内核做处理吗?
很明显不可能,做了一套移动端的操作系统,然后让浏览器过来实现,是不是有点杀人诛心。
所以大概率能猜测出来,应该是手机中的浏览器app做的处理。我们就基于这个猜想去看一看浏览器.apk的实现。
浏览器实现
基于上边说的/data/system/packages.xml文件,我们可以pull出来浏览器的.apk。
然后jadx反编译一下Browser.apk中WebView相关的源码:
我们可以发现对href的处理来自于隐式跳转,所以一切就和上边的流程串了起来。
尾声
结尾留个小问题:如果我自己写个WebView去load一个前端页面,能隐式跳转吗?
原文链接:https://mp.weixin.qq.com/s/vWZDgOB0oFSiQg305Qqwiw