专栏首页安卓圈ContentProvider插件化解决方案

ContentProvider插件化解决方案

1.当要传输的数据量大小不超过1M的时候,使用Binder;数据量超过1M时,Binder就搞不定了,需要ContentProvider

2.ContentProvider就是一个数据库引擎,向外界提供了CRUD的API

ContentProvider插件化

将静态Provider手动安装到宿主app中,把它们放在宿主的ContentProvider列表中,就可以使用了

/**
 * 由于应用程序使用的ClassLoader为PathClassLoader
 * 最终继承自 BaseDexClassLoader
 * 查看源码得知,这个BaseDexClassLoader加载代码根据一个叫做
 * dexElements的数组进行, 因此我们把包含代码的dex文件插入这个数组
 * 系统的classLoader就能帮助我们找到这个类
 *
 * 这个类用来进行对于BaseDexClassLoader的Hook
 * 类名太长, 不要吐槽.
 * @author weishu
 * @date 16/3/28
 */
//***第一步:宿主app和插件app的dex合并到一起
public final class BaseDexClassLoaderHookHelper {

    public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
            throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        // 获取 BaseDexClassLoader : pathList
        Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList");

        // 获取 PathList: Element[] dexElements
        Object[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj, "dexElements");

        // Element 类型
        Class<?> elementClass = dexElements.getClass().getComponentType();

        // 创建一个数组, 用来替换原始的数组
        Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);

        // 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数
        Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};
        Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
        Object o = RefInvoke.createObject(elementClass, p1, v1);

        Object[] toAddElementArray = new Object[] { o };
        // 把原始的elements复制进去
        System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
        // 插件的那个element复制进去
        System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);

        // 替换
        RefInvoke.setFieldObject(pathListObj, "dexElements", newElements);
    }
}
public class ProviderHelper {

    /**
     * 解析Apk文件中的 <provider>, 并存储起来
     * 主要是调用PackageParser类的generateProviderInfo方法
     *
     * @param apkFile 插件对应的apk文件
     * @throws Exception 解析出错或者反射调用出错, 均会抛出异常
     */
    public static List<ProviderInfo> parseProviders(File apkFile) throws Exception {

        //获取PackageParser对象实例
        Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
        Object packageParser = packageParserClass.newInstance();

        // 首先调用parsePackage获取到apk对象对应的Package对象
        Class[] p1 = {File.class, int.class};
        Object[] v1 = {apkFile, PackageManager.GET_PROVIDERS};
        Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage",p1, v1);

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

        // 调用generateProviderInfo 方法, 把PackageParser.Provider转换成ProviderInfo

        //准备generateProviderInfo方法所需要的参数
        Class<?> packageParser$ProviderClass = Class.forName("android.content.pm.PackageParser$Provider");
        Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
        Object defaultUserState = packageUserStateClass.newInstance();
        int userId = (Integer) RefInvoke.invokeStaticMethod("android.os.UserHandle", "getCallingUserId");
        Class[] p2 = {packageParser$ProviderClass, int.class, packageUserStateClass, int.class};

        List<ProviderInfo> ret = new ArrayList<>();
        // 解析出intent对应的Provider组件
        for (Object provider : providers) {
            Object[] v2 = {provider, 0, defaultUserState, userId};
            //***第二步:把得到的Package对象转换为我们需要的ProviderInfo类型对象***
            ProviderInfo info = (ProviderInfo) RefInvoke.invokeInstanceMethod(packageParser, "generateProviderInfo",p2, v2);
            ret.add(info);
        }

        return ret;
    }

    /**
     * 在进程内部安装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);
        //***第三步:把插件ContentProvider的packageName设置为当前apk的packageName
        for (ProviderInfo providerInfo : providerInfos) {
            providerInfo.applicationInfo.packageName = context.getPackageName();
        }
        //***第四步:把这些插件ContentProvider安装到宿主App中
        Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");

        Class[] p1 = {Context.class, List.class};
        Object[] v1 = {context, providerInfos};

        RefInvoke.invokeInstanceMethod(currentActivityThread, "installContentProviders", p1, v1);
    }
}

Hook的时机很重要,越早越好,不然外部app调用插件的ContentProvider就要等很久了

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

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

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

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

            // Hook ClassLoader, 让插件中的类能够被成功加载
            BaseDexClassLoaderHookHelper.patchClassLoader(getClassLoader(), apkFile, odexFile);

            //安装插件中的Providers
            ProviderHelper.installProviders(base, getFileStreamPath("plugin2.apk"));
        } catch (Exception e) {
            throw new RuntimeException("hook failed", e);
        }
    }
}

ContentProvider转发机制

在当前app中定义一个StubContentProvider作为中转,让外界app调用当前app的StubContentProvider,再调用插件里的ContentProvider

/**
 * 为了使得插件的ContentProvder提供给外部使用,我们需要一个StubProvider做中转;
 * 如果外部程序需要使用插件系统中插件的ContentProvider,不能直接查询原来的那个uri
 * 我们对uri做一些手脚,使得插件系统能识别这个uri;
 *
 * 这里的处理方式如下:
 *
 * 原始查询插件的URI应该为:
 * content://host_auth/plugin_auth/path/query
 * 例子 content://baobao222/jianqiang
 *
 * 如果需要查询插件,替换为:
 *
 * content://plugin_auth/path/query
 * 例子 content://jianqiang
 *
 * 也就是,我们把插件ContentProvider的信息放在URI的path中保存起来;
 * 然后在StubProvider中做分发。
 *
 * @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独有的URI机制,而且是简单的字符串,所以很适合这种转发机制

--摘自《android插件化开发指南》

本文分享自微信公众号 - 安卓圈(gh_df75572d44e4),作者:King磊

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Activity插件化解决方案

    2.最简单的插件化方案就是在宿主的androidmanifest.xml中申明插件中的四大组件

    用户3112896
  • 解决插件化资源id冲突

    第一步:aapt。为res目录下的资源生成R.java文件,同时为AndroidManifest.xml生成Manifest.java文件

    用户3112896
  • 对反射的封装

    用户3112896
  • HBase表的设计及与生态系统中其他框架的集成

    (3)创建t1表,列簇为f1,最大版本数为1,表的生存时间为2592000,使用blockcache缓存hbase表中读写的数据:

    魏晓蕾
  • 我是怎么把研发安全做“没”了的

    我叫王大锤,万万没想到,我成了马栏山不省心集团的研发安全工程师……这一定是对我的终极考验,相信用不了多久我就会升职加薪,当上总经理,出任CEO,迎娶白富美,走向...

    FB客服
  • 使用无觅相关文章插件一定要删除的代码

    无觅相关文章插件许多博主都在使用,这个插件的确是不错,图文模式的排版美观(虽然文章相关性一直不够),无论是读者还是博主,浏览体验都很好。对无觅本身来说,这一个小...

    Jeff
  • Mybatis插件机制详解

    Mybatis采用责任链模式,通过动态代理组织多个插件(拦截器),通过这些插件可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Myba...

    黄泽杰
  • 腾讯报告谷歌TensorFlow首个安全风险,谷歌确认并致谢

    作者:胡祥杰 【新智元导读】 TensorFlow爆出发布以来首个自身安全风险,据悉,腾讯安全平台部预研团队已向谷歌报告这一风险并获得致谢。 谷歌面向机器学习和...

    新智元
  • TensorFlow 内核剖析

    这是我找的一个Tensorflow的书,作者是刘光聪。书写的非常不错,我也借此机会学习一波。书中的TensorFlow使用的是1.2版本,目前来说算是很新的。 ...

    故事尾音
  • 用Qt写软件系列五:一个安全防护软件的制作(3)

    引言        上一篇中讲述了工具箱的添加。通过一个水平布局管理器,我们将一系列的工具按钮组合到了一起,完成了工具箱的编写。本文在前面的基础上实现窗体分割效...

    24K纯开源

扫码关注云+社区

领取腾讯云代金券