Android 插件化之——给插件中的ContentProvider发送请求

ContentProvider的插件化,可以借鉴静态广播的思路,在宿主中创建一个代理ContentProvider,第三方的app向插件中的ContentProvider发送增删改查数据的请求,都先找宿主的代理的ContentProvider,代理的ContentProvider在将实际的增删改查的请求转发给插件中的ContentProvider。这就是ContentProvider插件化的基本思路。其中,会有下面几个问题要解决:

  1. 插件中可能有多个ContentProvider,如何通过宿主中的一个代理ContentProvider就能将对应插件中的多个ContentProvider。

  2. 宿主中的代理ContentProvider获取到了外界给插件中的ContentProvider发送的请求,代理ContentProvider如何找到这些插件,毕竟,宿主在安装时,PMS会通过PackageParser来解析安装包中manifest文件信息,并将四大组件的信息都保存起来,当然也包括了ContentProvider的信息,当宿主app启动时,AMS会通过PMS中保存ContentProvider的信息,将宿主app中的ContentProvider都"安装"(其实就是将这些ContentProvider都实例化,并调用他们的onCreate方法,然后将这些实例化的ContentProvider缓存起来)一遍。由于插件是没有安装过的,即使宿主中的代理ContentProvider收到了外界的请求,转发给插件ContentProvider,但是,由于AMS不知道插件中的ContentProvider的存在,所以,没法转发给插件ContentProvider。所以,要解决如何让AMS知道插件中的ContentProvider的存在。

下面先解决第一个问题,为了方便讲解,下面以一个例子来辅助。

Uri uri = Uri.parse("content://test.cn.example.com.androidskill.stub/com.android.skill.plugin_cp_1");
Cursor queryCurosr = getContentResolver().query(uri, null, null, null, null);

上面这两行代码是用来向下面这样一个ContentProvider发送查询数据的请求的

<provider
    android:authorities="test.cn.example.com.androidskill.stub"
    android:name="test.cn.example.com.androidskill.hook.provider.StubContentProvider"
    android:exported="true"/>

上面的代码中的uri中的信息中的content://后面的第一部分就是要查找的ContentProvider的authorities,我们可以通过在代理的contentProvider的authorities后面添加插件的authorities,组成一个上面的路径
test.cn.example.com.androidskill.stub/com.android.skill.plugin_cp_1,这个路劲中"/"后的就是插件的authorities,前面的本分就是代理的authorities,这样,当代理的ContentProvider的query方法中获取到Uri时,就可以将这个Uri中的content://test.cn.example.com.androidskill.stub/com.android.skill.plugin_cp_1字符
转换成content://com.android.skill.plugin_cp_1,这样,这个转换后的字符串就是插件的authorities,这样就解决了一个代理ContentProvider可以对应多个插件ContentProvider的问题。
总结:

"content://代理authorities/插件authorities/..."
转换成
"content://插件authorities/..."

接着解决第二问题,将插件中的ContentProvider"安装"到AMS中,由于平常项目中,需要将插件(apk,jar,dex)文件下载到sd卡等外部路径中,这就需要将插件文件解析后获取到里面的ContentProvider才能去"安装",下面解决插件的解析,了解过插件apk,jar,dex文件解析的读者都知道,加载sd卡中的插件文件,需要使用DexClassLoader,因为PathClassLoader只能加载app内部路径的文件。下面看看具体解析插件中的ContentProvider的实现:

public static List getPluginProviderInfoList(File apkFile){
    List<ProviderInfo> providerInfos = new ArrayList<>(0);
    try {
        Class<?> packageParserClazz = Class.forName("android.content.pm.PackageParser");
        //反射这个方法
        //public Package parsePackage(File packageFile, int flags) throws PackageParserException {
        //    return parsePackage(packageFile, flags, false /* useCaches */);
        //}
        Class[] p = {File.class,int.class};

        Method parsePackageMethod = packageParserClazz.getDeclaredMethod("parsePackage", p);
        parsePackageMethod.setAccessible(true);
        Object[] v = {apkFile,PackageManager.GET_PROVIDERS};
        //解析插件apk
        Object packageParser = packageParserClazz.newInstance();
        Object packageObject = parsePackageMethod.invoke(packageParser, v);
        //PackageParser$Package类中存放ContentProvider的集合是providers,要拿到这个字段的值,这样就拿到了插件apk中的
        //所有ContentProvider
        List providers = (List) RefInvokeUtils.getObject(packageObject.getClass(), "providers", packageObject);

        //反射PackageParser类的generateProviderInfo,将Provider转换成ProviderInfo,
        //第二个参数flags,直接传0,还未弄清楚
        // public static final ProviderInfo generateProviderInfo(Provider p, int flags,
        // PackageUserState state, int userId)


        Class<?> packageUserStateClazz = Class.forName("android.content.pm.PackageUserState");
        Object packageUserState = packageUserStateClazz.newInstance();
        Class<?> userHandleClazz = Class.forName("android.os.UserHandle");
        Field ownerField = userHandleClazz.getDeclaredField("OWNER");
        Object userHandle = ownerField.get(null);
        Method getCallingUserIdMethod = userHandleClazz.getDeclaredMethod("getCallingUserId");
        Object userId = getCallingUserIdMethod.invoke(userHandle);
        Class<?> providerClazz = Class.forName("android.content.pm.PackageParser$Provider");
        Class[] parameterTypes = {providerClazz,int.class,packageUserStateClazz,int.class};
        Method generateProviderInfoMethod = packageParserClazz.getDeclaredMethod("generateProviderInfo", parameterTypes);

        for (Object provider:providers){
            ProviderInfo providerInfo = (ProviderInfo) generateProviderInfoMethod.invoke(packageParser, provider, 0, packageUserState, userId);
            LogUtil.i("替换前      "+providerInfo.applicationInfo.packageName);
            providerInfos.add(providerInfo);
        }
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    }
    return providerInfos;

}

通过这种方式,可以获取到插件中的所有ContentProvider信息。下面接着就是安装这些插件中的ContentProvider,
ActivityThread类有个installContentProviders就是专门用于"安装"ContentProvider的,所有,可以通过反射这个方法来手动的将插件中的ContnetProvider安装到AMS中,下面是"安装"的具体实现:

public static void installPluginContentProviders(Context context,String apkFileName) throws FileNotFoundException {
        File apkFile_dir = context.getDir(HookHelper.PLUGIN_ODEX,Context.MODE_PRIVATE);
        String filePath = apkFile_dir.getAbsolutePath()+File.separator+apkFileName;
        File apkFile = new File(filePath);
        if(!apkFile.exists()){
            throw new FileNotFoundException(apkFileName+"  not found");
        }
        File optDex_dir = context.getDir(OPT_DEX, Context.MODE_PRIVATE);
        if(optDex_dir.exists()){
            optDex_dir.delete();
        }
        optDex_dir.mkdirs();

        //关键1
        addPluginDex(apkFile.getAbsolutePath(),optDex_dir.getAbsolutePath(), context.getClassLoader(),false);


        List<ProviderInfo> providerInfoList = getPluginProviderInfoList(apkFile);
        LogUtil.e(providerInfoList+"");
        for(ProviderInfo providerInfo:providerInfoList){
            //将插件ContentProvider替换成宿主的包名,否则安装无效
            providerInfo.applicationInfo.packageName = context.getPackageName();
        }


        try {
            Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
            Field sCurrentActivityThreadFiled = RefInvokeUtils.getField(activityThreadClazz, "sCurrentActivityThread");
            sCurrentActivityThreadFiled.setAccessible(true);
            Object sCurrentActivityThread = sCurrentActivityThreadFiled.get(null);

            //反射ActivityThread类的installContentProviders方法,将插件apk中的ContentProviders安装到宿主app中
            //private void installContentProviders(
            //        Context context, List<ProviderInfo> providers)

            Class[] p = {Context.class,List.class};
            Method installContentProvidersMethod = activityThreadClazz.getDeclaredMethod("installContentProviders", p);
            installContentProvidersMethod.setAccessible(true);
            //这里要将context的getClassLoader的返回值替换成DexClassLoader
            Object[] v = {context,providerInfoList};
            installContentProvidersMethod.invoke(sCurrentActivityThread,v);
            LogUtil.i(providerInfoList+"");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
}

这个实现方法中,如果将关键点1下面的代码注释掉,如果是直接解析插件apk来获取插件中的ContentProvider,会报find not class 异常,读者开始就是按照直接解析插件apk中的ContentProvider,然后手动的"安装"到AMS中,但是确出现了 find not class 异常,后面根据这个异常分析了一下源码发现,ActivityThread类的installContentProviders方法内部会调用installProvider方法,这方法内部,会通过传入的context这个对象创建ClassLoader,我们安装apk之后会在/data/dalvik-cache目录下生产一 个名为 data@app@包名-1.apk@classes.dex 的ODEX文件,而PathClassLoader要加载apk的时候会到这个文件夹下找对应的dex文件。(ODEX文件就是经过优化的dex文件),当作者在"安装"插件中的ContentProvider时,传入的是宿主的context,这个context获取的classLoader就是PathClassLoader,在安装的过程中,当调动了ActivityThread类的installProvider时,这个方法内部,会通过这个context获取的PathClassLoader来加载插件apk中的ContentProvider字节码文件,由于插件apk中的文件复制的路径不是这个/data/dalvik-cache目录,所以PathClassLoader就无法找到插件apk中的ContentProvider字节码文件,导致报 find not class这个异常。既然搞清楚了报异常的原因,那就只能通过另外一种方式来解决这个问题,可以将插件中的dex文件取出,合并到宿主的DexElements中,这样PathClassLoader就能找到插件的ContentProvider类了。下面是将插件apk中的dex文件合并到宿主的DexElements数组的具体实现:

 private static void addPluginDex(String dexPath,String optDir,ClassLoader classLoader,boolean fixBug) {
        try {

            //public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
            //    super((String)null, (File)null, (String)null, (ClassLoader)null);
            //    throw new RuntimeException("Stub!");
            //}
            Class[] p = {String.class,String.class,String.class,ClassLoader.class};
            Object[] v = {dexPath,optDir,null,classLoader};
            Constructor<DexClassLoader> dexClassLoaderConstructor = DexClassLoader.class.getDeclaredConstructor(p);
            DexClassLoader dexClassLoader = dexClassLoaderConstructor.newInstance(v);
            Field pathListField = DexClassLoader.class.getSuperclass().getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Object pluginPathList = pathListField.get(dexClassLoader);
            //插件中的dexElements
            Object[] pluginDexElements = (Object[]) RefInvokeUtils.getObject(pluginPathList.getClass(), "dexElements", pluginPathList);
            PathClassLoader pathClassLoader = (PathClassLoader) classLoader;
            Object pathList = RefInvokeUtils.getObject(pathClassLoader.getClass().getSuperclass(), "pathList", pathClassLoader);
            Object[] dexElements = (Object[]) RefInvokeUtils.getObject(pathList.getClass(), "dexElements", pathList);
            Class<?> componentType = dexElements.getClass().getComponentType();
            Object newDexElements = Array.newInstance(componentType, pluginDexElements.length + dexElements.length);
            if(!fixBug){
                //如果不是修复bug的dex文件,则将插件dex合并到新的Dex数组的后面
                System.arraycopy(dexElements,0,newDexElements,0,dexElements.length);
                System.arraycopy(pluginDexElements,0,newDexElements,dexElements.length,pluginDexElements.length);
            }else {
                //如果不是修复bug的dex文件,则将插件dex合并到新的Dex数组的前面
                System.arraycopy(pluginDexElements,0,newDexElements,0,pluginDexElements.length);
                System.arraycopy(dexElements,0,newDexElements,pluginDexElements.length,dexElements.length);
            }
            RefInvokeUtils.setObject(pathList.getClass(),"dexElements",pathList,newDexElements);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
}

完成上面两个步骤,就完成了ContentProvider类,下面就是在何时调用上面的"安装"插件ContentProvider方法了,由于宿主apk安装时后启动时,ActivityThread会在application启动onCreate方法之前,attachBaseContext方法之后去"安装"宿主自身的ContentProvider,所以,建议在attachBaseContext方法中调用"安装"插件ContentProvider方法。

protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
        //需要提前将assets目录下的插件apk复制到app的内部存储路径
        HookHelper.copyApk2Inner(this,"plugin1-debug.apk");

        try {
            HookHelper.installPluginContentProviders(this,"plugin1-debug.apk");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
      
    }

这样,在其他app中给宿主的插件中的Provider发送请求时,宿主中的代理ContentProvider就会先收到请求,在将请求转发给插件ContentProvider。
下面是宿主中的代理ContentProvider的具体实现:

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import test.cn.example.com.util.LogUtil;

public class StubContentProvider extends ContentProvider {
    private static final String PLUGINAUTHHOST= "com.android.skill";
    @Override
    public boolean onCreate() {
        LogUtil.i("代理ContentProvider onCreate");
        return false;
    }

    @Nullable
    @android.support.annotation.Nullable
    @Override
    public Cursor query(@NonNull @android.support.annotation.NonNull Uri uri, @Nullable @android.support.annotation.Nullable String[] projection, @Nullable @android.support.annotation.Nullable String selection, @Nullable @android.support.annotation.Nullable String[] selectionArgs, @Nullable @android.support.annotation.Nullable String sortOrder) {
        String uriAuthority = uri.getAuthority();
        LogUtil.i("代理ContentProvider query      "+ uriAuthority);
        LogUtil.i("代理ContentProvider query      "+ uri.toString());
        String plugUristring = uri.toString().replaceAll(uriAuthority+"/","");
        Uri plugUri = Uri.parse(plugUristring);
        LogUtil.i("contentProvider的auth    "+plugUri.toString());
        //转发给插件的contentProvider
        getContext().getContentResolver().query(plugUri,null,null,null,null);
        return null;
    }

    @Nullable
    @android.support.annotation.Nullable
    @Override
    public String getType(@NonNull @android.support.annotation.NonNull Uri uri) {
        return null;
    }

    @Nullable
    @android.support.annotation.Nullable
    @Override
    public Uri insert(@NonNull @android.support.annotation.NonNull Uri uri, @Nullable @android.support.annotation.Nullable ContentValues values) {
        return null;
    }

    @Override
    public int delete(@NonNull @android.support.annotation.NonNull Uri uri, @Nullable @android.support.annotation.Nullable String selection, @Nullable @android.support.annotation.Nullable String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(@NonNull @android.support.annotation.NonNull Uri uri, @Nullable @android.support.annotation.Nullable ContentValues values, @Nullable @android.support.annotation.Nullable String selection, @Nullable @android.support.annotation.Nullable String[] selectionArgs) {
        return 0;
    }
}

宿主中的代理ContentProvider在Manifest文件中的声明:

<provider
    android:authorities="test.cn.example.com.androidskill.stub"
    android:name="test.cn.example.com.androidskill.hook.provider.StubContentProvider"
    android:exported="true"/>

第三方向插件ContentProvider发起查询数据请求如下:

Uri uri = Uri.parse("content://test.cn.example.com.androidskill.stub/com.android.skill.plugin_cp_1");
Cursor queryCurosr = getContentResolver().query(uri, null, null, null, null);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值