ContentProvider的插件化,可以借鉴静态广播的思路,在宿主中创建一个代理ContentProvider,第三方的app向插件中的ContentProvider发送增删改查数据的请求,都先找宿主的代理的ContentProvider,代理的ContentProvider在将实际的增删改查的请求转发给插件中的ContentProvider。这就是ContentProvider插件化的基本思路。其中,会有下面几个问题要解决:
-
插件中可能有多个ContentProvider,如何通过宿主中的一个代理ContentProvider就能将对应插件中的多个ContentProvider。
-
宿主中的代理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);