如何获取另一个apk的资源?在聊这个问题前,先来看看我们在程序中调用getResources()方法,为何获取的是自己apk中的资源文件。
追踪程序中getResources()方法,首先我们找到ContxtThemeWrapper.java中的如下代码:
@Override
public Resources getResources() {
if (mResources != null) {
return mResources;
}
if (mOverrideConfiguration == null) {
mResources = super.getResources();
return mResources;
} else {
Context resc = createConfigurationContext(mOverrideConfiguration);
mResources = resc.getResources();
return mResources;
}
}
我们看到mResources是从resc对象中对象得到的,我们跟进createConfigurationContext方法,最终会发现这个方法是在ContextImpl.java这个类中实现的,ContextImpl.java继承自Context.java,当然我们从类名上就可以看出,下面是ContextImpl.java中对于createConfigurationContext方法的具体实现:
@Override
public Context createConfigurationContext(Configuration overrideConfiguration) {
if (overrideConfiguration == null) {
throw new IllegalArgumentException("overrideConfiguration must not be null");
}
ContextImpl c = new ContextImpl();
c.init(mPackageInfo, null, mMainThread);
c.mResources = mResourcesManager.getTopLevelResources(mPackageInfo.getResDir(),
getDisplayId(), overrideConfiguration, mResources.getCompatibilityInfo(),
mActivityToken);
return c;
}
我们关注第8行c.init方法,这个方法调用的是下面重载的init方法。
final void init(LoadedApk packageInfo, IBinder activityToken, ActivityThread mainThread,
Resources container, String basePackageName, UserHandle user) {
...
if (mResources != null &&
((compatInfo != null && compatInfo.applicationScale !=
mResources.getCompatibilityInfo().applicationScale)
|| activityToken != null)) {
if (DEBUG) {
Log.d(TAG, "loaded context has different scaling. Using container's" +
" compatiblity info:" + container.getDisplayMetrics());
}
if (compatInfo == null) {
compatInfo = packageInfo.getCompatibilityInfo();
}
mDisplayAdjustments.setCompatibilityInfo(compatInfo);
mDisplayAdjustments.setActivityToken(activityToken);
mResources = mResourcesManager.getTopLevelResources(mPackageInfo.getResDir(),
Display.DEFAULT_DISPLAY, null, compatInfo, activityToken);
} else {
mDisplayAdjustments.setCompatibilityInfo(packageInfo.getCompatibilityInfo());
mDisplayAdjustments.setActivityToken(activityToken);
}
...
}
我们看到其中的mResource成员变量就是资源类的一个声明引用,它是由mResourcesManager的getTopLevelResources方法得到,并且我们回看createConfigurationContext方法c.init()方法后面一步仍是调用了getTopLevelResources, 继续跟进getTopLevelResources方法,注意,传个这个方法的第一个参数的值是mPackageInfo.getResDir(),这个值就是我们自己apk的的路径。
public Resources getTopLevelResources(String resDir, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
...
Resources r;
synchronized (this) {
// Resources is app scale dependent.
if (false) {
Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
}
WeakReference<Resources> wr = mActiveResources.get(key);
r = wr != null ? wr.get() : null;
//if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
if (r != null && r.getAssets().isUpToDate()) {
if (false) {
Slog.w(TAG, "Returning cached resources " + r + " " + resDir
+ ": appScale=" + r.getCompatibilityInfo().applicationScale);
}
return r;
}
}
//if (r != null) {
// Slog.w(TAG, "Throwing away out-of-date resources!!!! "
// + r + " " + resDir);
//}
AssetManager assets = new AssetManager();
if (assets.addAssetPath(resDir) == 0) {
return null;
}
...
r = new Resources(assets, dm, config, compatInfo, token);
...
}
这里使用了弱引用的技术,如果r在缓存中存在就从wr(WeakReference)中获取然后直接返回,不存在就调用new Resources(assets, dm, config, compatInfo, token);方法新建一个Resouces对象,在构造方法中传入了一个资源对象,我们主要关注一下获取资源的方法 很明显就是assets.addAssetPath(resDir) 这个了。
到这里就可以知道为什么用getResources()获取的是自己app的资源文件,因为这里传的是packageInfo.getResDir(),这是我们自己apk的的路径。 所以要获取别的apk的资源 ,我们可以在我们的app里面调用这个方法传入想要获取的apk路径,就可以获取别的apk资源。
但是想法虽好,现实是残酷的,我们发现addAssetPath方法是被加了@hide的,也就是我们不能直接在程序中使用。这时我们又想到了万能的反射技术,下面是具体代码:
String dexpath= "";
try {
//所需的资源的apk的APK路径
dexpath = getPackageManager().getApplicationInfo("com.chm.main", 0).sourceDir;
Toast.makeText(this, dexpath, Toast.LENGTH_SHORT).show();
} catch (PackageManager.NameNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
loadRes(dexpath);
loadRes方法:
private void loadRes(String path) {
try {
am = AssetManager.class.newInstance();
Method addAssPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssPath.invoke(am, path);
} catch (Exception e) {
e.printStackTrace();
}
rs = new Resources(am, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
}
然后可以像下面这样给一个ImageView设置背景图标
iv.setImageDrawable(rs.getDrawable(R.drawable.message_pay));
这里有一个问题,如果你是想获取另一个apk中的message_pay图片,而这个方法中这张message_pay图片肯定是本apk中的,那么它们分别在R类中随机生成的对应整数很可能是不一致的。所以大多数情况下,这并不能获取我们真正想要的资源文件。甚至会抛出异常,如果找不到图片。
看到这里大家是不是很郁闷,那么我们再来看看另一种获取其他apk资源的方式。
也是用到了反射,我们直接看代码:
private void setImage(String dexpath){
DexClassLoader loader = new DexClassLoader(dexpath, getApplicationInfo().dataDir, null, this.getClass().getClassLoader());
try {
Class<?> clazz = loader.loadClass("com.chm.main.HellogpsActivity");
Method getImageId = clazz.getMethod("getImageId");
//调用静态方法
// int ic_launcher = (Integer) getImageId.invoke(clazz);
int ic_launcher = (int) getImageId.invoke(clazz.newInstance());
iv.setImageDrawable(getResourcs().getDrawable(ic_launcher));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
这里我们通过DexClassLoader类加载器获取到了另一个apk的Activity类,然后调用了该Activity中的 getImageId方法。这种方式可以返回我们真正想要的另一个apk中的资源,但是那也必须让那个apk给我们提供相关的方法返回资源对应的随机数,我们通过这个随机数才能拿到这个资源。
最后,总的来说我们想完全准确的拿到与你毫无关系的apk中资源是做不到的,要不然岂不是天下大乱,但是如果我们做插件这样的东西到是可以用用。
欢迎关注公众号。