而Activity是否存在的校验是发生在AMS端,所以我们在于AMS交互前,提前将Activity的ComponentName进行替换为占坑的名字不就好了么?
这里可以选择hook Instrumentation,或者ActivityManagerProxy都可以达到目标,VirtualAPK选择了hook Instrumentation.
打开PluginManager
可以看到如下方法:
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
对象,拿的过程是首先拿到ActivityThread,由于ActivityThread可以通过静态变量sCurrentActivityThread
或者静态方法currentActivityThread()
获取,所以拿到其对象相当轻松。拿到ActivityThread对象后,调用其getInstrumentation()
方法,即可获取当前的Instrumentation对象。
然后自己创建了一个VAInstrumentation对象,接下来就直接反射将VAInstrumentation对象设置给ActivityThread对象即可。
这样就完成了hook Instrumentation,之后调用Instrumentation的任何方法,都可以在VAInstrumentation进行拦截并做一些修改。
这里还hook了ActivityThread的mH类的Callback,暂不赘述。
刚才说了,可以通过Instrumentation的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;
}
首先调用transformIntentToExplicitAsNeeded,这个主要是当component为null时,根据启动Activity时,配置的action,data,category等去已加载的plugin中匹配到确定的Activity的。
本例我们的写法ComponentName肯定不为null,所以直接看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);
}
}
在该方法中判断如果启动的是插件中类,则将启动的包名和Activity类名存到了intent中,可以看到这里存储明显是为了后面恢复用的。
然后调用了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;
}
// 省略LAUNCH_SINGLE_TASK,LAUNCH_SINGLE_INSTANCE
}
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.A$1
再看一眼,CoreLibrary下的AndroidManifest中:
<activity android:name=“.A$2” android:launchMode=“standard”
android:theme=“@android:style/Theme.Translucent” />
// 省略很多…
就完全明白了。
到这里就可以看到,替换我们启动的Activity为占坑Activity,将我们原本启动的包名,类名存储到了Intent中。
这样做只完成了一半,为什么这么说呢?
(2) 还原Activity
因为欺骗过了AMS,AMS执行完成后,最终要启动的不可能是占坑Activity,还应该是我们的启动的目标Activity呀。
这里需要知道Activity的启动流程:
AMS在处理完启动Activity后,会调用:app.thread.scheduleLaunchActivity
,这里的thread对应的server端未我们ActivityThread中的ApplicationThread对象(binder可以理解有一个client端和一个server端),所以会调用ApplicationThread.scheduleLaunchActivity
方法,在其内部会调用mH类的sendMessage方法,传递的标识为H.LAUNCH_ACTIVITY
,进入调用到ActivityThread的handleLaunchActivity方法->ActivityThread#handleLaunchActivity->mInstrumentation.newActivity()。
ps:这里流程不清楚没关系,暂时理解为最终会回调到Instrumentation的newActivity方法即可,细节可以自己去查看结合老罗的blog理解。
关键的来了,最终又到了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);
if (targetClassName != null) {
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
// 省略兼容性处理代码
return activity;
}
}
return mBase.newActivity(cl, className, intent);
}
核心就是首先从intent中取出我们的目标Activity,然后通过plugin的ClassLoader去加载(还记得在加载插件时,会生成一个LoadedPlugin对象,其中会对应其初始化一个DexClassLoader)。
这样就完成了Activity的“偷梁换柱”。
还没完,接下来在callActivityOnCreate
方法中:
@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对象。以及设置一些可动态设置的属性,这里仅设置了屏幕方向。
这里提一下,将mBase替换为PluginContext,可以修改Resources、AssetManager以及拦截相当多的操作。
看一眼代码就清楚了:
原本Activity的部分get操作
ContextWrapper
@Override
public AssetManager getAssets() {
return mBase.getAssets();
}
@Override
public Resources getResources()
{
return mBase.getResources();
}
@Override
public PackageManager getPackageManager() {
return mBase.getPackageManager();
}
@Override
public ContentResolver getContentResolver() {
return mBase.getContentResolver();
}
直接替换为:
PluginContext
@Override
public Resources getResources() {
return this.mPlugin.getResources();
}
@Override
public AssetManager getAssets() {
return this.mPlugin.getAssets();
}
@Override
public ContentResolver getContentResolver() {
return new PluginContentResolver(getHostContext());
}
看得出来还是非常巧妙的。可以做的事情也非常多,后面对ContentProvider的描述也会提现出来。
好了,到此Activity就可以正常启动了。
下面看Service。
三、Service的支持
Service和Activity有点不同,显而易见的首先我们也会将要启动的Service类替换为占坑的Service类,但是有一点不同,在Standard模式下多次启动同一个占坑Activity会创建多个对象来对象我们的目标类。而Service多次启动只会调用onStartCommond方法,甚至常规多次调用bindService,seviceConn对象不变,甚至都不会多次回调bindService方法(多次调用可以通过给Intent设置不同Action解决)。
还有一点,最明显的差异是,Activity的生命周期是由用户交互决定的,而Service的声明周期是我们主动通过代码调用的。
也就是说,start、stop、bind、unbind都是我们显示调用的,所以我们可以拦截这几个方法,做一些事情。
Virtual Apk的做法,即将所有的操作进行拦截,都改为startService,然后统一在onStartCommond中分发。
下面看详细代码:
(1) hook IActivityManager
再次来到PluginManager,发下如下方法:
private void hookSystemServices() {
try {
Singleton defaultSingleton = (Singleton) ReflectUtil.getField(ActivityManagerNative.class, null, “gDefault”);
IActivityManager activityManagerProxy = ActivityManagerProxy.newInstance(this, defaultSingleton.get());
// Hook IActivityManager from ActivityManagerNative
ReflectUtil.setField(defaultSingleton.getClass().getSuperclass(), defaultSingleton, “mInstance”, activityManagerProxy);
if (defaultSingleton.get() == activityManagerProxy) {
this.mActivityManager = activityManagerProxy;
}
} catch (Exception e) {
e.printStackTrace();
}
}
首先拿到ActivityManagerNative中的gDefault对象,该对象返回的是一个Singleton<IActivityManager>
,然后拿到其mInstance对象,即IActivityManager对象(可以理解为和AMS交互的binder的client对象)对象。
然后通过动态代理的方式,替换为了一个代理对象。
那么重点看对应的InvocationHandler对象即可,该代理对象调用的方法都会辗转到其invoke方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (“startService”.equals(method.getName())) {
try {
return startService(proxy, method, args);
} catch (Throwable e) {
Log.e(TAG, “Start service error”, e);
}
} else if (“stopService”.equals(method.getName())) {
try {
return stopService(proxy, method, args);
} catch (Throwable e) {
Log.e(TAG, “Stop Service error”, e);
}
} else if (“stopServiceToken”.equals(method.getName())) {
try {
return stopServiceToken(proxy, method, args);
} catch (Throwable e) {
Log.e(TAG, “Stop service token error”, e);
}
}
// 省略bindService,unbindService等方法
}
当我们调用startService时,跟进代码,可以发现调用流程为:
startService->startServiceCommon->ActivityManagerNative.getDefault().startService
这个getDefault刚被我们hook,所以会被上述方法拦截,然后调用:startService(proxy, method, args)
private Object startService(Object proxy, Method method, Object[] args) throws Throwable {
IApplicationThread appThread = (IApplicationThread) args[0];
Intent target = (Intent) args[1];
ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);
if (null == resolveInfo || null == resolveInfo.serviceInfo) {
// is host service
return method.invoke(this.mActivityManager, args);
}
return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
}
先不看代码,考虑下我们这里唯一要做的就是通过Intent保存关键数据,替换启动的Service类为占坑类。
所以直接看最后的方法:
private ComponentName startDelegateServiceForTarget(Intent target,
ServiceInfo serviceInfo,
Bundle extras, int command) {
Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
return mPluginManager.getHostContext().startService(wrapperIntent);
}
最后一行就是启动了,那么替换的操作应该在wrapperTargetIntent中完成:
private Intent wrapperTargetIntent(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
// fill in service with ComponentName
target.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation();
// start delegate service to run plugin service inside
boolean local = PluginUtil.isLocalService(serviceInfo);
Class<? extends Service> delegate = local ? LocalService.class : RemoteService.class;
Intent intent = new Intent();
intent.setClass(mPluginManager.getHostContext(), delegate);
intent.putExtra(RemoteService.EXTRA_TARGET, target);
intent.putExtra(RemoteService.EXTRA_COMMAND, command);
intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);
if (extras != null) {
intent.putExtras(extras);
}
return intent;
}
果不其然,重新初始化了Intent,设置了目标类为LocalService(多进程时设置为RemoteService),然后将原本的Intent存储到EXTRA_TARGET
,携带command为EXTRA_COMMAND_START_SERVICE
,以及插件apk路径。
(2)代理分发
那么接下来代码就到了LocalService的onStartCommond中啦:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 省略一些代码…
Intent target = intent.getParcelableExtra(EXTRA_TARGET);
int command = intent.getIntExtra(EXTRA_COMMAND, 0);
if (null == target || command <= 0) {
return START_STICKY;
}
ComponentName component = target.getComponent();
LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);
switch (command) {
case EXTRA_COMMAND_START_SERVICE: {
ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
IApplicationThread appThread = mainThread.getApplicationThread();
Service service;
if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
service = this.mPluginManager.getComponentsHandler().getService(component);
} else {
try {
service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();
Application app = plugin.getApplication();
IBinder token = appThread.asBinder();
Method attach = service.getClass().getMethod(“attach”, Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
IActivityManager am = mPluginManager.getActivityManager();
attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
service.onCreate();
this.mPluginManager.getComponentsHandler().rememberService(component, service);
} catch (Throwable t) {
return START_STICKY;
}
}
service.onStartCommand(target, 0, this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());
break;
}
// 省略下面的代码
case EXTRA_COMMAND_BIND_SERVICE:break;
case EXTRA_COMMAND_STOP_SERVICE:break;
case EXTRA_COMMAND_UNBIND_SERVICE:break;
}
这里代码很简单了,根据command类型,比如EXTRA_COMMAND_START_SERVICE
,直接通过plugin的ClassLoader去load目标Service的class,然后反射创建实例。比较重要的是,Service创建好后,需要调用它的attach方法,这里凑够参数,然后反射调用即可,最后调用onCreate、onStartCommand收工。然后将其保存起来,stop的时候取出来调用其onDestroy即可。
bind、unbind以及stop的代码与上述基本一致,不在赘述。
唯一提醒的就是,刚才看到还hook了一个方法叫做:stopServiceToken
,该方法是什么时候用的呢?
主要有一些特殊的Service,比如IntentService,其stopSelf是由自身调用的,最终会调用mActivityManager.stopServiceToken
方法,同样的中转为STOP操作即可。
四、BroadcastReceiver的支持
这个比较简单,直接解析Manifest后,静态转动态即可。
相关代码在LoadedPlugin的构造方法中:
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();
}
}
可以看到解析到receiver信息后,直接通过pluginClassloader去loadClass拿到receiver对象,然后调用this.mHostContext.registerReceiver即可。
开心,最后一个了~
五、ContentProvider的支持
(1)hook IContentProvider
ContentProvider的支持依然是通过代理分发。
看一段CP使用的代码:
Cursor bookCursor = getContentResolver().query(bookUri, new String[]{“_id”, “name”}, null, null, null);
这里用到了PluginContext,在生成Activity、Service的时候,为其设置的Context都为PluginContext对象。
所以当你调用getContentResolver时,调用的为PluginContext的getContentResolver。
@Override
public ContentResolver getContentResolver() {
return new PluginContentResolver(getHostContext());
}
返回的是一个PluginContentResolver对象,当我们调用query方法时,会辗转调用到
ContentResolver.acquireUnstableProvider
方法。该方法被PluginContentResolver中复写:
protected IContentProvider acquireUnstableProvider(Context context, String auth) {
try {
if (mPluginManager.resolveContentProvider(auth, 0) != null) {
return mPluginManager.getIContentProvider();
}
return (IContentProvider) sAcquireUnstableProvider.invoke(mBase, context, auth);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
如果调用的auth为插件apk中的provider,则直接返回mPluginManager.getIContentProvider()
。
public synchronized IContentProvider getIContentProvider() {
if (mIContentProvider == null) {
hookIContentProviderAsNeeded();
}
return mIContentProvider;
}
咦,又看到一个hook方法:
private void hookIContentProviderAsNeeded() {
Uri uri = Uri.parse(PluginContentResolver.getUri(mContext));
mContext.getContentResolver().call(uri, “wakeup”, null, null);
try {
最后送福利了,现在关注我可以获取包含源码解析,自定义View,动画实现,架构分享等。
内容难度适中,篇幅精炼,每天只需花上十几分钟阅读即可。
大家可以跟我一起探讨,有flutter—底层开发—性能优化—移动架构—资深UI工程师 —NDK相关专业人员和视频教学资料,还有更多面试题等你来拿
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
lver中复写:
protected IContentProvider acquireUnstableProvider(Context context, String auth) {
try {
if (mPluginManager.resolveContentProvider(auth, 0) != null) {
return mPluginManager.getIContentProvider();
}
return (IContentProvider) sAcquireUnstableProvider.invoke(mBase, context, auth);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
如果调用的auth为插件apk中的provider,则直接返回mPluginManager.getIContentProvider()
。
public synchronized IContentProvider getIContentProvider() {
if (mIContentProvider == null) {
hookIContentProviderAsNeeded();
}
return mIContentProvider;
}
咦,又看到一个hook方法:
private void hookIContentProviderAsNeeded() {
Uri uri = Uri.parse(PluginContentResolver.getUri(mContext));
mContext.getContentResolver().call(uri, “wakeup”, null, null);
try {
最后送福利了,现在关注我可以获取包含源码解析,自定义View,动画实现,架构分享等。
内容难度适中,篇幅精炼,每天只需花上十几分钟阅读即可。
大家可以跟我一起探讨,有flutter—底层开发—性能优化—移动架构—资深UI工程师 —NDK相关专业人员和视频教学资料,还有更多面试题等你来拿
[外链图片转存中…(img-2WZJrpgf-1715339755195)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!