在没有安装apk的情况下提供资源给宿主app使用

使用DexClassLoader这个类来加载未安装的apk,提供资源供宿主app使用

首先的思路肯定也是下载到我们手机的某个目录里。然后分别得到插件apk的信息(名称、包名等),然后显示可用的插件,最后动态加载apk获得资源

然后要想到的问题就是要怎么获取到apk的包信息,因为现在要的是获取未安装apk的资源,无法通过createPackageContext(…);方法来构建出一个context,所以这时只有在Resource上下功夫

1,在PackageManager类中有这样的一个方法getPackageArchiveInfo(),是用来获取未安装apk的信息

public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags)

这个方法的参数刚好是传入一个FilePath,然后返回apk文件的PackageInfo信息:

/**
 * 获取未安装apk的信息
 * @param context
 * @param archiveFilePath apk文件的path
 * @return
 */
private String[] getUninstallApkInfo(Context context, String archiveFilePath) {
    String[] info = new String[2];
    PackageManager pm = context.getPackageManager();
    PackageInfo pkgInfo = pm.getPackageArchiveInfo(archiveFilePath, PackageManager.GET_ACTIVITIES);
    if (pkgInfo != null) {
        ApplicationInfo appInfo = pkgInfo.applicationInfo;
        String versionName = pkgInfo.versionName;//版本号
        Drawable icon = pm.getApplicationIcon(appInfo);//图标
        String appName = pm.getApplicationLabel(appInfo).toString();//app名称
        String pkgName = appInfo.packageName;//包名
        info[0] = appName;
        info[1] = pkgName;
    }
    return info;
}

2、得到对应未安装apk的Resource对象,我们需要通过反射来获得:

/**
 * @param apkName
 * @return 得到对应插件的Resource对象
 */
private Resources getPluginResources(String apkName) {
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);//反射调用方法addAssetPath(String path)
        //第二个参数是apk的路径:Environment.getExternalStorageDirectory().getPath()+File.separator+"plugin"+File.separator+"apkplugin.apk"
        addAssetPath.invoke(assetManager, apkDir+File.separator+apkName);//将未安装的Apk文件的添加进AssetManager中,第二个参数为apk文件的路径带apk名
        Resources superRes = this.getResources();
        Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),
                superRes.getConfiguration());
        return mResources;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

通过得到AssetManager中的内部的方法addAssetPath,将未安装的apk路径传入从而添加进assetManager中,然后通过new Resource把assetManager传入构造方法中,进而得到未安装apk对应的Resource对象。

解决了上面的两个问题,接下来就是加载未安装的apk获得它的内部资源。

/**
 * 加载apk获得内部资源
 * @param apkDir apk目录
 * @param apkName apk名字,带.apk
 * @throws Exception
 */
private void dynamicLoadApk(String apkDir, String apkName, String apkPackageName) throws Exception {
    File optimizedDirectoryFile = getDir("dex", Context.MODE_PRIVATE);//在应用安装目录下创建一个名为app_dex文件夹目录,如果已经存在则不创建
    Log.v("zxy", optimizedDirectoryFile.getPath().toString());// /data/data/com.example.dynamicloadapk/app_dex
    //参数:1、包含dex的apk文件或jar文件的路径,2、apk、jar解压缩生成dex存储的目录,3、本地library库目录,一般为null,4、父ClassLoader
    DexClassLoader dexClassLoader = new DexClassLoader(apkDir+File.separator+apkName, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
    Class<?> clazz = dexClassLoader.loadClass(apkPackageName + ".R$mipmap");//通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源id
    Field field = clazz.getDeclaredField("one");//得到名为one的这张图片字段
    int resId = field.getInt(R.id.class);//得到图片id
    Resources mResources = getPluginResources(apkName);//得到插件apk中的Resource
    if (mResources != null) {
        //通过插件apk中的Resource得到resId对应的资源
        findViewById(R.id.background).setBackgroundDrawable(mResources.getDrawable(resId));
    }
}

其中通过new DexClassLoader()来创建未安装apk的类加载器,我们来看看它的参数:

public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

dexPath就是apk文件的路径,
optimizedDirectory就是apk解压缩后的存放dex的目录,要注意的是,在4.1以后该目录不允许在sd卡上,所以我们用getDir()方法在应用内部创建一个dexOutputDir。
libraryPath为本地的library,一般为null。 parent为 父加载器

后面就是用反射来获取出需要的资源。

下面是demo演示的效果,我是把三个apk插件先放在assets目录下,然后copy到sd上来模仿下载过程,然后加载出相应插件的资源:

copyApkFile("apkthemeplugin-1.apk"); 
copyApkFile("apkthemeplugin-2.apk"); 
copyApkFile("apkthemeplugin-3.apk"); 

显示前
这里写图片描述

clipboard.png

显示后
这里写图片描述

clipboard.png


灵机文化
71 声望8 粉丝