目录
Android黑科技动态加载(一)之Java中的ClassLoader
Android黑科技动态加载(二)之Android中的ClassLoader
Android黑科技动态加载(三)之动态加载资源
Android黑科技动态加载(四)之插件化开发
我们的认识
我们都知道, 在Android中我们获取一个资源只需要使用
Context.getResource().getXXXX()
就可以获取到对应的资源文件. 那么如果我们想要加载其他应用的res内容
, 那么就应该构造出他们环境的Resource
. 有了Resource还不行, 我们还需要获取资源文件的ID
, 其中ID我们可以通过R.java
文件通过反射获取.
所以我们的目标就是(分别对于已安装的应用和未安装的应用):
- 构造出Resource
- 获取资源的ID
ResourceBundle就是我们的资源包, 其中只有两张图片
android_resource_bundle_load.gif android_resource_bundle_struct.png android_resource_bundle_content.png已经安装的应用
获取Resource
对于已经安装的应用, 获取
Resource
的方法很简单, 只要获取到Context
就可以获取对应环境下的Resource
了, 其中有一个方法Context.createPackageContext(String packageName, int flags)
可以根据包名
获取已经安装应用的Context
.
首先我们建一个Bean来存储已经加载的资源
public class LoadedResource {
public Resources resources;
public String packageName;
public ClassLoader classLoader;
}
然后我们就可以写加载的方法
/**
* 获取已安装应用资源
*
* @param packageName
*/
public LoadedResource getInstalledResource(String packageName) {
LoadedResource resource = mResources.get(packageName); // 先从缓存中取, 没有就去加载
if (resource == null) {
try {
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;
}
至此, 我们就能获取到了
Resource
获取资源ID
根据上面的思路, 我们使用反射区获取. 大概看一下
R文件的结构
package com.example.resourcebundle;
public final class R {
public static final class attr {
}
public static final class drawable {
public static final int image=0x7f020000;
public static final int image1=0x7f020001;
}
public static final class mipmap {
public static final int ic_launcher=0x7f030000;
}
public static final class string {
public static final int app_name=0x7f040000;
}
...
}
对应的资源类型都有一个静态内部类, 那么我们就可以使用反射无获取对应的数值
/**
* 获取资源ID
*
* @param packageName 包名
* @param type 对应的资源类型, drawable mipmap等
* @param fieldName
* @return
*/
public int getResourceID(String packageName, String type, String fieldName) {
int resID = 0;
LoadedResource installedResource = getInstalledResource(packageName); // 获取已安装APK的资源
if (installedResource != null) {
String rClassName = packageName + ".R$" + type; // 根据匿名内部类的命名, 拼写出R文件的包名+类名
try {
Class cls = installedResource.classLoader.loadClass(rClassName); // 加载R文件
resID = (Integer) cls.getField(fieldName).get(null); // 反射获取R文件对应资源名的ID
} catch (Exception e) {
e.printStackTrace();
}
} else {
Log.w(TAG, "resource is null:" + packageName);
}
return resID;
}
现在我们加载已经安装APK的资源的编码就已经完成.
调用
getDrawable("com.example.resourcebundle", "image1")
未安装的应用
我们先看一下getDrawable
方法是怎么去获取资源的
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
return impl.loadDrawable(this, value, id, theme, true);
} finally {
releaseTempTypedValue(value);
}
}
上面代码我们可以看到, 资源其实是通过impl代理去拿到的, 继续...
void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
if (found) {
return;
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}
然后再通过assets去代理获取, 继续看看assets从哪里设置的
public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
@Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
mAssets = assets;
mMetrics.setToDefaults();
mDisplayAdjustments = displayAdjustments;
updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
mAssets.ensureStringBlocks();
}
再寻找ResourcesImpl的构造函数从哪里调用
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
最后, 我们回到Resource的构造函数中, 也就是说正确调用Resources的构造函数, 那么我们就能构造出正确的Resource
但是, 如何得到一个AssetManager呢? 大家请参考文章Android应用程序资源管理器(Asset Manager)的创建过程分析
, 里面说明了AssetManager的加载原理和过程. 我们按部就班反射调用public final int addAssetPath(String path)
方法去添加资源文件路径.
那么我们现在就可以编码了
/**
* 加载未安装应用资源包
*
* @param resourcePath
* @return
*/
public LoadedResource loadResource(String resourcePath) {
LoadedResource loadResource = null;
PackageInfo info = queryPackageInfo(resourcePath); // 获取未安装APK的PackageInfo
if (info != null) { // 获取成功
loadResource = mRescources.get(info.packageName); // 先从缓存中取, 存在则直接返回, 不重复添加. 否则就搜索添加
if (loadResource == null) {
try {
AssetManager assetManager = AssetManager.class.newInstance(); // 创建AssetManager实例
Class cls = AssetManager.class;
Method method = cls.getMethod("addAssetPath", String.class);
method.invoke(assetManager, resourcePath); // 反射设置资源加载路径
Resources resources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(),
mContext.getResources().getConfiguration()); // 构造出正确的Resource
loadResource = new LoadedResource();
loadResource.resources = resources;
loadResource.packageName = info.packageName;
loadResource.classLoader = new DexClassLoader(resourcePath, mDexDir, null,
mContext.getClassLoader()); // 设置正确的类加载器, 因为需要去加载R文件
mRescources.put(info.packageName, loadResource); // 缓存
Log.w(TAG, "build resource:" + resourcePath);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Log.w(TAG, "load resource:" + resourcePath);
return loadResource;
}
/**
* 获取未安装应用PackageInfo
*
* @param resourcePath
* @return
*/
private PackageInfo queryPackageInfo(String resourcePath) {
return mContext.getPackageManager().getPackageArchiveInfo(resourcePath, PackageManager.GET_ACTIVITIES);
}
既然我们现在已经获取了Resource, 那么下面获取资源文件就与上面是一样的
LoadedResource loadResource = loadResource("/storage/sdcard0/bundle.apk");
Drawable drawable = getDrawable(loadResource.packageName, "image");
最后代码
LoadedResource.java
package com.example.host.res;import android.content.res.Resources;public class LoadedResource { public Resources resources; public String packageName; public ClassLoader classLoader;}
ResourceManager.java
package com.example.host.res;
import java.io.File;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.util.Log;
import dalvik.system.DexClassLoader;
public class ResourceManager {
private static final String TAG = "ResourceManager";
private ResourceManager() {
}
public static void init(Context context) {
UnInstalled.sManager.init(context);
Installed.sManager.init(context);
}
public static UnInstalled unInstalled() {
return UnInstalled.sManager;
}
public static Installed installed() {
return Installed.sManager;
}
/**
* 针对于未安装应用
*/
public static class UnInstalled {
static final UnInstalled sManager = new UnInstalled();
private Context mContext;
private Map<String, LoadedResource> mRescources = new HashMap<String, LoadedResource>();
private String mDexDir;
private UnInstalled() {
}
/**
* 初始化
*
* @param context
*/
public void init(Context context) {
mContext = context.getApplicationContext();
File dexDir = mContext.getDir("dex", Context.MODE_PRIVATE);
if (!dexDir.exists()) {
dexDir.mkdir();
}
mDexDir = dexDir.getAbsolutePath();
}
/**
* 获取未安装应用资源的ID
*
* @param packageName
* @param fieldName
* @return
*/
public int getResourceID(String packageName, String type, String fieldName) {
int resID = 0;
LoadedResource recource = getUnInstalledRecource(packageName);
String rClassName = packageName + ".R$" + type;
Log.w(TAG, "resource class:" + rClassName + ",fieldName:" + fieldName);
try {
Class cls = recource.classLoader.loadClass(rClassName);
resID = (Integer) cls.getField(fieldName).get(null);
} catch (Exception e) {
e.printStackTrace();
}
return resID;
}
/**
* 获取未安装应用Drawable
*
* @param packageName
* @param fieldName
* @return
*/
public Drawable getDrawable(String packageName, String fieldName) {
Drawable drawable = null;
int resourceID = getResourceID(packageName, "drawable", fieldName);
LoadedResource recource = getUnInstalledRecource(packageName);
if (recource != null) {
drawable = recource.resources.getDrawable(resourceID);
}
return drawable;
}
/**
* 加载未安装应用资源包
*
* @param resourcePath
* @return
*/
public LoadedResource loadResource(String resourcePath) {
LoadedResource loadResource = null;
PackageInfo info = queryPackageInfo(resourcePath); // 获取未安装APK的PackageInfo
if (info != null) { // 获取成功
loadResource = mRescources.get(info.packageName); // 先从缓存中取, 存在则直接返回, 不重复添加. 否则就搜索添加
if (loadResource == null) {
try {
AssetManager assetManager = AssetManager.class.newInstance(); // 创建AssetManager实例
Class cls = AssetManager.class;
Method method = cls.getMethod("addAssetPath", String.class);
method.invoke(assetManager, resourcePath); // 反射设置资源加载路径
Resources resources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(),
mContext.getResources().getConfiguration()); // 构造出正确的Resource
loadResource = new LoadedResource();
loadResource.resources = resources;
loadResource.packageName = info.packageName;
loadResource.classLoader = new DexClassLoader(resourcePath, mDexDir, null,
mContext.getClassLoader()); // 设置正确的类加载器, 因为需要去加载R文件
mRescources.put(info.packageName, loadResource); // 缓存
Log.w(TAG, "build resource:" + resourcePath);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Log.w(TAG, "load resource:" + resourcePath);
return loadResource;
}
/**
* 获取未安装应用PackageInfo
*
* @param resourcePath
* @return
*/
private PackageInfo queryPackageInfo(String resourcePath) {
return mContext.getPackageManager().getPackageArchiveInfo(resourcePath, PackageManager.GET_ACTIVITIES);
}
/**
* 获取未安装应用LoadResource
*
* @param packageName
* @return
*/
public LoadedResource getUnInstalledRecource(String packageName) {
LoadedResource loadResource = mRescources.get(packageName);
if (loadResource == null) {
Log.w(TAG, "resource " + packageName + " not founded");
}
return loadResource;
}
}
/**
* 针对于已安装应用
*/
public static class Installed {
static final Installed sManager = new Installed();
private Context mContext;
private Map<String, LoadedResource> mResources = new HashMap<String, LoadedResource>();
private Installed() {
}
/**
* 初始化
*
* @param context
*/
public void init(Context context) {
mContext = context.getApplicationContext();
}
/**
* 获取已安装应用资源
*
* @param packageName
*/
public LoadedResource getInstalledResource(String packageName) {
LoadedResource resource = mResources.get(packageName); // 先从缓存中取, 没有就去加载
if (resource == null) {
try {
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;
}
/**
* 获取资源ID
*
* @param packageName
* @param type
* @param fieldName
* @return
*/
public int getResourceID(String packageName, String type, String fieldName) {
int resID = 0;
LoadedResource installedResource = getInstalledResource(packageName); // 获取已安装APK的资源
if (installedResource != null) {
String rClassName = packageName + ".R$" + type; // 根据匿名内部类的命名, 拼写出R文件的包名+类名
try {
Class cls = installedResource.classLoader.loadClass(rClassName); // 加载R文件
resID = (Integer) cls.getField(fieldName).get(null); // 反射获取R文件对应资源名的ID
} catch (Exception e) {
e.printStackTrace();
}
} else {
Log.w(TAG, "resource is null:" + packageName);
}
return resID;
}
/**
* 获取已加载应用Drawable
*
* @param packageName
* @param fieldName
* @return
*/
public Drawable getDrawable(String packageName, String fieldName) {
Drawable drawable = null;
int resourceID = getResourceID(packageName, "drawable", fieldName);
LoadedResource installedResource = getInstalledResource(packageName);
if (installedResource != null) {
drawable = installedResource.resources.getDrawable(resourceID);
}
return drawable;
}
}
}
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="loadInstalledBundle"
android:text="加载已安装APK资源" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="loadUninstalledBundle"
android:text="加载未安装APK资源" />
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside" />
</LinearLayout>
MainActivity.java
package com.example.host;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import com.example.host.act.ActivityManager;
import com.example.host.res.LoadedResource;
import com.example.host.res.ResourceManager;
public class MainActivity extends Activity {
ImageView imageView;
ActivityManager mPluginManager = ActivityManager.getInstance();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ResourceManager.init(this);
mPluginManager.init(this);
imageView = (ImageView) findViewById(R.id.imageView);
}
/**
* 加载已安装APK资源
*
* @param v
*/
public void loadInstalledBundle(View v) {
Drawable drawable = ResourceManager.installed().getDrawable("com.example.resourcebundle", "image1");
if (drawable != null) {
imageView.setImageDrawable(drawable);
}
}
/**
* 加载未安装APK资源
*
* @param v
*/
public void loadUninstalledBundle(View v) {
LoadedResource loadResource = ResourceManager.unInstalled().loadResource("/storage/sdcard0/bundle.apk");
Drawable drawable = ResourceManager.unInstalled().getDrawable(loadResource.packageName, "image");
if (drawable != null) {
imageView.setImageDrawable(drawable);
}
}
}
测试加载安装APK资源就安装ResourceBundle应用, 测试加载未安装APK资源就把ResourceBundle应用放到指定位置
其中ResourceBundle只要包含image和image1的两个drawable就可以了
网友评论