滴滴插件化方案 VirtualApk 源码解析

2023-11-07 18:32

本文主要是介绍滴滴插件化方案 VirtualApk 源码解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

       

本文已在我的公众号hongyangAndroid原创首发。
  转载请标明出处: 
  http://blog.csdn.net/lmj623565791/article/details/75000580 
  本文出自张鸿洋的博客

   

本文已在我的公众号hongyangAndroid原创首发,文章合集。

一、概述

之前一直没有写过插件化相关的博客,刚好最近滴滴和360分别开源了自家的插件化方案,赶紧学习下,写两篇博客,第一篇是滴滴的方案:

  • https://github.com/didi/VirtualAPK

那么其中的难点很明显是对四大组件支持,因为大家都清楚,四大组件都是需要在AndroidManifest中注册的,而插件apk中的组件是不可能预先知晓名字,提前注册中宿主apk中的,所以现在基本都采用一些hack方案类解决,VirtualAPK大体方案如下:

  • Activity:在宿主apk中提前占几个坑,然后通过“欺上瞒下”(这个词好像是360之前的ppt中提到)的方式,启动插件apk的Activity;因为要支持不同的launchMode以及一些特殊的属性,需要占多个坑。
  • Service:通过代理Service的方式去分发;主进程和其他进程,VirtualAPK使用了两个代理Service。
  • BroadcastReceiver:静态转动态
  • ContentProvider:通过一个代理Provider进行分发。

这些占坑的数量并不是固定的,比如Activity想支持某个属性,该属性不能动态设置,只能在Manifest中设置,那就需要去占坑支持。所以占坑数量这些,可以根据自己的需求进行调整。

下面就逐一去分析代码啦~

注:本篇博客涉及到的framework逻辑,为API 22.
 
分期版本为 com.didi.virtualapk:core:0.9.0

二、Activity的支持

这里就不按照某个流程一行行代码往下读了,针对性的讲一些关键流程,可能更好阅读一些。

首先看一段启动插件Activity的代码:

final String pkg = "com.didi.virtualapk.demo";if (PluginManager.getInstance(this).getLoadedPlugin(pkg) == null) {    Toast.makeText(this, "plugin [com.didi.virtualapk.demo] not loaded", Toast.LENGTH_SHORT).show();    return;}// test Activity and ServiceIntent intent = new Intent();intent.setClassName(pkg, "com.didi.virtualapk.demo.aidl.BookManagerActivity");startActivity(intent);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

pluginManager.loadPlugin(apk);
  • 1

加载插件。

这里就不赘述源码了,大致为调用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();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

可以看到首先通过反射拿到了原本的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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

首先调用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);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在该方法中判断如果启动的是插件中类,则将启动的包名和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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

可以直接看最后一行,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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

可以看到主要就是根据launchMode去选择不同的占坑类。
例如:

stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
  • 1

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=".A$2" android:launchMode="standard"    android:theme="@android:style/Theme.Translucent" /><!-- Stub Activities --><activity android:name=".B$1" android:launchMode="singleTop"/><activity android:name=".B$2" android:launchMode="singleTop"/><activity android:name=".B$3" android:launchMode="singleTop"/>// 省略很多...    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

就完全明白了。

到这里就可以看到,替换我们启动的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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

核心就是首先从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 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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

设置了修改了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();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

直接替换为:

# PluginContext@Overridepublic Resources getResources() {    return this.mPlugin.getResources();}@Overridepublic AssetManager getAssets() {    return this.mPlugin.getAssets();}@Overridepublic ContentResolver getContentResolver() {    return new PluginContentResolver(getHostContext());}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

看得出来还是非常巧妙的。可以做的事情也非常多,后面对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 ActivityManagerNative        ReflectUtil.setField(defaultSingleton.getClass().getSuperclass(), defaultSingleton, "mInstance", activityManagerProxy);        if (defaultSingleton.get() == activityManagerProxy) {            this.mActivityManager = activityManagerProxy;        }    } catch (Exception e) {        e.printStackTrace();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

首先拿到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等方法}    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

当我们调用startService时,跟进代码,可以发现调用流程为:

startService->startServiceCommon->ActivityManagerNative.getDefault().startService
  • 1

这个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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

先不看代码,考虑下我们这里唯一要做的就是通过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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最后一行就是启动了,那么替换的操作应该在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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

果不其然,重新初始化了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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

这里代码很简单了,根据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();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

可以看到解析到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);
  • 1
  • 2

这里用到了PluginContext,在生成Activity、Service的时候,为其设置的Context都为PluginContext对象。

所以当你调用getContentResolver时,调用的为PluginContext的getContentResolver。

@Overridepublic ContentResolver getContentResolver() {    return new PluginContentResolver(getHostContext());}
  • 1
  • 2
  • 3
  • 4

返回的是一个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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

如果调用的auth为插件apk中的provider,则直接返回mPluginManager.getIContentProvider()

public synchronized IContentProvider getIContentProvider() {    if (mIContentProvider == null) {        hookIContentProviderAsNeeded();    }    return mIContentProvider;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

咦,又看到一个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();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

前两行比较重要,第一行是拿到了占坑的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方法:

@Overridepublic 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();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

直接看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));        builder.append("/?plugin=" + plugin.getLocation());        builder.append("&pkg=" + pkg);        builder.append("&uri=" + pluginUri);        Uri wrapperUri = Uri.parse(builder.toString());        if (method.getName().equals("call")) {            bundleInCallMethod.putString(KEY_WRAPPER_URI, wrapperUri.toString());        } else {            args[index] = wrapperUri;        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

从参数中找到uri,往下看,搞了个StringBuilder首先加入占坑provider的uri,然后将目标uri,pkg,plugin等参数等拼接上去,替换到args中的uri,然后继续走原本的流程。

假设是query方法,应该就到达我们占坑provider的query方法啦。

(2)代理分发

占坑如下:

<provider    android:name="com.didi.virtualapk.delegate.RemoteContentProvider"    android:authorities="${applicationId}.VirtualAPK.Provider"    android:process=":daemon" />
  • 1
  • 2
  • 3
  • 4

打开RemoteContentProvider,直接看query方法:

@Overridepublic Cursor query(Uri uri, String[] projection, String selection,                    String[] selectionArgs, String sortOrder) {    ContentProvider provider = getContentProvider(uri);    Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI));    if (provider != null) {        return provider.query(pluginUri, projection, selection, selectionArgs, sortOrder);    }    return null;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

可以看到通过传入的生成了一个新的provider,然后拿到目标uri,在直接调用provider.query传入目标uri即可。

那么这个provider实际上是这个代理类帮我们生成的:

private ContentProvider getContentProvider(final Uri uri) {    final PluginManager pluginManager = PluginManager.getInstance(getContext());    Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI));    final String auth = pluginUri.getAuthority();    // 省略了缓存管理    LoadedPlugin plugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));    if (plugin == null) {        try {            pluginManager.loadPlugin(new File(uri.getQueryParameter(KEY_PLUGIN)));        } catch (Exception e) {            e.printStackTrace();        }    }    final ProviderInfo providerInfo = pluginManager.resolveContentProvider(auth, 0);    if (providerInfo != null) {        RunUtil.runOnUiThread(new Runnable() {            @Override            public void run() {                try {                    LoadedPlugin loadedPlugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));                    ContentProvider contentProvider = (ContentProvider) Class.forName(providerInfo.name).newInstance();                    contentProvider.attachInfo(loadedPlugin.getPluginContext(), providerInfo);                    sCachedProviders.put(auth, contentProvider);                } catch (Exception e) {                    e.printStackTrace();                }            }        }, true);        return sCachedProviders.get(auth);    }    return null;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

很简单,取出原本的uri,拿到auth,在通过加载plugin得到providerInfo,反射生成provider对象,在调用其attachInfo方法即可。

其他的几个方法:insert、update、delete、call逻辑基本相同,就不赘述了。

感觉这里其实通过hook AMS的getContentProvider方法也能完成上述流程,感觉好像可以更彻底,不需要依赖PluginContext了。

六、总结

总结下,其实就是文初的内容,可以看到VritualApk大体方案如下:

  • Activity:在宿主apk中提前占几个坑,然后通过“欺上瞒下”(这个词好像是360之前的ppt中提到)的方式,启动插件apk的Activity;因为要支持不同的launchMode以及一些特殊的属性,需要占多个坑。
  • Service:通过代理Service的方式去分发;主进程和其他进程,VirtualAPK使用了两个代理Service。
  • BroadcastReceiver:静态转动态。
  • ContentProvider:通过一个代理Provider进行分发。

整体代码看起来还是很轻松的~

当然如果你要选择某一个插件化方案进行使用,一定要了解其中的实现原理,文档上描述的并不是所有细节,很多一些属性什么的,以及由于其实现的方式造成一些特性的不支持。了解源码,可以方便自己排查问题,扩展,甚至写一套根据自己业务需求的插件化方案~~

再多嘴一句,还是建议大多多在某一方面深入了解,不要痴迷于UI特效(上班路上看看我的推文就好啦~玩笑~,很多特效的,了解下原理即可)~~其实我早期浪费了很多时间在上面,在你掌握了自定义View的详细细节、事件分发机制这些机制后,大部分UI的编写都是时间问题。

不要在上面浪费过多时间,比别人多研究几个特效并不会对自己的提升有巨大的帮助,过来人,忠言逆耳~。


支持我的话可以关注下我的公众号,每天都会推送新知识~

欢迎关注我的微信公众号:hongyangAndroid 
  (可以给我留言你想学习的文章,支持投稿) 
 

           

再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.csdn.net/jiangjunshow

这篇关于滴滴插件化方案 VirtualApk 源码解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/365516

相关文章

C语言中自动与强制转换全解析

《C语言中自动与强制转换全解析》在编写C程序时,类型转换是确保数据正确性和一致性的关键环节,无论是隐式转换还是显式转换,都各有特点和应用场景,本文将详细探讨C语言中的类型转换机制,帮助您更好地理解并在... 目录类型转换的重要性自动类型转换(隐式转换)强制类型转换(显式转换)常见错误与注意事项总结与建议类型

MySQL 缓存机制与架构解析(最新推荐)

《MySQL缓存机制与架构解析(最新推荐)》本文详细介绍了MySQL的缓存机制和整体架构,包括一级缓存(InnoDBBufferPool)和二级缓存(QueryCache),文章还探讨了SQL... 目录一、mysql缓存机制概述二、MySQL整体架构三、SQL查询执行全流程四、MySQL 8.0为何移除查

在Rust中要用Struct和Enum组织数据的原因解析

《在Rust中要用Struct和Enum组织数据的原因解析》在Rust中,Struct和Enum是组织数据的核心工具,Struct用于将相关字段封装为单一实体,便于管理和扩展,Enum用于明确定义所有... 目录为什么在Rust中要用Struct和Enum组织数据?一、使用struct组织数据:将相关字段绑

使用Java实现一个解析CURL脚本小工具

《使用Java实现一个解析CURL脚本小工具》文章介绍了如何使用Java实现一个解析CURL脚本的工具,该工具可以将CURL脚本中的Header解析为KVMap结构,获取URL路径、请求类型,解析UR... 目录使用示例实现原理具体实现CurlParserUtilCurlEntityICurlHandler

Redis 多规则限流和防重复提交方案实现小结

《Redis多规则限流和防重复提交方案实现小结》本文主要介绍了Redis多规则限流和防重复提交方案实现小结,包括使用String结构和Zset结构来记录用户IP的访问次数,具有一定的参考价值,感兴趣... 目录一:使用 String 结构记录固定时间段内某用户 IP 访问某接口的次数二:使用 Zset 进行

深入解析Spring TransactionTemplate 高级用法(示例代码)

《深入解析SpringTransactionTemplate高级用法(示例代码)》TransactionTemplate是Spring框架中一个强大的工具,它允许开发者以编程方式控制事务,通过... 目录1. TransactionTemplate 的核心概念2. 核心接口和类3. TransactionT

数据库使用之union、union all、各种join的用法区别解析

《数据库使用之union、unionall、各种join的用法区别解析》:本文主要介绍SQL中的Union和UnionAll的区别,包括去重与否以及使用时的注意事项,还详细解释了Join关键字,... 目录一、Union 和Union All1、区别:2、注意点:3、具体举例二、Join关键字的区别&php

解读Redis秒杀优化方案(阻塞队列+基于Stream流的消息队列)

《解读Redis秒杀优化方案(阻塞队列+基于Stream流的消息队列)》该文章介绍了使用Redis的阻塞队列和Stream流的消息队列来优化秒杀系统的方案,通过将秒杀流程拆分为两条流水线,使用Redi... 目录Redis秒杀优化方案(阻塞队列+Stream流的消息队列)什么是消息队列?消费者组的工作方式每

Spring IOC控制反转的实现解析

《SpringIOC控制反转的实现解析》:本文主要介绍SpringIOC控制反转的实现,IOC是Spring的核心思想之一,它通过将对象的创建、依赖注入和生命周期管理交给容器来实现解耦,使开发者... 目录1. IOC的基本概念1.1 什么是IOC1.2 IOC与DI的关系2. IOC的设计目标3. IOC

java中的HashSet与 == 和 equals的区别示例解析

《java中的HashSet与==和equals的区别示例解析》HashSet是Java中基于哈希表实现的集合类,特点包括:元素唯一、无序和可包含null,本文给大家介绍java中的HashSe... 目录什么是HashSetHashSet 的主要特点是HashSet 的常用方法hasSet存储为啥是无序的