实现方式是使用 Context.createPackageContext(String packageName, int flags) 方法来加载一个apk包的Context,有了Context后就可以得到Resources和LayoutInflater, 再通过反射机制可以得到资源ID。
把加载资源apk包的工作封装到ResLoader中, 代码如下:
package com.example.resapktest;
import java.lang.reflect.Field;
import java.util.HashMap;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
public class ResLoader
{
private static final String TAG = "ResLoader";
private Context mContext = null;
private Context mResApkContext = null;
private LayoutInflater mResApkInflater = null;
private String mResApkPackage = null;
private ResIDMap mResIDMap = null;
private Resources mResApkResources = null;
public ResLoader(Context context)
{
this.mContext = context;
}
public View loadLayout(String resource)
{
if (null != resource)
{
int id = mResIDMap.mSkinIDMap.get(resource);
return mResApkInflater.inflate(id, null);
}
return null;
}
public View loadLayout(int id)
{
if(id <= 0) return null;
return mResApkInflater.inflate(id, null);
}
public int findIDByName(String resource)
{
if (null == resource)
{
return -1;
}
return mResIDMap.mSkinIDMap.get(resource);
}
public String getString(String resource)
{
if (null == resource)
{
return null;
}
int id = mResIDMap.mSkinIDMap.get(resource);
return mResApkContext.getString(id);
}
public float getDimension(String resource)
{
if (null == resource)
{
return 0;
}
int id = mResIDMap.mSkinIDMap.get(resource);
return mResApkResources.getDimension(id);
}
public Drawable getDrawable(String resource)
{
if (null == resource)
{
return null;
}
int id = mResIDMap.mSkinIDMap.get(resource);
return mResApkResources.getDrawable(id);
}
public Context getSkinContext()
{
return mResApkContext;
}
public ResIDMap getResIDMap()
{
return mResIDMap;
}
/**
*
* Title: loadResApk
* @Description:
* @param resApk the res package name,such as "com.example.resapk"
*/
public boolean loadResApk(String resApk)
{
if (mResApkPackage != null
&& 0 == resApk.compareToIgnoreCase(mResApkPackage)) {
return false;
}
if(resApk == null) return false;
try {
// 创建资源apk包的Context
mResApkContext = mContext.createPackageContext(resApk,
Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
// 获取资源apk包的Resources
mResApkResources = mResApkContext.getResources();
// 获取资源apk包的LayoutInflater
mResApkInflater = (LayoutInflater) mResApkContext.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
mResApkPackage = resApk;
// 使用反射机制把资源apk包中的资源名及id获取出来
mResIDMap = new ResIDMap(mResApkContext, mResApkPackage);
}
catch (NameNotFoundException e)
{
e.printStackTrace();
return false;
}
return true;
}
public class ResIDMap
{
public HashMap<String, Integer> mSkinIDMap = null;
@SuppressWarnings("unused")
private Context mContext = null;
public ResIDMap(Context context, String skinPackage)
{
mContext = context;
mSkinIDMap = getResIDMap(context, skinPackage);
}
/**
*
* Title: getResIDMap
* @Description:
* @param resApkContext the context of the resource apk,
* create by createPackageContext
* @param resApkPackagename the resource apk package name,
* such as com.example.resapk
* @return
*/
private HashMap<String, Integer> getResIDMap(Context resApkContext,
String resApkPackagename)
{
HashMap<String, Integer> mResIDMap = new HashMap<String, Integer>();
if (null == resApkContext || null == resApkPackagename)
{
return mResIDMap;
}
try
{
Class<?> RClass = resApkContext.getClassLoader().loadClass(
resApkPackagename+".R");
Class<?>[] cl = RClass.getClasses();
for (int i = 0; i < cl.length; i++)
{
Field field[] = cl[i].getFields();
for (int j = 0; j < field.length; j++)
{
if(field[j].getType().getCanonicalName().equals("int[]")){
Log.d(TAG,"continue");
continue;
}
mResIDMap.put(field[j].getName(), field[j].getInt(
field[j].getName()));
}
}
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
catch (IllegalArgumentException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
return mResIDMap;
}
}
}
在Activity中调用, 以下是MainAcitvity.java的代码:
package com.example.resapktest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
public class MainActivity extends Activity {
ResLoader mResLoader;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i("MainActivity", "ResApkTest");
//setContentView(R.layout.activity_main);
mResLoader = new ResLoader(getApplicationContext());
// 加载资源apk包
mResLoader.loadResApk("com.example.resapk");
// 使用字符串加载 Layout
View activityMain = mResLoader.loadLayout("activity_main");
// 使用ID 加载Layout
activityMain = mResLoader.loadLayout(com.example.resapk.R.layout.activity_main);
if(activityMain != null) {
setContentView(activityMain);
View view = activityMain.findViewById(
com.example.resapk.R.id.time_clock_2);
view.setAlpha(0.5f);
Clock_setTextColor(view, 0xff0000ff);
}
}
public void Clock_setTextColor(View view, int color) {
Class clockClass = view.getClass();
try {
Method method = clockClass.getMethod("setTextColor", int.class);
method.invoke(view, color);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
右击ResApkTest工程, 选择Build Path -> Link Source, 在弹框中选中ResApk包中的Gen目录,把这个目录命名为ResApk_Gen 。点击OK。
activityMain = mResLoader.loadLayout(com.example.resapk.R.layout.activity_main);
View view = activityMain.findViewById( com.example.resapk.R.id.time_clock_2);
项目开发工程中经常会用到自定义控件, 因为类加载器不同, 暂时没有在宿主工程中正常访问ResApk中加载出来的扩展控件的方法,原因在第六点解释,这里给出替代方案, 也是使用类的反射机制, 这里各个工程的依赖关系如下:
public void setTextColor(int color) {
mColor = color;
}
在ResApkTest中无法直接使用此方法,需要用反射机制来调用, 这里封装一个函数用来实现这个调用,代码如下:
public void Clock_setTextColor(View view, int color) {
Class clockClass = view.getClass();
try {
Method method = clockClass.getMethod("setTextColor", int.class);
method.invoke(view, color);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
那么调用方式就是:
if(activityMain != null) {
setContentView(activityMain);
View view = activityMain.findViewById(
com.example.resapk.R.id.time_clock_2);
view.setAlpha(0.5f);
Clock_setTextColor(view, 0xff0000ff);
}
如果有很多扩展方法, 那么这样调用起来会有些繁琐, 尽量把这部分调用封装起来,不要影响到客户端代码的编写。以下是我做的一些尝试,事实上,我是在尝试失败之后才使用反射机制来实现这个功能的。
想要正常调用扩展控件,那么ResApkTest应该要对ExtendView可见, 即以某种方式引用ExtendView.jar; 以下是我尝试的依赖关系:
修改MainActivity的onCreate函数为以下内容:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i("MainActivity", "ResApkTest");
//setContentView(R.layout.activity_main);
mResLoader = new ResLoader(getApplicationContext());
// 加载资源apk包
mResLoader.loadResApk("com.example.resapk");
// 使用字符串加载 Layout
View activityMain = mResLoader.loadLayout("activity_main");
// 使用ID 加载Layout
activityMain = mResLoader.loadLayout(com.example.resapk.R.layout.activity_main);
View view = null;
if(activityMain != null) {
setContentView(activityMain);
view = activityMain.findViewById(
com.example.resapk.R.id.time_clock_2);
view.setAlpha(0.5f);
Clock_setTextColor(view, 0xff0000ff);
}
ClassLoader loader = getClassLoader();
Log.i("Demo", "ResApkTest.apk 默认的类加载器 "+loader);
Log.i("Demo", "ResApkTest.apk 包中的Clock类加载器 "+Clock.class.getClassLoader());
Log.i("Demo", "ResApk.apk 包中的Clock类加载器 "+view.getClass().getClassLoader());
Log.i("Demo", "ResApkTest.apk 包中的 Clock.class hashcode "+Clock.class.hashCode());
Log.i("Demo", "ResApk.apk 包中的 Clock.class hashcode "+view.getClass().hashCode());
Log.i("Demo", "打印 ResApkTest 中类加载器的 parent");
ClassLoader resApkTestLoader = Clock.class.getClassLoader();
while (resApkTestLoader != null) {
Log.i("Demo", "ResApkTest parent Loader " + resApkTestLoader);
resApkTestLoader = resApkTestLoader.getParent();
}
Log.i("Demo", "打印 ResApk 中类加载器的 parent");
ClassLoader resApkLoader = view.getClass().getClassLoader();
while (resApkLoader != null) {
Log.i("Demo", "ResApk parent Loader " + resApkLoader);
resApkLoader = resApkLoader.getParent();
}
Log.i("Demo", "view = " + view);
Clock clock = (Clock)view;
}
运行MainActivity, 得到以下打印信息和异常:
01-01 00:28:33.429: I/Demo(2808): ResApkTest.apk 默认的类加载器 dalvik.system.PathClassLoader[/data/app/com.example.resapktest-1.apk]
01-01 00:28:33.429: I/Demo(2808): ResApkTest.apk 包中的Clock类加载器 dalvik.system.PathClassLoader[/data/app/com.example.resapktest-1.apk]
01-01 00:28:33.429: I/Demo(2808): ResApk.apk 包中的Clock类加载器 dalvik.system.PathClassLoader[/data/app/com.example.resapk-2.apk]
01-01 00:28:33.429: I/Demo(2808): ResApkTest.apk 包中的 Clock.class hashcode 1093584792
01-01 00:28:33.429: I/Demo(2808): ResApk.apk 包中的 Clock.class hashcode 1093481360
01-01 00:28:33.429: I/Demo(2808): 打印 ResApkTest 中类加载器的 parent
01-01 00:28:33.429: I/Demo(2808): ResApkTest parent Loader dalvik.system.PathClassLoader[/data/app/com.example.resapktest-1.apk]
01-01 00:28:33.429: I/Demo(2808): ResApkTest parent Loader java.lang.BootClassLoader@40c3c5c0
01-01 00:28:33.429: I/Demo(2808): 打印 ResApk 中类加载器的 parent
01-01 00:28:33.429: I/Demo(2808): ResApk parent Loader dalvik.system.PathClassLoader[/data/app/com.example.resapk-2.apk]
01-01 00:28:33.429: I/Demo(2808): ResApk parent Loader java.lang.BootClassLoader@40c3c5c0
01-01 00:28:33.429: I/Demo(2808): view = com.example.extendview.Clock@412d8700
01-01 00:28:33.429: D/AndroidRuntime(2808): Shutting down VM
01-01 00:28:33.429: W/dalvikvm(2808): threadid=1: thread exiting with uncaught exception (group=0x40c35300)
01-01 00:28:33.439: E/AndroidRuntime(2808): FATAL EXCEPTION: main
01-01 00:28:33.439: E/AndroidRuntime(2808): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.resapktest/com.example.resapktest.MainActivity}: java.lang.ClassCastException: com.example.extendview.Clock cannot be cast to com.example.extendview.Clock
开始很困惑, 为什么com.example.extendview.Clock cannot be cast to com.example.extendview.Clock ?明明是相同的类呀。后来寻根问底,发现是这的确是两个不同的类,它们的类对象的hash code 不一样:
01-01 00:28:33.429: I/Demo(2808): ResApk.apk 包中的 Clock.class hashcode 1093481360
01-01 00:28:33.429: I/Demo(2808): ResApk.apk 包中的Clock类加载器 dalvik.system.PathClassLoader[/data/app/com.example.resapk-2.apk]
Return a new Context object for the given application name. This Context is the same as what the named application gets when it is launched, containing the same resources and class loader. Each call to this method returns a new instance of a Context object; Context objects are not shared, however they share common state (Resources, ClassLoader, etc) so the Context instance itself is fairly lightweight.
也就是说Context.createPackageContext加载出来的Context还是跟说被加载的Application相关, 跟主调的Application不一样。
后来我尝试了很多方法来实现用ResApkTest中的类加载器来加载ResApk中的类, 发现都不能实现需要的功能, 其中还参考了这篇关于插件开发的博文:
http://blog.csdn.net/jiangwei0910410003/article/details/41384667
里面有涉及类加载器的说明, 讲得比较好, 读者可以去看看。
这个问题总结一下:
本文Demo 代码位于:
http://download.csdn.net/detail/romantic_energy/8793793
需要的朋友自己去下载。