zoukankan      html  css  js  c++  java
  • ContentProvider插件化解决方案

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

    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机制,而且是简单的字符串,所以很适合这种转发机制

    欢迎关注我的微信公众号:安卓圈

  • 相关阅读:
    Object.Instantiate 实例
    .idata数据的解析
    数据结构-静态链表
    数据结构-循环链表
    Android---两个视图间的淡入淡出
    HDU 4597 Play Game 2013 ACM-ICPC吉林通化全国邀请赛H题
    Android 编译时出现r cannot be resolved to a variable
    找工作笔试面试那些事儿(5)---构造函数、析构函数和赋值函数
    SFINAE 模板替换失败而非报错的应用
    模板实参推导 & xx_cast的实现
  • 原文地址:https://www.cnblogs.com/anni-qianqian/p/10109356.html
Copyright © 2011-2022 走看看