滴滴插件化方案 VirtualApk 源码解析(1)

}

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 {

Field authority = null;

Field mProvider = null;

ActivityThread activityThread = (ActivityThread) ReflectUtil.getActivityThread(mContext);

Map mProviderMap = (Map) ReflectUtil.getField(activityThread.getClass(), activityThread, “mProviderMap”);

Iterator iter = mProviderMap.entrySet().iterator();

while (iter.hasNext()) {

Map.Entry entry = (Map.Entry) iter.next();

Object key = entry.getKey();

Object val = entry.getValue();

String auth;

if (key instanceof String) {

auth = (String) key;

} else {

if (authority == null) {

authority = key.getClass().getDeclaredField(“authority”);

authority.setAccessible(true);

}

auth = (String) authority.get(key);

}

if (auth.equals(PluginContentResolver.getAuthority(mContext))) {

if (mProvider == null) {

mProvider = val.getClass().getDeclaredField(“mProvider”);

mProvider.setAccessible(true);

}

IContentProvider rawProvider = (IContentProvider) mProvider.get(val);

IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider);

mIContentProvider = proxy;

Log.d(TAG, "hookIContentProvider succeed : " + mIContentProvider);

break;

}

}

} catch (Exception e) {

e.printStackTrace();

}

}

前两行比较重要,第一行是拿到了占坑的provider的uri,然后主动调用了其call方法。

如果你跟进去,会发现,其会调用acquireProvider->mMainThread.acquireProvider->ActivityManagerNative.getDefault().getContentProvider->installProvider。简单来说,其首先调用已经注册provider,得到返回的IContentProvider对象。

这个IContentProvider对象是在ActivityThread.installProvider方法中加入到mProviderMap中。

而ActivityThread对象又容易获取,mProviderMap又是它成员变量,那么也容易获取,所以上面的一大坨(除了前两行)代码,就为了拿到占坑的provider对应的IContentProvider对象。

然后通过动态代理的方式,进行了hook,关注InvocationHandler的实例IContentProviderProxy。

IContentProvider能干吗呢?其实就能拦截我们正常的query、insert、update、delete等操作。

拦截这些方法干嘛?

当然是修改uri啦,把用户调用的uri,替换为占坑provider的uri,再把原本的uri作为参数拼接在占坑provider的uri后面即可。

好了,直接看invoke方法:

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

Log.v(TAG, method.toGenericString() + " : " + Arrays.toString(args));

wrapperUri(method, args);

try {

return method.invoke(mBase, args);

} catch (InvocationTargetException e) {

throw e.getTargetException();

}

}

直接看wrapperUri

private void wrapperUri(Method method, Object[] args) {

Uri uri = null;

int index = 0;

if (args != null) {

for (int i = 0; i < args.length; i++) {

if (args[i] instanceof Uri) {

uri = (Uri) args[i];

index = i;

break;

}

}

}

// 省略部分代码

PluginManager pluginManager = PluginManager.getInstance(mContext);

ProviderInfo info = pluginManager.resolveContentProvider(uri.getAuthority(), 0);

if (info != null) {

String pkg = info.packageName;

LoadedPlugin plugin = pluginManager.getLoadedPlugin(pkg);

String pluginUri = Uri.encode(uri.toString());

StringBuilder builder = new StringBuilder(PluginContentResolver.getUri(mContext));

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

题外话

不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊

这里我为大家准备了一些我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~

欢迎评论区讨论。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

der builder = new StringBuilder(PluginContentResolver.getUri(mContext));

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-E4rmvQrJ-1713682268520)]

[外链图片转存中…(img-ci4dNfPC-1713682268521)]

[外链图片转存中…(img-KLOQMkKw-1713682268522)]

[外链图片转存中…(img-gz5TnMNm-1713682268523)]

[外链图片转存中…(img-4QOrOey7-1713682268524)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

题外话

不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊

这里我为大家准备了一些我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~

[外链图片转存中…(img-11uMvEzT-1713682268525)]

欢迎评论区讨论。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值