写点什么

【中级—,一线互联网架构师设计思想解读开源框架

用户头像
Android架构
关注
发布于: 刚刚

可以看到优先根据包名判断该插件是否已经加载,所以在插件使用前其实还需要调用


pluginManager.loadPlugin(apk);


加载插件。


这里就不赘述源码了,大致为调用PackageParser.parsePackage解析 apk,获得该 apk 对应的 PackageInfo,资源相关(AssetManager,Resources),DexClassLoader(加载类),四大组件相关集合(mActivityInfos,mServiceInfos,mReceiverInfos,mProviderInfos),针对 Plugin 的 PluginContext 等一堆信息,封装为 LoadedPlugin 对象。


详细可以参考com.didi.virtualapk.internal.LoadedPlugin类。


ok,如果该插件以及加载过,则直接通过 startActivity 去启动插件中目标 Activity。

(1)替换 Activity

这里大家肯定会有疑惑,该 Activity 必然没有在 Manifest 中注册,这么启动不会报错吗?


正常肯定会报错呀,所以我们看看它是怎么做的吧。


跟进 startActivity 的调用流程,会发现其最终会进入 Instrumentation 的 execStartActivity 方法,然后再通过 ActivityManagerProxy 与 AMS 进行交互。


而 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 intentif (intent.getComponent() != null) {Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),intent.getComponent().getClassName()));// resolve intent with Stub Activity if neededthis.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 activityif (!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$1" android:launchMode="standard"/><activity android:name="


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


.A$2" android:launchMode="standard"android:theme="@android:style/Theme.Translucent" />


<!-- Stub Activities --><activity android:name=".B2" android:launchMode="singleTop"/><activity android:name=".B$3" android:launchMode="singleTop"/>// 省略很多... 123456789123456789


就完全明白了。


到这里就可以看到,替换我们启动的 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 方法:


@Overridepublic 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方法中:


@Overridepublic 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 screenOrientationActivityInfo 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

@Overridepublic AssetManager getAssets() {return mBase.getAssets();}


@Overridepublic Resources getResources(){return mBase.getResources();}


@Overridepublic PackageManager getPackageManager() {return mBase.getPackageManager();}


@Overridepublic ContentResolver getContentResolver() {return mBase.getContentResolver();}


直接替换为:

PluginContext

@Overridepublic Resources getResources() {return this.mPlugin.getResources();}


@Overridepublic AssetManager getAssets() {return this.mPlugin.getAssets();}


@Overridepublic 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<IActivityManager> defaultSingleton = (Singleton<IActivityManager>) ReflectUtil.getField(ActivityManagerNative.class, null, "gDefault");IActivityManager activityManagerProxy = ActivityManagerProxy.newInstance(this, defaultSingleton.get());


// Hook IActivityManager from ActivityManagerNativeReflectUtil.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 方法:


@Overridepublic 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 servicereturn 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 ComponentNametarget.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation();


// start delegate service to run plugin service insideboolean 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 中啦:


@Overridepublic 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,该方法是什么时候用的呢?

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
【中级—,一线互联网架构师设计思想解读开源框架