为了演示静态广播的插件化,我们首先想到的也是使用类似Activity的占坑方案,但是,由于广播的action是不确定的,就
无法确定占坑的广播的action,这样就无法使用在宿主中预先使用占坑广播的方案,还有一点,就是,静态广播是在apk安装时就被PMS解析,并将manifest文件中的关于四大组件的配置信息都保存起来,当然也包括了广播的信息。由于插件apk是没有被安装的,所以,插件apk中的静态广播也就无法被存储到PMS中,这样在发送时,AMS就不知道插件中的静态广播的存在,这样,插件中的静态广播也就无法收到广播了。所以,插件apk中的静态广播的接收,需要将插件apk先解析,获取到插件apk文件中的静态广播,然后将这些静态广播通过动态注册的方式注册保存到AMS中,这样,再给插件中的静态广播发送广播消息时,插件中的静态广播才能够接收到广播。这就是插件中静态广播的处理思路。
下面就来具体实现这个过程:
1.获取插件apk中的所有注册的静态广播。由于PMS具体解析apk文件是交给PackageParser类的parsePackage方法来完成的,下面看看这个方法:
http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/content/pm/PackageParser.java
public Package parsePackage(File packageFile, int flags) throws PackageParserException {
return parsePackage(packageFile, flags, false /* useCaches */);
}
这个方法,接收两个参数,第一个参数是apk文件,第二个参数,如果传入PackageManager.GET_RECEIVERS,表示解析时,获取manifest文件中的静态广播的信息。这个方法经过一系列的调用,最后,将解析的recevier信息保存到PackageParser类的静态内部类Package的receivers集合中的。
所以,可以通过反射创建一个PacageParser对象,并调用parsePackage方法,来将插件apk文件解析,并获取到PackageParser的静态内部类Package类中的receivers集合,拿到这个receviers集合,就拿到了插件apk文件中的所有静态广播的信息了。
下面看看具体代码实现获取插件apk中的静态广播
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import dalvik.system.DexClassLoader;
import test.cn.example.com.util.LogUtil;
public class HookHelper {
public static List getPluginStaticReceivers(Context context,File apkFile){
try {
Class<?> packageParserClazz = Class.forName("android.content.pm.PackageParser");
Object packageParser = packageParserClazz.newInstance();
//反射下面这个方法
//public Package parsePackage(File packageFile, int flags) throws PackageParserException {
// return parsePackage(packageFile, flags, false /* useCaches */);
//}
Class[] parameterTypes = new Class[]{File.class,int.class};
Method parsePackageMethod = packageParserClazz.getDeclaredMethod("parsePackage",parameterTypes);
parsePackageMethod.setAccessible(true);
Object[] args = {apkFile,PackageManager.GET_RECEIVERS};
Object packageObject = parsePackageMethod.invoke(packageParser, args);
List receivers = (List) RefInvokeUtils.getObject(packageObject.getClass(), "receivers", packageObject);
return receivers;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
}
RefInvokeUtils这个工具类的代码如下:
//RefInvokeUtils.java
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class RefInvokeUtils {
private RefInvokeUtils(){}
public static Field getField(Class clazz,String fieldName) throws NoSuchFieldException {
return clazz.getDeclaredField(fieldName);
}
public static Field getField(String className,String fieldName){
try {
Class<?> clazz = Class.forName(className);
Field declaredField = clazz.getDeclaredField(fieldName);
declaredField.setAccessible(true);
return declaredField;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
public static Object getObject(String className,String fieldName,Object obj){
Field field = getField(className, fieldName);
try {
return field.get(obj);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
public static Object getObject(Class clazz,String fieldName,Object obj) throws NoSuchFieldException, IllegalAccessException {
Field field = getField(clazz, fieldName);
field.setAccessible(true);
return field.get(obj);
}
public static void setObject(Class clazz,String fieldName,Object obj,Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = getField(clazz, fieldName);
field.setAccessible(true);
field.set(obj,value);
}
public static void setObject(String className,String fieldName,Object obj,Object value){
Field field = getField(className, fieldName);
try {
field.set(obj,value);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public static Object getInstance(String className,Class[] parameterTypes,Object[] initArgs){
try {
Class<?> clazz = Class.forName(className);
Constructor<?> constructor = clazz.getConstructor(parameterTypes);
Object newInstance = constructor.newInstance(initArgs);
return newInstance;
} 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();
}
return null;
}
public static Object getInstance(String className){
try {
Class<?> clazz = Class.forName(className);
Object instance = clazz.newInstance();
return instance;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return null;
}
}
2.拿到插件中的所有静态注册的广播后,将这些静态的广播在宿主中,进行动态注册
public static void registerPluginStaticReceivers(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");
}
try {
List receivers = getPluginStaticReceivers(apkFile);
DexClassLoader dexClassLoader = (DexClassLoader) HookHelper.getClassLoader(context, apkFile.getName());
for(Object receiver:receivers){
Bundle metaData = (Bundle) RefInvokeUtils.getObject("android.content.pm.PackageParser$Component", "metaData", receiver);
String oldAction = metaData.getString("oldAction");
List<? extends IntentFilter> filters = (List) RefInvokeUtils.getObject("android.content.pm.PackageParser$Component", "intents", receiver);
for(IntentFilter intentFilter:filters){
ActivityInfo activityInfo = (ActivityInfo) RefInvokeUtils.getObject(receiver.getClass(), "info", receiver);
//这里创建插件广播实例时,要用DexClassLoader,否则会出现找不到class的异常
BroadcastReceiver pluginReceiver = (BroadcastReceiver) dexClassLoader.loadClass(activityInfo.name).newInstance();
//这里不能通过context来动态注册广播,否则会报
//android.content.ReceiverCallNotAllowedException: BroadcastReceiver components are not allowed to register to receive intents
// context.registerReceiver(pluginReceiver,intentFilter);
//下面这个方法如果在自定义的application的attachBaseContext方法中调用,会报下面这个异常
//Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.Intent android.content.Context.registerReceiver(android.content.BroadcastReceiver, android.content.IntentFilter)' on a null object reference
context.getApplicationContext().registerReceiver(pluginReceiver,intentFilter);
String pluginAction = intentFilter.getAction(0);
old2newActionsMap.put(oldAction,pluginAction);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
3.完成这两个步骤后,还缺少一个步骤,如何将插件apk文件加载到内存。下面看具体实现(这里先将插件apk文件复制到了assets目录下,然后将其复制到app的内部存储空间中)
//HookHelper.java类
public static void copyApk2Inner(Context context,String apkName){
AssetManager assetManager = context.getAssets();
InputStream inputStream = null;
BufferedOutputStream bos = null;
try {
inputStream = assetManager.open(apkName);
File plugin_odex_dir = context.getDir("plugin_odex", Context.MODE_PRIVATE);
LogUtil.i("文件夹目录的路径是 "+plugin_odex_dir.getAbsolutePath());
String filePath = plugin_odex_dir.getAbsolutePath()+File.separator+apkName;
File file = new File(filePath);
if(file.exists()){
file.delete();
}
//注意,这里是要传具体的文件路径构建的File对象,不是文件所在的文件夹的路径构建的File对象
bos = new BufferedOutputStream(new FileOutputStream(file));
byte[] bytes = new byte[1024];
int len = 0;
while ((len= inputStream.read(bytes))!=-1){
bos.write(bytes,0,len);
}
//完成了将插件apk从assets目录复制到apk内部的odex目录下面
if(file.exists()){
LogUtil.i("文件复制成功 "+file.getAbsolutePath());
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(null != bos){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(null != inputStream){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
完成上面三个步骤后,这样就完成了将插件apk中的静态广播注册为动态广播了。接着在自定义的application类的attachBaseContext方法中,将插件apk复制到宿主app的内部存储路径
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
//需要提前将assets目录下的插件apk复制到app的内部存储路径
HookHelper.copyApk2Inner(this,"plugin1-debug.apk");
}
}
在自定义的application的onCreate方法中,将插件apk中的静态广播注册为动态广播。
@Override
public void onCreate() {
super.onCreate();
//只能放到application的onCreate方法中,不能放到attachBaseContext方法中,因为HookHelper.registerPluginStaticReceivers
//方法中,要用到application对象,但是在attachBaseContext方法中,这个对象还未创建。
try {
HookHelper.registerPluginStaticReceivers(this,"plugin1-debug.apk");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
至于插件apk怎么生成,这个读者自己去写,本案例是在在插件中注册了两个静态广播。
后面就可以给插件的静态广播发送广播了。打印日志如下:
10-24: StubReceiver.java::15::onReceive–>>接收到静态广播 com.android.skill.braocastrecevier
10-24: 插件recevier1 com.android.skill.plugin.test.static.braocastrecevier
10-24: StubReceiver.java::15::onReceive–>>接收到静态广播 com.android.skill.braocastrecevier2
10-24: 插件recevier2 com.android.skill.plugin.test.static.braocastrecevier2
这种处理方式,有个前提,就是宿主app必须要启动,这样的话,就失去了静态广播的特性,不需要启动app就能收到广播,为了解决这个问题,可以通过在宿主app中,注册一个代理的静态广播,通过给插件apk中的静态广播发送广播时,其实是给宿主中的静态代理广播发送广播,这样,宿主app的进程就会启动,这样宿主app的自定义的application的attachBaseContext方法和onCreate方法就会被调用,这样,就会将插件中的静态广播转变为动态注册的广播,然后宿主中的静态代理的广播在接收到广播后,获取到intent信息中实际要发送的插件的广播的action信息,然后给插件中的静态广播接收者发送广播,这样静态广播接收者就能接收到广播了。具体实现是:
在宿主app中,新增一个代理的静态广播:
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import test.cn.example.com.androidskill.hook.HookHelper;
import test.cn.example.com.util.LogUtil;
import test.cn.example.com.util.ToastUtils;
public class StubReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String newAction = intent.getAction();
LogUtil.i("接收到静态广播 "+newAction);
ToastUtils.shortToast(context,"接收到静态广播");
String pluginAction = HookHelper.old2newActionsMap.get(newAction);
context.sendBroadcast(new Intent(pluginAction));
}
}
接着在宿主app的manifest文件中,静态注册这个广播
<receiver
android:name="test.cn.example.com.androidskill.hook.receiver.StubReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.android.skill.braocastrecevier"></action>
</intent-filter>
<intent-filter>
<action android:name="com.android.skill.braocastrecevier2"></action>
</intent-filter>
</receiver>
这里给这个静态广播配置了多个intent-filter,是为了方便一个静态代理广播对应插件中的多个静态广播。本例为了方便演示,只是配置了两个intent-filter。
在接着就是要重新处理一下插件apk中的manifest文件中配置的静态广播信息
<receiver android:name="com.android.skill.PluginReceiverOne"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.android.skill.plugin.test.static.braocastrecevier"/>
</intent-filter>
<meta-data android:name="oldAction" android:value="com.android.skill.braocastrecevier"/>
</receiver>
<receiver android:name="com.android.skill.PluginReceiverTwo"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.android.skill.plugin.test.static.braocastrecevier2"/>
</intent-filter>
<meta-data android:name="oldAction" android:value="com.android.skill.braocastrecevier2"/>
</receiver>
注意,插件中的静态广播,配置了meta-data,里面的value属性的值,和宿主app的静态代理广播的intent-filter中的action是相同的,这样做是为了让插件中的静态广播和宿主中的代理静态广播建立多对一的关系,当在宿主中给插件中的静态广播接收者发送广播时,就可以设置action为这个meta-data这个标签中设置的value属性的值,这样,在宿主app的代理静态广播中获取到这个meta-data中的值,然后,通过这个meta-data中的value属性的值,找到真正要执行接收的插件的静态广播。具体查找,是通过在宿主中解析插件apk的过程中,解析出插件apk中的静态广播的intent-filter中的action,和meta-data标签中的value值,做一个映射,将这两个信息放入到一个map集合中保存,当宿主中的代理静态广播收到广播后,可以从intent获取到插件中静态广播配置的meta-data的value的值,然后通过map,找到插件广播实际的action的值,这样就可以在宿主app的代理静态广播中对插件中的静态广播接收者发送广播了。由于宿主app的代理静态广播不需要启动就能接收到广播,代理静态广播在转发给插件中的静态广播接收者,这样就完成了静态插件广播所在的app无需启动,也能使插件apk中的静态注册的广播接收者接收到广播。