VirtualAPK插件化方案原理探索

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

一、前言

最近刚好在看任玉刚的《安卓开发艺术探索》,学到了不少东西。前面刚写完四大组件的原理分析,算是对Android组件进一步了解。趁热打铁,了解一下滴滴出品的VirtualAPK插件化方案,同样是刚哥参与的,站在巨人的肩膀来看看。
插件化、热更新在国内近段时间相当火热,我们也得跟上啊。学习插件化不仅仅只有”插件化”这么简单而已,涉及到相当多的知识,对我们提升还是有一定帮助的。之前也看过鸿洋的文章,写的很简练、直击要害,过不对刚了解插件化的同学们不太友好,我这里就稍微详细谈谈。不过同样,阅读本文之前需要先了解四大组件启动原理。网上资源很多,我这里不要脸的推荐一下我的文章:
【Android源码系列】Activity启动源码解析
【Android源码系列】Service启动源码解析
【Android源码系列】BroadcastReceiver启动源码解析
【Android源码系列】ContentProvider启动源码解析
同时插件化还会涉及到Binder、classLoader、Hook、反射、动态代理等知识,先了解一波会有助于阅读本文,建议大家学习一波。废话不多说,进入正题。

二、VirtualAPK使用

具体使用方案可以参见VirtualAPK的wiki,里面也有推荐第三方文章,写的都很详细。我这里就简单的说一下,不详细的讲怎样引入之类的了。我们都知道插件化分为宿主工程和插件工程,首先需要重写宿主工程Application里的attachBaseContext方法:

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    PluginManager.getInstance(base).init();
}

然后再加载插件:

    private void loadPlugin(Context base) {
        PluginManager pluginManager = PluginManager.getInstance(base);
        //这里加载插件APK,可以看出,需要把APK放到手机根文件夹下(可自定义).
        File apk = new File(Environment.getExternalStorageDirectory(), "Test.apk");
        if (apk.exists()) {
            try {
                pluginManager.loadPlugin(apk);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

调用了pluginManager.loadPlugin(apk),PluginManager这个类是VirtualAPK的核心类,我们跟进方法看看

    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());
        }
        //这里解析APK,并保存到LoadedPlugin对象中
        LoadedPlugin plugin = LoadedPlugin.create(this, this.mContext, apk);
        if (null != plugin) {
            //放到mPlugins这个map里保存
            this.mPlugins.put(plugin.getPackageName(), plugin);
            // 启动pluginAPK
            plugin.invokeApplication();
        } else {
            throw  new RuntimeException("Can't load plugin which is invalid: " + apk.getAbsolutePath());
        }
    }

这个方法将插件APK进行解析、启动、保存,具体如何解析的这里就不详细分析,有兴趣的童鞋可以看看LoadedPlugin 这个类的实现。
这样初始工作就做完了(当然还需要gradle配置),可以直接在插件工程里码代码了~没什么不一样,和平时一样码就行,还是很方便的。

三、Activity支持原理

我们之前看到PluginManager.getInstance(base)的初始化,现在我们来看看如何实现。
getInstance是我们熟悉的单例,接着发现在构造方法里调用了prepare方法

private void prepare() {
        Systems.sHostContext = getHostContext();
        this.hookInstrumentationAndHandler();
        if (Build.VERSION.SDK_INT >= 26) {
            this.hookAMSForO();
        } else {
            this.hookSystemServices();
        }
    }

先来看看this.hookInstrumentationAndHandler();

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、先是利用反射获取到context里的Instrumentation对象
2、然后把他保存到自定义的VAInstrumentation里
3、利用反射获取到ActivityThread的对象,将原来的Instrumentation对象用新的VAInstrumentation替换掉。

这样就VAInstrumentation就可以工作了,我们都知道Activity启动时,需要调用Instrumentation的execStartActivity方法(以下都默认读者已经了解过四大组件启动原理),所以…嘿嘿,我们看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;

    }

1、调用transformIntentToExplicitAsNeeded方法,注释:transform intent from implicit to explicit(将隐式启动的Activity转为显示启动),代码就不贴了,可以自己看(其实我一直推荐阅读类似源码文章时,一定要把源码打开跟着走,效果最佳)
2、接着,依然调用ComponentsHandler的方法:markIntentIfNeeded,这个方法将intent的偷换成我们的,达到欺骗系统的作用(下文分析)。
3、最后调用realExecStartActivity返回结果,其实点进去发现,就是调用了真正的Instrumentation的方法。

接下来详细解析一下步骤2。

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);
        }
    }

这段代码很简单,将包名、类名、插件标记保存在intent里,然后调用dispatchStubActivity

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);
    }

我们都知道,Activity必须在Manifest里注册,所以这里将Intent里ComponentName替换为提前在Manifest“占坑”的Activity,这样就骗过了系统检测,而检测是在AMS(ActivityManagerService)端完成的,最后一句intent.setClassName(mContext, stubActivity);就是这个作用。
我们再来看看,stubActivity这个名字是如何替换的:


    public static final String corePackage = "com.didi.virtualapk.core";
    public static final String STUB_ACTIVITY_STANDARD = "%s.A$%d";
    public static final String STUB_ACTIVITY_SINGLETOP = "%s.B$%d";
    public static final String STUB_ACTIVITY_SINGLETASK = "%s.C$%d";
    public static final String STUB_ACTIVITY_SINGLEINSTANCE = "%s.D$%d";

    public final int usedStandardStubActivity = 1;
    public int usedSingleTopStubActivity = 0;
    public int usedSingleTaskStubActivity = 0;
    public int usedSingleInstanceStubActivity = 0;

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;
    }

很明显,根据Activity的启动方式分别取名,这里我们看看标准的standard模式。

                stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
                if (windowIsTranslucent) {
                    stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
                }

是以”%s.A$%d”的格式取名,可以预见我们的Activity名字大概是:
“com.didi.virtualapk.core.A\$1”,有转场动画的为2。好的我们去看看Manifest里的占坑Activity

 <!-- Stub Activities -->
        <activity android:name=".A$1" android:launchMode="standard"/>
        <activity android:name=".A$2" android:launchMode="standard"
            android:theme="@android:style/Theme.Translucent" />

没错了,和我们预想的一样。其他启动模式也差不多,读者可自行查看。

这样就完成了”欺骗”启动的效果,但是这样启动的不就是占坑Activity而已吗?所以还需要在AMS工作后,将启动Activity变回目标Activity。
我们知道真正创建Activity实例是在Instrumentation里的newActivity,Virtual就选择从这里下手。

    @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);
    }

上诉代码比较简单,先用我们保存好的className替代掉占坑的Name,然后用真正的Instrumentation调用newActivity,最后再讲Intent替换掉,一顿操作行云流水….咳咳,同时还重写了callActivityOnCreate方法

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);
    }

这个方法主要是利用反射替换掉了resource、context等对象,resource就不说了,context也就是我们的上下文,保存了很多Activity信息,替换掉也就意味着我们的Activity很多配置被支持了。具体实现类是PluginContext,这个类用了Context一族的装饰模式来写,非常巧妙的完成了他的工作,有兴趣的童鞋可以自行查看PluginContext源码。
到这Activity算是插入了,接下来是Service。

四、Service的插入原理

刚刚我们分析到PluginManager的prepare方法

    private void prepare() {
        Systems.sHostContext = getHostContext();
        this.hookInstrumentationAndHandler();
        if (Build.VERSION.SDK_INT >= 26) {
            this.hookAMSForO();
        } else {
            this.hookSystemServices();
        }
    }

前面是Activity相关,后面就是Service了,只不过有两个方法,因为Android O改动了AMS/AMN的实现(我粗略看了看AMN整个类好像被干掉了),所以Virtual做了适配,不过原理上都是一样的,所以我们只看hookSystemServices方法

    /**
     * hookSystemServices, but need to compatible with Android O in future.
     */
    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、利用反射获取了ActivityManagerNative的静态成员gDefault。了解源码的童鞋应该知道,这个gDefault其实就是AMS的代理对象,具体实现如下
这里写图片描述
2、自定义了ActivityManagerProxy类,用动态代理获取一个IActivityManager的代理对象,newInstance实现如下

    public static IActivityManager newInstance(PluginManager pluginManager, IActivityManager activityManager) {
        return (IActivityManager) Proxy.newProxyInstance(activityManager.getClass().getClassLoader(), new Class[] { IActivityManager.class }, new ActivityManagerProxy(pluginManager, activityManager));
    }

3、用生成的代理对象替换掉原来的,并保存起来。

其实,看到这里,Service的插入已经很明朗了。
我们都知道startService的逻辑会走到AMS的startService里,就像这样

private ComponentName startServiceCommon(Intent service, UserHandle user) {
        try {
            validateServiceIntent(service);
            service.prepareToLeaveProcess();
            ComponentName cn = ActivityManagerNative.getDefault().startService(
                mMainThread.getApplicationThread(), service,
                service.resolveTypeIfNeeded(getContentResolver()), user.getIdentifier());
            //省略部分代码
            return cn;
        } catch (RemoteException e) {
            return null;
        }
    }

我们把ActivityManagerNative.getDefault()代理掉了,让我们自己的方法实现startService,让我们来看看代理类ActivityManagerProxy是怎么处理的。

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);
            }
        }
        //省略巨量代码.........
 }

拦截了startService、bindService等等方法,我们就不一一分析了,这里拿startService来当案例

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);
    }

先取出Service信息,判断是不是宿主工程的Service,如果是就按原来的逻辑,如果是插件的就走startDelegateServiceForTarget

private ComponentName startDelegateServiceForTarget(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
        Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
        return mPluginManager.getHostContext().startService(wrapperIntent);
    }

很明显,在wrapperTargetIntent里处理了intent,然后启动了Service

    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当做参数,新建了一个intent,目标Service是LocalService或RemoteService,RemoteService继承了LocalService,他主要负责在多进程的时候新加一个LoadedPlugin保存起来。主要工作还是在LocalService里实现

public int onStartCommand(Intent intent, int flags, int startId) {
        if (null == intent || !intent.hasExtra(EXTRA_TARGET) || !intent.hasExtra(EXTRA_COMMAND)) {
            return START_STICKY;
        }

        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);
        // ClassNotFoundException when unmarshalling in Android 5.1
        target.setExtrasClassLoader(plugin.getClassLoader());
        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;
            }
           //省略bind等方法....
        }

        return START_STICKY;
    }

通过intent、PluginManager获取到Service信息,再通过command判断执行的方法(这里所有方法都在startCommand执行了),这里我们只看start方法。
其实就是模拟了service的启动:反射获取service实例,然后调用他的attach、onCreate、onStartCommand等方法。如果已经启动过,那直接调用onStartCommand方法。

五、BroadcastReceiver

这个实现就方便了,直接全部动态注册一下就好了。代码位于LoadedPlugin的构造方法内

// 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()]);

通过classLoader获取到Receiver对象,注册即可。

六、ContentProvider

终于到了最后了,相信(假装相信)大家还记得前面提到过PluginContext这个类,他”代理”了Context,而我们使用ContentProvider会是这样

getContentResolver().query(bookUri, new String[]{"_id", "name"}, null, null, null);

熟悉ContentProvider源码后,我们知道getContentResolver会获取一个ContentResolver对象,操作都在它里面执行,而这个对象保存在ContextImpl里面,而Virtual巧妙的用PluginContext”装饰了”ContextImpl,使得获取到的ContentResolver变成我们自定义的ContentResolver:PluginContentResolver。这里可能有点绕,不清楚的童鞋建议了解Context一族的实现方式,PluginContext的实现和他们一样一样的!~为了便于理解,画了两张图
Context原来的逻辑
Context
Hook后的逻辑
这里写图片描述

好了,我接着看PluginContext的getContentResolver

    public ContentResolver getContentResolver() {
        return new PluginContentResolver(getHostContext());
    }

上文也提到了,自定义了PluginContentResolver来实现。我们知道,增删改查会调用acquireProvider类方法来获取IContentProvider的binder对象实现IPC通信。

    protected IContentProvider acquireProvider(Context context, String auth) {
        try {
            if (mPluginManager.resolveContentProvider(auth, 0) != null) {
                return mPluginManager.getIContentProvider();
            }

            return (IContentProvider) sAcquireProvider.invoke(mBase, context, auth);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

接着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,然后通过call方法,是Provider启动起来(调用任意方法都可以),”wakeup”也算是一个提示了,其实Provider根本没有这个方法。
之后获取到ActivityThread,通过他获取mProviderMap,然后就能获取到占坑的Provider啦。然后动态代理这个Provider,返回回去,这样我们拿到的provider就被代理了。主要是这三句代码实现的

IContentProvider rawProvider = (IContentProvider) mProvider.get(val);
                    IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider);
                    mIContentProvider = proxy;

我们接着看如何实现代理

    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();
        }
    }

这里provider的所有操作都会先执行一遍wrapperUri方法,然后按原逻辑走。那这个wrapperUri是干嘛的?

 private void wrapperUri(Method method, Object[] args) {
        Uri uri = null;
        int index = 0;
        if (args != null) {
            //获取Uri
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Uri) {
                    uri = (Uri) args[i];
                    index = i;
                    break;
                }
            }
        }

        Bundle bundleInCallMethod = null;
        if (method.getName().equals("call")) {
            bundleInCallMethod = getBundleParameter(args);
            if (bundleInCallMethod != null) {
                String uriString = bundleInCallMethod.getString(KEY_WRAPPER_URI);
                if (uriString != null) {
                    uri = Uri.parse(uriString);
                }
            }
        }

        if (uri == null) {
            return;
        }

        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;
            }
        }
    }

其实就是把uri处理一下,将APK位置、包名、真正的uri信息都放到占坑Uri的后面当做参数,后面应该会通过这个进行判断吧,其实到这里Hook已经结束了。嗯,为了验证我们的猜测,去看看占坑Provider怎么实现的吧

首先我们在Manifest里找到了他

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

看看query方法吧,其他的都类似

    public 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;
    }

主要是getContentProvider方法

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();
        ContentProvider cachedProvider = sCachedProviders.get(auth);
        if (cachedProvider != null) {
            return cachedProvider;
        }

        synchronized (sCachedProviders) {
            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、通过key拿到我们的pluginUri,auth
2、用auth当做key,做了一个缓存Map
3、通过反射获取provider对象并启动,放入缓存,所以占坑的provider不仅仅只是占坑作用,还成为了所有插件Provider的”容器”。

七、总结

VirtualAPK不仅仅是这里的内容,他还有很多细节需要去摸索,笔者也有些地方持有疑惑,想要更了解VirtualAPK,或者说更了解插件化,就需要很清楚Android源码的实现方式,这是一个积累的过程。
可以这么说,你如果很擅长插件化,那你对Android的理解一定很深刻,所以还不快行动?

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值