android contentprovider 插件化之二

------本文转载自 Android插件化原理解析——contentprovider的插件化      这一系列的文章实在是写的好! 

4. 插件化

4.1 思路分析

在分析ContentProvider的工作原理的过程中我们提出了一种插件化方案:在进程启动之初,

手动把ContentProvider安装到本进程,使得后续对于插件ContentProvider的请求能够顺利完成。

我们也指出它的一个严重缺陷,那就是它只能在插件系统内部掩耳盗铃,在插件系统之外,

第三方App依然无法感知到插件中的ContentProvider的存在。

如果插件的ContentProvider组件仅仅是为了共享给其他插件或者宿主程序使用,那么这种方案可以解决问题;

不需要Hook AMS,非常简单。

但是,如果希望把插件ContenProvider共享给整个系统呢?在分析AMS中获取ContentProvider的过程中我们了解到,

ContentProvider信息的注册是在Android系统启动或者新安装App的时候完成的,而AMS把ContentProvider返回 

给第三方App也是在system_server进程完成;我们无法对其暗箱操作。

在完成Activity,Service组件的插件化之后,这种限制对我们来说已经是小case了:我们在宿主程序里面注册一个货真价实、

被系统认可的StubContentProvider组件,把这个组件共享给第三方App;

然后通过代理分发技术把第三方App对于插件ContentProvider的请求通过这个StubContentProvider分发给对应的插件。

 

但是这还存在一个问题,由于第三方App查阅的其实是StubContentProvider,因此他们查阅的URI也必然是 

StubContentProvider的authority,要查询到插件的ContentProvider,必须把要查询的真正的插件ContentProvider信息传递进来。

这个问题的解决方案也很容易,我们可以制定一个「插件查询协议」来实现。

举个例子,假设插件系统的宿主程序在AndroidManifest.xml中注册了一个StubContentProvider,

它的Authority为com.test.host_authority;由于这个组件被注册在AndroidManifest.xml中,是系统认可的ContentProvider组件,

整个系统都是可以使用这个共享组件的,使用它的URI一般为content://com.test.host_authority;

那么,如果插件系统中存在一个插件,这个插件提供了一个PluginContentProvider,它的Authority为com.test.plugin_authorith,

 因为这个插件的PluginContentProvider没有在宿主程序的AndroidMainifest.xml中注册(预先注册就失去插件的意义了),

整个系统是无法感知到它的存在的;前面提到代理分发技术,也就是,我们让第三方App请求宿主程序的StubContentProvider,

这个 StubContentProvider把请求转发给合适的插件的ContentProvider就能完成了(插件内部通过预先 installProvider可以查询所有的ContentProvider组件);

这个协议可以有很多,比如说:如果第三方App需要请求插件的 StubContentProvider,

可以以content://com.test.host_authority/com.test.plugin_authorith去查询系统;也就是说,

我们假装请求StubContentProvider,把真正的需要请求的PluginContentProvider的 Authority放在路径参数里面,

StubContentProvider收到这个请求之后,拿到这个真正的Authority去请求插件的 PluginContentProvider,

拿到结果之后再返回给第三方App。

这样,我们通过「代理分发技术」以及「插件查询协议」可以完美解决「共享」的问题,

开篇提到了我们之前对于Activity,Service组件插件化方案中对于「共享」功能的缺失,按照这个思路,基本可以解决这一系列问题。

比如,对于第三方App无法绑定插件服务的问题,我们可以注册一个StubService,把真正需要bind的插件服务信息放在intent的某个字段中,

然后在StubService的onBind中解析出这个插件 服务信息,然后去拿到插件Service组件的Binder对象返回给第三方。实现步骤如下.

4.2预先installProvider

要实现预先installProvider,我们首先需要知道,所谓的「预先」到底是在什么时候?

前文我们提到过App进程安装ContentProvider的时机非常之早,在Application类的onCreate回调执行之前已经完成了;这意味着什么?

现在我们对于ContentProvider插件化的实现方式是通过「代理分发技术」,也就是说在请求插件ContentProvider的时候会 

先请求宿主程序的StubContentProvider;如果一个第三方App查询插件的ContentProvider,而宿主程序没有启动的 话,

AMS会启动宿主程序并等待宿主程序的StubContentProvider完成安装,一旦安装完成就会把得到的IContentProvider

返回给这个第三方App;第三方App拿到IContentProvider这个Binder对象之后就可能发起CURD操作,

如果这个时候插件ContentProvider还没有启动,那么肯定就会出异常;要记住,“这个时候”可能宿主程序的onCreate还没有执行完毕呢!!

所以,我们基本可以得出结论,预先安装这个所谓的「预先」必须早于Application的onCreate方法,

在Android SDK给我们的回调里面,attachBaseContent这个方法是可以满足要求的,它在Application这个对象被创建之后就会立即调用。

解决了时机问题,那么我们接下来就可以安装ContentProvider了。

安装ContentProvider也就是要调用ActivityThread类的installProvider方法,这个方法需要的参数有点多,

而且它的第二个参数IActivityManager.ContentProviderHolder是一个隐藏类,我们不知道如何构造,

就算通过反射构造由于SDK没有暴露稳定性不易保证,我们看看有什么方法调用了这个installProvider。

installContentProviders这个方法直接调用installProvder看起来可以使用,但是它是一个private的方法,还有public的方法吗?继续往上寻找调用链,发现了installSystemProviders这个方法:

public final void installSystemProviders(List<ProviderInfo> providers) {
    if (providers != null) {
        installContentProviders(mInitialApplication, providers);
    }
}

但是,我们说过ContentProvider的安装必须相当早,必须在Application类的attachBaseContent方法内,

而这个mInitialApplication字段是在onCreate方法调用之后初始化的,所以,如果直接使用这个installSystemProviders势必抛出空指针异常;

因此,我们只有退而求其次,选择通过installContentProviders这个方法完成ContentProvider的安装.

要调用这个方法必须拿到ContentProvider对应的ProviderInfo,这个我们在之前也介绍过,可以通过PackageParser类完成,

当然这个类有一些兼容性问题,我们需要手动处理:

/**
 * 解析Apk文件中的 <provider>, 并存储起来
 * 主要是调用PackageParser类的generateProviderInfo方法
 *
 * @param apkFile 插件对应的apk文件
 * @throws Exception 解析出错或者反射调用出错, 均会抛出异常
 */
public static List<ProviderInfo> parseProviders(File apkFile) throws Exception {
    Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
    Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);

    Object packageParser = packageParserClass.newInstance();

    // 首先调用parsePackage获取到apk对象对应的Package对象
    Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, PackageManager.GET_PROVIDERS);

    // 读取Package对象里面的services字段
    // 接下来要做的就是根据这个List<Provider> 获取到Provider对应的ProviderInfo
    Field providersField = packageObj.getClass().getDeclaredField("providers");
    List providers = (List) providersField.get(packageObj);

    // 调用generateProviderInfo 方法, 把PackageParser.Provider转换成ProviderInfo
    Class<?> packageParser$ProviderClass = Class.forName("android.content.pm.PackageParser$Provider");
    Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
    Class<?> userHandler = Class.forName("android.os.UserHandle");
    Method getCallingUserIdMethod = userHandler.getDeclaredMethod("getCallingUserId");
    int userId = (Integer) getCallingUserIdMethod.invoke(null);
    Object defaultUserState = packageUserStateClass.newInstance();

    // 需要调用 android.content.pm.PackageParser#generateProviderInfo
    Method generateProviderInfo = packageParserClass.getDeclaredMethod("generateProviderInfo",
            packageParser$ProviderClass, int.class, packageUserStateClass, int.class);

    List<ProviderInfo> ret = new ArrayList<>();
    // 解析出intent对应的Provider组件
    for (Object service : providers) {
        ProviderInfo info = (ProviderInfo) generateProviderInfo.invoke(packageParser, service, 0, defaultUserState, userId);
        ret.add(info);
    }
    return ret;
}

解析出ProviderInfo之后,就可以直接调用installContentProvider了:

/**
 * 在进程内部安装provider, 也就是调用 ActivityThread.installContentProviders方法
 *
 * @param context you know
 * @param apkFile
 * @throws Exception
 */
public static void installProviders(Context context, File apkFile) throws Exception {
    List<ProviderInfo> providerInfos = parseProviders(apkFile);

    for (ProviderInfo providerInfo : providerInfos) {
        providerInfo.applicationInfo.packageName = context.getPackageName();
    }

    Log.d("test", providerInfos.toString());
    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);
    Method installProvidersMethod = activityThreadClass.getDeclaredMethod("installContentProviders", Context.class, List.class);
    installProvidersMethod.setAccessible(true);
    installProvidersMethod.invoke(currentActivityThread, context, providerInfos);
}

整个安装过程必须在Application类的attachBaseContent里面完成:

/**
 * 一定需要Application,并且在attachBaseContext里面Hook
 * 因为provider的初始化非常早,比Application的onCreate还要早
 * 在别的地方hook都晚了。
 */
public class UPFApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        try {
            File apkFile = getFileStreamPath("testcontentprovider-debug.apk");
            if (!apkFile.exists()) {
                Utils.extractAssets(base, "testcontentprovider-debug.apk");
            }

            File odexFile = getFileStreamPath("test.odex");

            // Hook ClassLoader, 让插件中的类能够被成功加载
            BaseDexClassLoaderHookHelper.patchClassLoader(getClassLoader(), apkFile, odexFile);
            ProviderHelper.installProviders(base, getFileStreamPath("testcontentprovider-debug.apk"));
        } catch (Exception e) {
            throw new RuntimeException("hook failed", e);
        }
    }
}

4.3代理分发以及协议解析

把插件中的ContentProvider安装到插件系统中之后,在插件内部就可以自由使用这些ContentProvider了;

要把这些插件共享给整个系统,我们还需要一个货真价实的ContentProvider组件来执行分发:

<provider
    android:name="com.example.weishu.contentprovider_management.StubContentProvider"
    android:authorities="com.example.weishu.contentprovider_management.StubContentProvider"
    android:process=":p"
    android:exported="true" />

第三方App如果要查询到插件的ContentProvider,必须遵循一个「插件查询协议」,

这样StubContentProvider才能把对于插件的请求分发到正确的插件组件:

/**
 * 为了使得插件的ContentProvder提供给外部使用,我们需要一个StubProvider做中转;
 * 如果外部程序需要使用插件系统中插件的ContentProvider,不能直接查询原来的那个uri
 * 我们对uri做一些手脚,使得插件系统能识别这个uri;
 * 这里的处理方式如下:
 * 原始查询插件的URI应该为:content://plugin_auth/path/query
 * 如果需要查询插件,需要修改为: content://stub_auth/plugin_auth/path/query
 * 也就是,我们把插件ContentProvider的信息放在URI的path中保存起来;
 * 然后在StubProvider中做分发。
 * 当然,也可以使用QueryParamerter,比如:
 * content://plugin_auth/path/query/ ->  content://stub_auth/path/query?plugin=plugin_auth
 * @param raw 外部查询我们使用的URI
 * @return 插件真正的URI
 */
private Uri getRealUri(Uri raw) {
    String rawAuth = raw.getAuthority();
    if (!AUTHORITY.equals(rawAuth)) {
        Log.w(TAG, "rawAuth:" + rawAuth);
    }

    String uriString = raw.toString();
    uriString = uriString.replaceAll(rawAuth + '/', "");
    Uri newUri = Uri.parse(uriString);
    Log.i(TAG, "realUri:" + newUri);
    return newUri;
}

通过以上过程我们就实现了ContentProvider的插件化。需要说明的是,DroidPlugind的插件化与上述介绍的方案有一些不同之处:

 1.首先DroidPlugin并没有选择预先安装的方案,而是选择HookActivityManagerNative,

拦截它的getContentProvider以及publishContentProvider方法实现 对于插件组件的控制;

从这里可以看出它对ContentProvider与Service的插件化几乎是相同的,Hook才是DroidPlugin Style ^_^.

 2.然后,关于携带插件信息,或者说「插件查询协议」方面;DroidPlugin把插件信息放在查询参数里面,本文呢则是路径参数;这一点完全看个人喜好。

5. 小结

本文我们通过「代理分发技术」以及「插件查询协议」完成了ContentProvider组件的插件化,

并且给出了对「插件共享组件」的问题的一般解决方案。值得一提的是,系统的ContentProvider其实是lazy load的,

也就是说只有在需要使用的时候才会启动对应的ContentProvider,而我们对于插件的实现则是预先加载,

这里还有改进的空间,读者可以思考一下解决方案。

由于ContentProvider的使用频度非常低,而很多它使用的场景(比如系统)并不太需要「插件化」,

因此在实际的插件方案中,提供 ContentProvider插件化的方案非常之少;就算需要实现ContentProvider的插件化,

也只是解决插件内部之间共享组件的问题,并没有把插件组件暴露给整个系统。我个人觉得,如果只是希望插件化,

那么是否支持ContentProvider无伤大雅,但是,如果希望实现虚拟化或者说容器技术,所有组件是必须支持插件化的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值