上篇文章已经介绍了VirtualAPK的配置和使用,请参考https://blog.csdn.net/shineflowers/article/details/80167302
今天看下从源码角度分析下VirtualAPK是怎么启动插件APK的吧!从上篇文章我们已经知道,启动插件APK的核心代码如下:
// 加载Plugin.apk插件
PluginManager pluginManager = PluginManager.getInstance(this);
// 此处是当查看插件apk是否存在,如果存在就去加载(比如修改线上的bug,把插件apk下载到sdcard的根目录下取名为Plugin.apk)
File file = new File(getExternalStorageDirectory(), "Plugin.apk");
if (file.exists()) {
try {
pluginManager.loadPlugin(file);
} catch (Exception e) {
e.printStackTrace();
}
}
PluginManager的loadPlugin方法:
/** * load a plugin into memory, then invoke it's Application. * @param apk the file of plugin, should end with .apk * @throws Exception */
public void loadPlugin(File apk) throws Exception {
if (null == apk) {
throw new IllegalArgumentException("error : apk is null.");
}
if (!apk.exists()) {
throw new FileNotFoundException(apk.getAbsolutePath());
}
LoadedPlugin plugin = LoadedPlugin.create(this, this.mContext, apk);
if (null != plugin) {
this.mPlugins.put(plugin.getPackageName(), plugin);
// try to invoke plugin's application
plugin.invokeApplication();
} else {
throw new RuntimeException("Can't load plugin which is invalid: " + apk.getAbsolutePath());
}
}
调用了LoadedPlugin.create方法,生成一个LoadedPlugin对象,这个对象里面就包含了插件APK的packageInfo,资源相关(AssetManager,Resources),DexClassLoader(加载类),四大组件相关集合(mActivityInfos,mServiceInfos,mReceiverInfos,mProviderInfos)
LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws PackageParser.PackageParserException {
this.mPluginManager = pluginManager;
this.mHostContext = context;
this.mLocation = apk.getAbsolutePath();
this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
this.mPackageInfo = new PackageInfo();
this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
this.mPackageInfo.signatures = this.mPackage.mSignatures;
this.mPackageInfo.packageName = this.mPackage.packageName;
if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) {
throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName);
}
this.mPackageInfo.versionCode = this.mPackage.mVersionCode;
this.mPackageInfo.versionName = this.mPackage.mVersionName;
this.mPackageInfo.permissions = new PermissionInfo[0];
this.mPackageManager = new PluginPackageManager();
this.mPluginContext = new PluginContext(this);
this.mNativeLibDir = context.getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
this.mResources = createResources(context, apk);
this.mAssets = this.mResources.getAssets();
this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
tryToCopyNativeLib(apk);
// Cache instrumentations
Map<ComponentName, InstrumentationInfo> instrumentations = new HashMap<ComponentName, InstrumentationInfo>();
for (PackageParser.Instrumentation instrumentation : this.mPackage.instrumentation) {
instrumentations.put(instrumentation.getComponentName(), instrumentation.info);
}
this.mInstrumentationInfos = Collections.unmodifiableMap(instrumentations);
this.mPackageInfo.instrumentation = instrumentations.values().toArray(new InstrumentationInfo[instrumentations.size()]);
// Cache activities
Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity activity : this.mPackage.activities) {
activityInfos.put(activity.getComponentName(), activity.info);
}
this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);
// Cache services
Map<ComponentName, ServiceInfo> serviceInfos = new HashMap<ComponentName, ServiceInfo>();
for (PackageParser.Service service : this.mPackage.services) {
serviceInfos.put(service.getComponentName(), service.info);
}
this.mServiceInfos = Collections.unmodifiableMap(serviceInfos);
this.mPackageInfo.services = serviceInfos.values().toArray(new ServiceInfo[serviceInfos.size()]);
// Cache providers
Map<String, ProviderInfo> providers = new HashMap<String, ProviderInfo>();
Map<ComponentName, ProviderInfo> providerInfos = new HashMap<ComponentName, ProviderInfo>();
for (PackageParser.Provider provider : this.mPackage.providers) {
providers.put(provider.info.authority, provider.info);
providerInfos.put(provider.getComponentName(), provider.info);
}
this.mProviders = Collections.unmodifiableMap(providers);
this.mProviderInfos = Collections.unmodifiableMap(providerInfos);
this.mPackageInfo.providers = providerInfos.values().toArray(new ProviderInfo[providerInfos.size()]);
// Register broadcast receivers dynamically
Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity receiver : this.mPackage.receivers) {
receivers.put(receiver.getComponentName(), receiver.info);
try {
BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
this.mHostContext.registerReceiver(br, aii);
}
} catch (Exception e) {
e.printStackTrace();
}
}
this.mReceiverInfos = Collections.unmodifiableMap(receivers);
this.mPackageInfo.receivers = receivers.values().toArray(new ActivityInfo[receivers.size()]);
}
private void tryToCopyNativeLib(File apk) {
Bundle metaData = this.mPackageInfo.applicationInfo.metaData;
if (metaData != null && metaData.getBoolean("VA_IS_HAVE_LIB")) {
PluginUtil.copyNativeLib(apk, mHostContext, mPackageInfo, mNativeLibDir);
}
}
替换Activity
好了,插件APK已经加载完成了,接下来就是启动插件APK的Acitivity,插件Activity必然没有在宿主中注册,这样启动不会报错么?当然是肯定的,我们看看startActivity做了什么操作:
跟进startActivity的调用流程,会发现其最终会进入Instrumentation的execStartActivity方法,在execStartActivity方法中有checkStartActivityResult方法:
这个错误的提示大家应该很熟悉,就是Activity没有再AndroidManifest.xml注册过提示的错误。那么怎么解决呢?我们可以在checkStartActivityResult之前,提前将Activity的ComponentName进行替换为占坑的名字不就好了么?我们看到检查的操作时发生在Instrumentation.java中,那么我们选择hook Instrumentation。回过头来看PluginManager.java
private void hookInstrumentationAndHandler() {
try {
Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
if (baseInstrumentation.getClass().getName().contains("lbe")) {
// reject executing in paralell space, for example, lbe.
System.exit(0);
}
final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);
Object activityThread = ReflectUtil.getActivityThread(this.mContext);
ReflectUtil.setInstrumentation(activityThread, instrumentation);
ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
this.mInstrumentation = instrumentation;
} catch (Exception e) {
e.printStackTrace();
}
}
可以看到首先通过反射拿到了原本的Instrumentation对象,然后自己创建了一个VAInstrumentation对象,接下来就直接反射将VAInstrumentation对象设置给ActivityThread对象即可。
这样就完成了hook Instrumentation,之后调用Instrumentation的任何方法,都可以在VAInstrumentation进行拦截并做一些修改。上面说到,startActivity会最终执行Instrumentation的execStartActivity方法,由于我们又直接hook Instrumentation,所以直接看VAInstrumentation的execStartActivity方法:
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
// null component is an implicitly intent
if (intent.getComponent() != null) {
Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),
intent.getComponent().getClassName()));
// resolve intent with Stub Activity if needed
this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
}
ActivityResult result = realExecStartActivity(who, contextThread, token, target,
intent, requestCode, options);
return result;
}
直接看markIntentIfNeeded方法:
public void markIntentIfNeeded(Intent intent) {
if (intent.getComponent() == null) {
return;
}
String targetPackageName = intent.getComponent().getPackageName();
String targetClassName = intent.getComponent().getClassName();
// search map and return specific launchmode stub activity
if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
intent.putExtra(Constants.KEY_IS_PLUGIN, true);
intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
dispatchStubActivity(intent);
}
}
private void dispatchStubActivity(Intent intent) {
ComponentName component = intent.getComponent();
String targetClassName = intent.getComponent().getClassName();
LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
ActivityInfo info = loadedPlugin.getActivityInfo(component);
if (info == null) {
throw new RuntimeException("can not find " + component);
}
int launchMode = info.launchMode;
Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
themeObj.applyStyle(info.theme, true);
String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
intent.setClassName(mContext, stubActivity);
}
可以直接看最后一行,intent通过setClassName替换启动的目标Activity了!这个stubActivity是由mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj)返回。
很明显,传入的参数launchMode、themeObj都是决定选择哪一个占坑类用的。
public String getStubActivity(String className, int launchMode, Theme theme) {
String stubActivity= mCachedStubActivity.get(className);
if (stubActivity != null) {
return stubActivity;
}
TypedArray array = theme.obtainStyledAttributes(new int[]{
android.R.attr.windowIsTranslucent,
android.R.attr.windowBackground
});
boolean windowIsTranslucent = array.getBoolean(0, false);
array.recycle();
if (Constants.DEBUG) {
Log.d("StubActivityInfo", "getStubActivity, is transparent theme ? " + windowIsTranslucent);
}
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
switch (launchMode) {
case ActivityInfo.LAUNCH_MULTIPLE: {
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
if (windowIsTranslucent) {
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
}
break;
}
case ActivityInfo.LAUNCH_SINGLE_TOP: {
usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
break;
}
case ActivityInfo.LAUNCH_SINGLE_TASK: {
usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
break;
}
case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
break;
}
default:break;
}
mCachedStubActivity.put(className, stubActivity);
return stubActivity;
}
通过launchMode去选择占坑的类。
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
STUB_ACTIVITY_STANDARD值为”%s.A$%d”, corePackage值为com.didi.virtualapk.core,usedStandardStubActivity为数字值,所以最终类名格式为:com.didi.virtualapk.core.A1
再看一眼,VirtualAPK中的AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.didi.virtualapk.core" android:versionCode="1" android:versionName="1.0.0" >
<uses-sdk android:minSdkVersion="15" android:targetSdkVersion="15" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application>
<!-- Stub Activities -->
<activity android:name="com.didi.virtualapk.core.A$1" android:launchMode="standard" />
<activity android:name="com.didi.virtualapk.core.A$2" android:launchMode="standard" android:theme="@android:style/Theme.Translucent" />
<!-- Stub Activities -->
<activity android:name="com.didi.virtualapk.core.B$1" android:launchMode="singleTop" />
<activity android:name="com.didi.virtualapk.core.B$2" android:launchMode="singleTop" />
<activity android:name="com.didi.virtualapk.core.B$3" android:launchMode="singleTop" />
<activity android:name="com.didi.virtualapk.core.B$4" android:launchMode="singleTop" />
<activity android:name="com.didi.virtualapk.core.B$5" android:launchMode="singleTop" />
<activity android:name="com.didi.virtualapk.core.B$6" android:launchMode="singleTop" />
<activity android:name="com.didi.virtualapk.core.B$7" android:launchMode="singleTop" />
<activity android:name="com.didi.virtualapk.core.B$8" android:launchMode="singleTop" />
<!-- Stub Activities -->
<activity android:name="com.didi.virtualapk.core.C$1" android:launchMode="singleTask" />
<activity android:name="com.didi.virtualapk.core.C$2" android:launchMode="singleTask" />
<activity android:name="com.didi.virtualapk.core.C$3" android:launchMode="singleTask" />
<activity android:name="com.didi.virtualapk.core.C$4" android:launchMode="singleTask" />
<activity android:name="com.didi.virtualapk.core.C$5" android:launchMode="singleTask" />
<activity android:name="com.didi.virtualapk.core.C$6" android:launchMode="singleTask" />
<activity android:name="com.didi.virtualapk.core.C$7" android:launchMode="singleTask" />
<activity android:name="com.didi.virtualapk.core.C$8" android:launchMode="singleTask" />
<!-- Stub Activities -->
<activity android:name="com.didi.virtualapk.core.D$1" android:launchMode="singleInstance" />
<activity android:name="com.didi.virtualapk.core.D$2" android:launchMode="singleInstance" />
<activity android:name="com.didi.virtualapk.core.D$3" android:launchMode="singleInstance" />
<activity android:name="com.didi.virtualapk.core.D$4" android:launchMode="singleInstance" />
<activity android:name="com.didi.virtualapk.core.D$5" android:launchMode="singleInstance" />
<activity android:name="com.didi.virtualapk.core.D$6" android:launchMode="singleInstance" />
<activity android:name="com.didi.virtualapk.core.D$7" android:launchMode="singleInstance" />
<activity android:name="com.didi.virtualapk.core.D$8" android:launchMode="singleInstance" />
<!-- Local Service running in main process -->
<service android:name="com.didi.virtualapk.delegate.LocalService" />
<!-- Daemon Service running in child process -->
<service android:name="com.didi.virtualapk.delegate.RemoteService" android:process=":daemon" >
<intent-filter>
<action android:name="${applicationId}.intent.ACTION_DAEMON_SERVICE" />
</intent-filter>
</service>
<provider android:name="com.didi.virtualapk.delegate.RemoteContentProvider" android:authorities="${applicationId}.VirtualAPK.Provider" android:process=":daemon" />
</application>
</manifest>
到这里就可以看到,替换我们启动的Activity为占坑Activity,将我们原本启动的包名,类名存储到了Intent中。
还原Activity
我们用占坑的Activity替换了插件的Activity,但占坑的Activity不是我们最终要启动的目标Activity。这里需要了解下Activity的启动流程,这里省略很多很多字,具体的Activity的启动流程可以去看看源码或者其他的博客,这里不展开描述,可以理解为,
最终会回调到Instrumentation.newActivity(),那么直接看VAInstrumentation的newActivity方法:
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
try {
cl.loadClass(className);
} catch (ClassNotFoundException e) {
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
String targetClassName = PluginUtil.getTargetActivity(intent);
Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName));
if (targetClassName != null) {
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
try {
// for 4.1+
ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
} catch (Exception ignored) {
// ignored.
}
return activity;
}
}
return mBase.newActivity(cl, className, intent);
}
核心就是首先从intent中取出我们的目标Activity,然后通过plugin的ClassLoader去加载(还记得在加载插件时,会生成一个LoadedPlugin对象,其中会对应其初始化一个DexClassLoader)。
这样就完成了Activity的“偷梁换柱”。
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
final Intent intent = activity.getIntent();
if (PluginUtil.isIntentFromPlugin(intent)) {
Context base = activity.getBaseContext();
try {
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
ReflectUtil.setField(base.getClass(), base, "mResources", plugin.getResources());
ReflectUtil.setField(ContextWrapper.class, activity, "mBase", plugin.getPluginContext());
ReflectUtil.setField(Activity.class, activity, "mApplication", plugin.getApplication());
ReflectUtil.setFieldNoException(ContextThemeWrapper.class, activity, "mBase", plugin.getPluginContext());
// set screenOrientation
ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
activity.setRequestedOrientation(activityInfo.screenOrientation);
}
} catch (Exception e) {
e.printStackTrace();
}
}
mBase.callActivityOnCreate(activity, icicle);
}
这个方法中设置了前面获取到的mResources、mBase(Context)、mApplication对象和屏幕方向等等。
至此,插件APK的Activity的启动就完成了!