宿主启动插件中的BroadCastReceiver和ContentProvider

BroadcastReceiver插件化解决方案

  1. 静态广播和动态广播仅区别于注册方式的不同。静态广播的注册信息保存在PMS中,动态广播的注册信息保存在AMS中

  2. 发送广播,也就是Context的sendBroadcast方法,最终会调用ActivityManager.getService().broadcastIntentWithFeature,把要发送的广播告诉AMS;AMS在收到上述信息后,搜索AMS和PMS中保存的广播,看哪些广播符合条件,然后通知App进程启动这些广播,也就是调用这些广播的onReceive方法

  3. 注册广播和发送广播时,都需要携带一个筛选条件:intent-filter。这样才能匹配到相应的BroadCastReceiver。

静态广播注册


    
        

动态广播注册


    
        

发送广播

Intent intent = new Intent();
intent.setAction("baobao2");
intent.putExtra("msg","jianqiang");
sendBroadcast(intent);

动态广播的插件化解决方案

使用前面介绍的dex合并技术,插件中的动态广播就可以被宿主App正常调用了

public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
            throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        // 获取 BaseDexClassLoader : pathList
        //clazz: dalvik.system.BaseDexClassLoader
        Class clazz = DexClassLoader.class.getSuperclass();
        Object pathListObj = RefInvoke.getFieldObject(clazz, 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) 这个构造函数
        // 构造插件Element
        Class[] params = {DexFile.class,File.class,};
        DexFile dexFile1 = DexFile.loadDex(apkFile.getCanonicalPath(),optDexFile.getAbsolutePath(),0);
        Object[] values = {dexFile1,apkFile};
        Object dexObj = RefInvoke.createObject(elementClass,params,values);

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

        // 替换
        RefInvoke.setFieldObject(pathListObj, "dexElements", newElements);
    }

ApplicationattachBaseContext中:

	//解压到本地
        Utils.extractAssets(this, apkName);

        File dexFile = getFileStreamPath(apkName);
        File optDexFile = getFileStreamPath(dexName);

        try {
            // Hook 动态注册
            BaseDexClassLoaderHookHelper.patchClassLoader(getClassLoader(), dexFile, optDexFile);
        } catch (Throwable e) {
            e.printStackTrace();
        }

静态广播的插件化解决方案

首先我们回忆一下在实现Activity与Service插件化之前我们都会在宿主中预埋一个对应的Activity或者Service用于欺骗AMS;对于这里的静态广播插件化处理也同样不例外需要在宿主中预埋一个Receiver,只是该Receiver的作用不再是欺骗AMS,而单纯是在接收到对应Action的广播之后再转发给插件静态注册的广播接收器。

插件中静态注册广播解析原理

  1. PMS只能读取宿主App的AndroidManifest文件,读取其中的静态广播并注册。我们可以通过反射,手动控制PMS读取插件的AndroidManifest中声明的静态广播列表

  2. 遍历这个静态广播列表。使用插件的classLoader加载列表中的每个广播类,实例化成一个对象,然后作为动态广播注册到AMS中

PMS是通过PackageParser.parserPackage 来解析APK。parserPackage方法的定义如下:

/**
 * 
 * @param packageFile  可以指定为插件Apk路径
 * @param flags   过滤器,设置为PackageManager.GET_RECEIVERS 时就返回apk中所有的静态receiver
 * @return   package对象,其中存放着插件apk中声明的静态receiver集合
 */
public Package parsePackage(File packageFile, int flags, boolean useCaches)
        throws PackageParserException {
    Package parsed = useCaches ? getCachedResult(packageFile, flags) : null
    //其它实现
    ......
    return parsed;
}

该方法返回的Package对象是PackageParser类的内部类。在该内部类中中存储着从AndroidManifest.xml文件中解析出来的数据:

public final ArrayList activities = new ArrayList(0);
public final ArrayList receivers = new ArrayList(0);
public final ArrayList providers = new ArrayList(0);
public final ArrayList services = new ArrayList(0);

这里我们只需要关心存储着类对象Activity的receivers列表,注意此Activity非彼Activity,这里的Activity也是PackageParser类的一个内部类而已,其声明如下:

public final static class Activity extends Component implements Parcelable {
    public final ActivityInfo info;
    //其它实现
    ........
}

对于静态注册在插件AndroidManifest.xml文件中的Receiver,宿主需要知道Receiver的className以及其对应注册的Action,这样在宿主中才能实现广播的中转;因此整个解析过程也就需要解析出广播的ClassName以及其注册的Action就OK了。

插件中静态注册广播方案实现

  1. 在宿主的androidmanifest中注册占位StubReceiver

    
        
            

            
        
    

    
        
            
        
        
            
        
        
            
        
        
            
        
        
            
        
        
            
        
        
            
        
        
            
        
        
            
        
        
            
        
    

为插件中每个静态注册的Receiver 配置StubReceiver对应的Action:


            
                
            
            ****
        

        
            
                
            
            ****
        
  1. 解析插件Apk文件中的Receiver并动态注册到宿主中

具体流程是在宿主清单文件中查找jianqiang6,然后取出插件的清单文件中receiver的oldAction的值jianqiang1,最后找到jianqiang6对应的action 为 baobao6,这样就把jianqiang6 和 baobao6 一一对应起来

public class ReceiverHelper {
    private static final String TAG = "ReceiverHelper";
    /**
     * 解析插件Apk文件中的 , 并存储起来
     *
     * @param apkFile 插件apk  receivertest.apk
     * @throws Exception
     */
    public static void preLoadReceiver(Context context, File apkFile) {
        // 首先调用parsePackage获取到apk对象对应的Package对象
        Object packageParser = RefInvoke.createObject("android.content.pm.PackageParser");
        Class[] p1 = {File.class, int.class};
        Object[] v1 = {apkFile, PackageManager.GET_RECEIVERS};
        Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage", p1, v1);

        String packageName = (String)RefInvoke.getFieldObject(packageObj, "packageName");

        // 读取Package对象里面的receivers字段,注意这是一个 List (没错,底层把当作处理)
        // 接下来要做的就是根据这个List 获取到Receiver对应的 ActivityInfo (依然是把receiver信息用activity处理了)
        List receivers = (List) RefInvoke.getFieldObject(packageObj, "receivers");

        for (Object receiver : receivers) {
            registerDynamicReceiver(context, receiver);
        }
    }

    // 解析出 receiver以及对应的 intentFilter
    // 手动注册Receiver
    public static void registerDynamicReceiver(Context context, Object receiver) {
        Bundle metadata = (Bundle)RefInvoke.getFieldObject(
                "android.content.pm.PackageParser$Component", receiver, "metaData");
        String oldAction = metadata.getString("oldAction");

        //取出receiver的intents字段
        List filters = (List) RefInvoke.getFieldObject(
                "android.content.pm.PackageParser$Component", receiver, "intents");

        try {
            // 把解析出来的每一个静态Receiver都注册为动态的
            for (IntentFilter intentFilter : filters) {
                ActivityInfo receiverInfo = (ActivityInfo) RefInvoke.getFieldObject(receiver, "info");

                BroadcastReceiver broadcastReceiver = (BroadcastReceiver) RefInvoke.createObject(receiverInfo.name);
                context.registerReceiver(broadcastReceiver, intentFilter);

                String newAction = intentFilter.getAction(0);
                ReceiverManager.pluginReceiverMappings.put(oldAction, newAction);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  1. 在宿主的StubReceiveronReceiver方法中分发收到的静态广播
public class StubReceiver extends BroadcastReceiver {
    public StubReceiver() {
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        String newAction = intent.getAction();
        // 拿到收到的广播的action 例如 jianqiang6
        if(ReceiverManager.pluginReceiverMappings.containsKey(newAction)) {
            // 获取到对应的插件中的Receiver的action 例如 baobao6
            String oldAction = ReceiverManager.pluginReceiverMappings.get(newAction);
            // 发送给插件中的Receiver6
            context.sendBroadcast(new Intent(oldAction));
        }
    }
}

这样就可以了,缺点是要为StubReceiver配置几百个Action。

源码

ContentProvider的插件化解决方案

和BroadCastReceiver的方案类似。

  1. 在宿主中定义一个StubContentProvider中转,让第三方app调用宿主app的StubContentProvider,再调用插件里的ContentProvider
public class StubContentProvider extends ContentProvider {
    private static final String TAG = "StubContentProvider";

    public static final String AUTHORITY = "cn.scu.myprovider";

    @Override
    public boolean onCreate() {
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return getContext().getContentResolver().query(getRealUri(uri), projection, selection, selectionArgs, sortOrder);
    }

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

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        return getContext().getContentResolver().insert(getRealUri(uri), values);
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return getContext().getContentResolver().delete(getRealUri(uri), selection, selectionArgs);
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return getContext().getContentResolver().update(getRealUri(uri), values, selection, selectionArgs);
    }

    /**
     * 为了使得插件的ContentProvder提供给外部使用,我们需要一个StubProvider做中转;
     * 如果外部程序需要使用插件系统中插件的ContentProvider,不能直接查询原来的那个uri
     * 我们对uri做一些手脚,使得插件系统能识别这个uri;
     *
     * 这里的处理方式如下:
     *
     * 原始查询插件的URI应该为:
     * content://cn.scu.hostprovider
     *
     * 如果需要查询插件,替换为:
	 * content://cn.scu.myprovider/user
     *
     * 也就是,我们在StubProvider中接收到第三方的查询Uri,转换为插件ContentProvider需要的Uri;
     * 然后在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 , "cn.scu.myprovider/user");
        Uri newUri = Uri.parse(uriString);
        Log.i(TAG, "realUri:" + newUri);
        return newUri;
    }
}
  1. 我们仍然需要先合并dex
public class BaseDexClassLoaderHookHelper {
    public static void patchClassLoader(ClassLoader cl, File apkFile,File dexFile) throws IOException {
        // 获取BaseDexClassLoader:pathList
        Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(),cl,"pathList");

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

        // Element类型
        Class elementClass = dexElementsObj.getClass().getComponentType();

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

        // 构建插件Element对象
        Class[] params = {DexFile.class,File.class,};
        DexFile dexFile1 = DexFile.loadDex(apkFile.getCanonicalPath(),dexFile.getAbsolutePath(),0);
        Object[] values = {dexFile1,apkFile};
        Object dexObj = RefInvoke.createObject(elementClass,params,values);
        // 构建插件Element 数组
        Object[] toAddElementArray = new Object[] { dexObj };

        // 把原始的elements复制进去
        System.arraycopy(dexElementsObj,0,newElements,0,dexElementsObj.length);
        // 把插件的elements复制进去
        System.arraycopy(toAddElementArray,0,newElements,dexElementsObj.length,toAddElementArray.length);

        // 替换
        RefInvoke.setFieldObject(pathListObj,"dexElements",newElements);
    }
}
  1. 将静态Provider手动安装到宿主app中,把它们放在宿主的ContentProvider列表中
public class ProviderHelper {
    /**
     * 解析Apk文件中的 , 并存储起来
     * 主要是调用PackageParser类的generateProviderInfo方法
     *
     * @param apkFile 插件对应的apk文件
     * @throws Exception 解析出错或者反射调用出错, 均会抛出异常
     */
    public static List parseProviders(File apkFile) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        // 获取PackageParse对象实例
        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对象里面的providers字段
        // 接下来要做的就是根据这个List 获取到Provider对应的 ProviderInfo
        List providers = (List) RefInvoke.getFieldObject(packageObj,"providers");

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

        // 准备generateProviderInfo方法需要的参数
        Class packageParse$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 = {packageParse$ProviderClass,int.class,packageUserStateClass,int.class};

        List ret = new ArrayList<>();
        // 解析出intent对应的Provider组件
        for(Object provider:providers){
            Object[] v2 = {provider,0,defaultUserState,userId};
            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 installProvides(Context context,File apkFile) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        List providerInfos = parseProviders(apkFile);

        // 将插件中的ContentProvider的packageName设置为当前apk的packageName
        for(ProviderInfo providerInfo:providerInfos){
            providerInfo.applicationInfo.packageName = context.getPackageName();
        }

        Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread","sCurrentActivityThread");

        Class[] p1 = {Context.class,List.class};
        Object[] v1 = {context,providerInfos};
        // 通过反射执行ActivityThread的installContentProviders方法,把ContentProvider作为插件的参数,相当于把这些插件ContentProvider“安装”到了宿主中
        RefInvoke.invokeInstanceMethod(currentActivityThread,"installContentProviders",p1,v1);
    }
}
  1. 别忘记了在宿主的AndroidManifest文件中声明插件的ContentProvider和StubContentProvider
        

        

还有一件事,如果需要跨进程访问ContentProvider,例如在宿主中访问插件的ContentProvider,在第三方中访问宿主的StubContentProvider,在高版本的系统中需要声明,填充的内容是所要访问的apk的包名

 
        
    

源码

你可能感兴趣的:(Android插件化,android)