1.资源分类
一个项目的目录大致入下,其中assets和res都是资源文件
assets中的资源:
1.通过AssetsManager访问;
2.在打包的时候不会编译成二进制文件;
3.不会在R.java中生成id;
4.在assets目录下可以创建子文件夹;
res文件中的资源:
1.通过context.getResource()访问;
2.会被编译成二进制文件,raw文件除外
3.会在编译时生成R文件
系统构造Resource的过程参考Resource管理器
2.加载其他已安装应用的资源文件:
2.1 实现关键代码:
1.通过Context的createPackageContext方法得到其他App的Context,从而得到Resource对象
Context remoteContext = mContext.createPackageContext(packageName, flag);
Resource remoteResource = context.getResources()
2.由于Resource中的资源只能通过ID来访问,现在我们只知道资源名,所以要通过查询R文件把资源名转化为资源ID:
//根据packageName和资源类型拼写R文件的类名:
String rClassName = packageName + ".R$" + type;
//通过反射加载R文件:
Class cls = installedResource.classLoader.loadClass(rClassName);
//查询R文件得到资源名称对应的资源ID
resID = (Integer) cls.getField(fieldName).get(null);
3.有了Resource和资源ID,就可以轻易获得我们需要的资源:
resources.getString(resourceID)
2.2 将上面的逻辑简单封住成一个工具类:
1.首先创建一个JavaBean装载解析结果
public class LoadedResource {
public Resources resources;
public String packageName;
public ClassLoader classLoader;
}
2.实现工具类:
public static class InstalledResourceLoader {
static final InstalledResourceLoader loader = new InstalledResourceLoader ();
private Context mContext;
private Map<String, LoadedResource> mResources = new HashMap<String, LoadedResource>();
private InstalledResourceLoader () {}
public void init(Context context) {
mContext = context.getApplicationContext();
}
public LoadedResource getInstalledResource(String packageName) {
// 先从缓存中取, 没有就去加载
LoadedResource resource = mResources.get(packageName);
if (resource == null) {
try {
//createPackageContext可以创建其它程序的Context
//通过创建的这个Context,就可以访问该软件包的资源,甚至可以执行其它软件包的代码
Context context = mContext.createPackageContext(packageName, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
resource = new LoadedResource();
resource.packageName = packageName;
resource.resources = context.getResources();
resource.classLoader = context.getClassLoader();
mResources.put(packageName, resource); // 得到结果缓存起来
} catch (Exception e) {
e.printStackTrace();
}
}
return resource;
}
public int getResourceID(String packageName, String type, String fieldName) {
int resID = 0;
// 获取已安装APK的资源
LoadedResource installedResource = getInstalledResource(packageName);
if (installedResource != null) {
// 根据匿名内部类的命名, 拼写出R文件的包名+类名
String rClassName = packageName + ".R$" + type;
try {
// 加载R文件
Class cls = installedResource.classLoader.loadClass(rClassName);
// 反射获取R文件对应资源名的ID
resID = (Integer) cls.getField(fieldName).get(null);
} catch (Exception e) {
e.printStackTrace();
}
}
return resID;
}
public String getString(String packageName, String fieldName) {
String s = null;
int resourceID = getResourceID(packageName, "string", fieldName);
LoadedResource installedResource = getInstalledResource(packageName);
if (installedResource != null) {
s = installedResource.resources.getString(resourceID);
}
return s;
}
}
3.加载未安装应用的资源文件:
3.1 实现关键代码:
1.构造一个路径去存放解压优化后的dex文件,这个路径是为了构造一个远端APK的类加载器:
File dexDir = mContext.getDir("dex", Context.MODE_PRIVATE);
mDexDir = dexDir.getAbsolutePath();
2.构造远端APK的类加载器,这个加载器用来加载R文件:
loadResource.classLoader = new DexClassLoader(resourcePath, mDexDir, null, mContext.getClassLoader());
3.通过远端APK文件的完整路径获取PackageInfo,从而获取远端APK的包名:
PackageInfo info = mContext.getPackageManager()
.getPackageArchiveInfo(resourcePath, PackageManager.GET_ACTIVITIES);
String packageName = info.packageName;
4.为AssetManager设置远端APK路径,然后用这个AssetManager构造Resources,这样就得到了这个APK中的Resource对象:
AssetManager assetManager = AssetManager.class.newInstance();
Class cls = AssetManager.class;
Method method = cls.getMethod("addAssetPath", String.class);
method.invoke(assetManager, resourcePath);
Resources resources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(),
5.由于Resource中的资源只能通过ID来访问,现在我们只知道资源名,所以要通过查询R文件把资源名转化为资源ID:
//根据packageName和资源类型拼写R文件的类名:
String rClassName = packageName + ".R$" + type;
//通过反射加载R文件:
Class cls = installedResource.classLoader.loadClass(rClassName);
//查询R文件得到资源名称对应的资源ID
resID = (Integer) cls.getField(fieldName).get(null);
6.有了Resource和资源ID,就可以轻易获得我们需要的资源:
resources.getString(resourceID)
3.2 将上面的逻辑简单封住成一个工具类:
1.创建一个远端APK作为被加载对象,该项目中含有一个id为client_string的字符串,打包并放入sd卡根目录;
2.创建一个JavaBean装载解析结果
public class LoadedResource {
public Resources resources;
public String packageName;
public ClassLoader classLoader;
}
3.然后编写相关的工具类:
public class UnInstalledResourceLoader {
static final UnInstalledResourceLoader loader = new UnInstalledResourceLoader ();
private Context mContext;
private Map<String, LoadedResource> mRescources = new HashMap<String, LoadedResource>();
//由于解析APK只能用DexClassLoader,构造DexClassLoader需要一个路径mDexDir,作为dex解压缩优化后的存放目录
private String mDexDir;
private UnInstalled() {
}
//这里初始化主要是构造一个mDexDir,因为这个路径是构造DexClassLoader的必要参数
public void init(Context context) {
mContext = context.getApplicationContext();
File dexDir = mContext.getDir("dex", Context.MODE_PRIVATE);
if (!dexDir.exists()) {
dexDir.mkdir();
}
mDexDir = dexDir.getAbsolutePath();
}
public int getResourceID(String packageName, String type, String fieldName) {
int resID = 0;
LoadedResource recource = mRescources.get(packageName);
//寻找类名为com.example.client.R$string的类
String rClassName = packageName + ".R$" + type;
try {
Class cls = recource.classLoader.loadClass(rClassName);
//在com.example.client.R$string类中获取名为fieldName的成员变量的值,它就是这个资源的id
resID = (Integer) cls.getField(fieldName).get(null);
} catch (Exception e) {
e.printStackTrace();
}
return resID;
}
public String getString(String packageName, String fieldName) {
String s = null;
int resourceID = getResourceID(packageName, "string", fieldName);
LoadedResource recource = mRescources.get(packageName);
if (recource != null) {
s= recource.resources.getString(resourceID);
}
return s;
}
public LoadedResource loadResource(String resourcePath) {
LoadedResource loadResource = null;
//获取未安装APK的PackageInfo
//这里必须要有sd卡读权限,否则info返回值为null
PackageInfo info = mContext.getPackageManager().getPackageArchiveInfo(resourcePath, PackageManager.GET_ACTIVITIES);
if (info != null) { // 获取成功
// 先从缓存中取, 存在则直接返回, 不重复添加. 否则就搜索添加
loadResource = mRescources.get(info.packageName);
if (loadResource == null) {
try {
// 创建AssetManager实例
AssetManager assetManager = AssetManager.class.newInstance();
Class cls = AssetManager.class;
Method method = cls.getMethod("addAssetPath", String.class);
// 反射设置资源加载路径
method.invoke(assetManager, resourcePath);
Resources resources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(),
// 构造出正确的Resource
mContext.getResources().getConfiguration());
loadResource = new LoadedResource();
loadResource.resources = resources;
//这里解析得到的包名为com.example.client
loadResource.packageName = info.packageName;
// 设置正确的类加载器, 因为需要去加载R文件
loadResource.classLoader = new DexClassLoader(resourcePath, mDexDir, null,
mContext.getClassLoader());
mRescources.put(info.packageName, loadResource); // 缓存
} catch (Exception e) {
e.printStackTrace();
}
}
}
return loadResource;
}
}
4.记得动态获取APK资源一定要SD卡读写权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
private boolean getPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == 1) { //这里的requestCode 就是动态申请权限时传入的最后一个参数
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
}
}
}
5.最后验证是否可以成功调用资源文件:
String url = Environment.getExternalStorageDirectory().getAbsolutePath() + "/app-release.apk";
LoadedResource loadResource = ResourceManager.unInstalled().loadResource(url);
String ss = ResourceManager.unInstalled().getString(loadResource.packageName, "client_string");
Log.d("clientString = ", ss);