在上一节分析中,我们已经知道宿主已经加载了插件的资源、类。也就是说在宿主中是可以使用插件中的类的。但是对于启动Activity
这件事就比较特殊了:
在Android中一个Activity必须在AndroidManifest.xml
中注册才可以被启动,可是很明显的是插件中的Activity
是不可能提前在宿主的manifest文件中注册的。也就是说直接在宿主中启动一个插件的Acitvity必定失败。那怎么办呢 ?VirtualApk
的实现方式是通过hook系统启动Activity
过程中的一些关键点,绕过系统检验,并添加插件Activity
相关运行环境等一系列处理,使插件Activity
可以正常运行。
接下来的分析会涉及到Activity
的启动源码相关知识,如果你还不是很熟悉可以先去回顾一下。这篇文章讲的也比较仔细 : https://www.kancloud.cn/digest/androidframeworks/127782
hook Instrumentation
在Activtiy的启动过程中Instrumentation
有着至关重要的作用。它可以向ActivityManager
请求一个Acitivity的启动、构造一个Activity
对象等。VirtualApk
就hook了这个类:
final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
hook了这个类后,就有办法把一个非正常的插件Activtiy
包装成一个正常的Activity
了。先来看一下一小段Activity
启动源代码:
//Activity.java
void startActivityForResult(@RequiresPermission Intent intent, int requestCode,@Nullable Bundle options) {
......
mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this,intent, requestCode, options);
......
}
//Instrumentation.java
ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
......
ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
.....
}
即Instrumentation
带着要启动的Activity的Intent
就去找ActivityManager
启动Activity了。但这就有一个问题,还记得上一节插件APK的解析
吗?系统也会对宿主apk进行解析,并保存包中声明的Activity
信息。如果这个intent
所启动的插件Activity
。并不在宿主的Activity
信息集合中,那么就会报此Activity并未在manifest文件中注册
,下面这一小段就是ActivityManagerService
对要启动的Activity
进行校验的源码:
//PackageManagerService.java
final PackageParser.Package pkg = mPackages.get(pkgName);
if (pkg != null) {
//从宿主的包中查询是否注册过这个intent相关信息。
result = filterIfNotSystemUser(mActivities.queryIntentForPackage(intent, resolvedType, flags, pkg.activities, userId), userId);
}
......
插件Activity
并没有在manifest文件中注册,所以怎么办呢? VirtualApk
采用的方式是 :
- 提前在Manifest文件中注册一些Activity。简称这种Activity为 : “占坑 Activity”
- 在向
ActivityManagerService
提出启动Activtiy请求时,把插件的activity intent
换成已经在manifest文件中注册的占坑Activity的intent
- 在
Instrumentation
真正构造Activity
时,再换回来。即构造要启动的插件Activity
接下来我们看一下具体操作, 首先在manifest文件中注册一些Activity
<activity android:exported="false" android:name=".A$1" android:launchMode="standard"/>
<activity android:exported="false" android:name=".A$2" android:launchMode="standard"
你在manifest中随意注册Activity是ok的。并不会发生异常。你的项目有没有出现删除一个Activity时,忘记了删除manifest文件中相关注册信息的情况呢?
启动插件activity时,替换为已经注册的acitvity
我们前面也看到了,Instrumentation.execStartActivity()
会像ActivityManagerService
发起启动Activity的请求,因此我们只要hook这个方法,并替换掉intent为提前在manifest注册的Activity就可以了:
@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode) {
injectIntent(intent); // 替换 插件Activity intent 为提前在manifest文件中注册的 activity intent
return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode);
}
protected void injectIntent(Intent intent) {
......
PluginManager.getComponentsHandler().markIntentIfNeeded(intent);
}
即对一个将要启动的Activity的intent做了一些处理,看看做了什么处理:
public void markIntentIfNeeded(Intent intent) {
.....
String targetPackageName = intent.getComponent().getPackageName();
String targetClassName = intent.getComponent().getClassName();
//这个Activity是插件的Activity
if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
intent.putExtra(Constants.KEY_IS_PLUGIN, true);
intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName); //保留好真正要启动的Activity信息
intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
dispatchStubActivity(intent);
}
}
private void dispatchStubActivity(Intent intent) {
//根据要启动的activity的启动模式、主题,去选择一个 符合条件的`占坑Activity`
String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
intent.setClassName(mContext, stubActivity); //把intent的启动目标设置为这个”占坑Activity“
}
即如果要启动的Activity是一个插件的Activity,那么就选择一个合适的”占坑Activity”。来作为真正要启动的对象,并在intent中保存真正要启动的插件Acitvity的信息。
好,到这里我们知道对于插件Activity的启动
,通过hookInstrumentation.execStartActivity()
,实际上向ActivityManagerService
请求的启动的是一个占坑的Activity
。
经过上面这些操作,ActivityManagerService
做过一些列处理后,会让APP真正来启动这个占坑Activity
:
//ActivityThread.java : 真正开始实例化Activity,并开始走生命周期相关方法
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
//实例化一个Activity
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
...
}
这时候我们真的要去实例化这个”占坑Activity”吗?当然不会,我们要实例化的是”插件Activity”, 所有VirtualApk
hook了Instrumentation.newActivity()
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
try {
cl.loadClass(className);
} catch (ClassNotFoundException e) {
....
//这个 intent是否是插件的intent
ComponentName component = PluginUtil.getComponent(intent);
if (component == null) {//不是插件的intent,具体实例化逻辑,交给父类去处理
return newActivity(mBase.newActivity(cl, className, intent));
}
//拿到启动前保存的真正要启动的插件Activity的信息
String targetClassName = component.getClassName();
//使用插件的classloader来构造插件Activity
Activity activity = mParentInstrumentation.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
// for 4.1+
Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());
return newActivity(activity); //这个方法其实是对已经实例化过的Activity做了一个缓存。
}
return newActivity(mBase.newActivity(cl, className, intent));
}
cl.loadClass(className)
是要实例化占坑Activity
,这里肯定是会抛类找不到异常的,因为对于占坑Activity
,我们只是在manifest文件中做了声明,实际上并没有对应的类。所以对于插件Activity的启动会走到catch
代码中。
这里有一个需要注意的点: 即加载插件的类使用的是plugin.getClassLoader()
。那么可不可以直接使用宿主的ClassLoader呢?其实是可以的,在前面文章插件APK解析
时,我们已经看到VirtualApk
支持把插件类加载器的pathList
合并到宿主的pathList
中,
因此这里直接mParentInstrumentation.newActivity(cl, targetClassName, intent)
也是可以的。
如何让一个插件Activity正常运行?
VirtualApk
通过对Instrumentation
的hook,成功启动了一个插件Acitivity。可是要知道的是这个Activtiy毕竟是个小黑孩。想要正常运行,还是需要插件框架来给予支持的。
资源
是的,首先就是资源, 插件Activity的资源是怎么设置的呢? 我们来看一下一个Activity的资源是怎么设置的(Android 8.0):
在
ActivityThread.performLaunchActivity()
方法中,先来看一下为一个Activity
设置Context的过程:
ContextImpl appContext = createBaseContextForActivity(r);
...
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
...
activity.attach(appContext, this, getInstrumentation(), r.token,.....);
...
mInstrumentation.callActivityOnCreate(activity, r.state);
在创建
ContextImpl
时就设置了ContextImpl
的资源, 即Acitivity的资源:
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, .....;
context.setResources(packageInfo.getResources());
return context;
}
即一个Activity
的资源实际上来自apk
资源,这是当然。但对于插件Activity的设置就有问题了。看performLaunchActivity()
的执行流程,即context
的资源设置的是app打包的时的资源。宿主app打出来的包,肯定不会包含插件的资源的。
因此插件的Activity
现在它所拥有的资源是宿主的资源而不是插件的资源,因此,我们需要把插件Activity
的资源换成插件的。那在哪一步换呢? VirtualApk
是在Instrumentation.callActivityOnCreate
换的:
public void callActivityOnCreate(Activity activity, Bundle icicle) {
injectActivity(activity);
mBase.callActivityOnCreate(activity, icicle);
}
protected void injectActivity(Activity activity) {
final Intent intent = activity.getIntent();
if (PluginUtil.isIntentFromPlugin(intent)) { //插件的intent
Context base = activity.getBaseContext();
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
Reflector.with(base).field("mResources").set(plugin.getResources()); //把插件Activity的资源换成自己的。插件拿宿主的资源没什么用
Reflector reflector = Reflector.with(activity);
reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext())); //把插件的 context 也换掉
reflector.field("mApplication").set(plugin.getApplication()); /
....
}
}
对于资源相关更详细的了解,你可以看一下这篇文章 : https://www.notion.so/pengchengdasf/VirtualAPK-1fce1a910c424937acde9528d2acd537
可以看到上面的代码不仅把插件的资源替换为了自己的,并且还为插件重新设置了Context
。
Context的替换
一个Activity在运行是离不开Context
的, 它的Context
也是一个连接点。比如我们会使用Activity的Context
来获取一个ContentResolver
、资源、Theme。所有我们需要对插件Activity
的Context做一个hook。来方便我们对于插件Activity
的特殊处理。
对于所有的插件Activity
, 在VirtualApk
中它们的Context为PluginContext
:
class PluginContext extends ContextWrapper {
private final LoadedPlugin mPlugin;
public PluginContext(LoadedPlugin plugin) {
super(plugin.getPluginManager().getHostContext());
this.mPlugin = plugin;
}
.....
@Override
public ContentResolver getContentResolver() {
return new PluginContentResolver(getHostContext());
}
....
@Override
public Resources getResources() {
return this.mPlugin.getResources();
}
@Override
public AssetManager getAssets() {
return this.mPlugin.getAssets();
}
@Override
public Resources.Theme getTheme() {
return this.mPlugin.getTheme();
}
@Override
public void startActivity(Intent intent) {
ComponentsHandler componentsHandler = mPlugin.getPluginManager().getComponentsHandler();
componentsHandler.transformIntentToExplicitAsNeeded(intent);
super.startActivity(intent);
}
}
这里我只是列了一些重要的点:
- 插件Activity的
Context
获取Resources
、Assets
、Theme
都是特殊处理的 - 插件Activity获取的
ContentResolver
也是被hook过的 - ….
对于插件Activity
的启动,本文就只看个大体流程和关键点,对于具体的细节可以去看VirtualApk
源码。
我们用一张图来总结插件Activity的启动过程:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/20273,转载请注明出处。
评论0