Android动态加载学习总结(二):资源访问

参考资料:《Android开发艺术探索 --任玉刚》 Android中插件开发篇之----应用换肤原理解析

前言:动态加载要解决的三个问题,分别是资源访问,Activity生命周期管理,类加载器的管理。前面一片文章总结了类加载器的学习,里面介绍了一些东西,对于管理类加载器还没有涉及。这篇是资源访问相关的学习,里面有用到类加载器文章中的PathClassLoader,如果对此没有什么了解的话,可以先去看看介绍Android动态加载学习总结(一):类加载器,本篇博文的Demo来自于博文 Android中插件开发篇之----应用换肤原理解析,对于初步接触,为了更好的学习,我简略了一些内容。并且由于DexClassLoader的一些问题,我将博主的类加载方式更改成了PathClassLoader,既然是PathClassLoader,我们知道,这个类加载器只能加载dex文件,和已安装的apk文件,所以本篇博文的demo是访问已安装apk中的资源。

一、资源访问的问题

动态加载一个插件,如何访问它的资源?在我们宿主程序中,我们通过R文件访问资源,但是去访问插件的资源,明显是行不通的,我们在宿主程序中并没有插件的资源。如果只是去解决资源访问的问题的话,我们的确有方法,比如提前在宿主程序中预置一份,那么我们就需要在一个插件发布的时候将资源复制到宿主程序。能解决资源访问吗?肯定可以,但是我们为什么要有插件化技术(动态加载)?为了减小宿主程序apk大小,为了降低宿主程序的更新频率,那么去复制到宿主程序明显违背了这项技术最初的目的。

那么我们的解决方案如下:
Context中有两个与资源有关的抽象方法:

public abstract AssetManager getAssets();
public abstract Resources getResources();

我们需要实现这两个方法,实现方式如下:

protected void loadResources(String dexPath) {
//关于/assets目录下的文件,该目录下的文件不生成ID,如果我们要使用插件中该目录下的文件,我们需要指定文件的路径和文件名
 try { 
AssetManager assetManager=AssetManager.class.newInstance();
//我们通过调用AssetManager中的addAssetPath方法,可以将一个apk中资源加载到Resources对象中,而addAssetPath是隐藏API,所以通过反射调用 
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
//我们将APK的路径传给这个方法,资源便加载到AssetManager中 

addAssetPath.invoke(assetManager, dexPath); 
mAssetManager = assetManager; 
} catch (Exception e) { 
e.printStackTrace(); 
}  
Resources superRes = super.getResources(); superRes.getDisplayMetrics(); 
superRes.getConfiguration();
//通过AssetManger创建一个新的Resources对象,通过这个对象去访问插件资源 
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration()); 
mTheme = mResources.newTheme(); 
mTheme.setTo(super.getTheme()); 
} 
@Override 
public AssetManager getAssets() { 
return mAssetManager == null ? super.getAssets() : mAssetManager; 
} 
@Override 
public Resources getResources() { 
return mResources == null ? super.getResources() : mResources; 
} 
@Override public Resources.Theme getTheme() 
{ return mTheme == null ? super.getTheme() : mTheme; 
}
}

二、插件的设计

我们已经知道了解决方案,那么开始插件设计,新建一个工程,命名为ResourcesLoaderApk1。

  • MainActivity
public class MainActivity extends Activity { 
@Override 
protected void onCreate(Bundle savedInstanceState) { 
super.onCreate(savedInstanceState); 
setContentView(R.layout.activity_main);
 }
}
  • 主活动布局activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
xmlns:tools="http://schemas.android.com/tools" 
android:layout_width="match_parent" 
android:layout_height="match_parent" 
tools:context="com.example.gao.resourceloaderapk1.MainActivity" 
android:background="#998877" >
</RelativeLayout>
  • 类UIUtil
public class UIUtil {  
public static String getTextString(Context ctx){ 
//在宿主程序中通过反射调用该方法得到本插件程序中的strings.xml中定义的app_name资源 
return ctx.getResources().getString(R.string.app_name);
 }  
public static Drawable getImageDrawable(Context ctx){ 
//在宿主程序中通过反射调用该方法得到本插件程序中的icon图片资源 
return ctx.getResources().getDrawable(R.drawable.icon);
 }
}
  • AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
package="com.example.gao.resourceloaderapk1"> 
<application android:allowBackup="true" 
android:label="@string/app_name" 
android:icon="@mipmap/ic_launcher" 
android:theme="@style/AppTheme"> 
<activity 
android:name=".MainActivity"> 
<intent-filter> 
<action android:name="android.intent.action.MAIN"/> 
<category android:name="android.intent.category.LAUNCHER"/> 
</intent-filter> 
</activity> 
</application>
</manifest>

OK,插件设计完成,在模拟器中运行,也就相当于安装到了模拟器中,我们在Android动态加载学习总结(一):类加载器中已经知道,PathClassLoader可以加载dex文件和已安装的apk文件(其实是因为已安装的在cache中有缓存的dex文件),我们进入adb shell ,看看生成的apk的名字是什么(如果多次运行的话,这个名字会变的,建议修改的话,去看看名字变没有,我们要根据这个名字在宿主程序中进行加载)

  • 进入adb shell,并进入data/app目录
    这里写图片描述
  • ls查看这个目录下的所有apk,并找到我们插件程序的apk名字
    这里写图片描述

    也就是说我们宿主程序中PathClassLoader要加载的apk的路径是"/data/app/com.example.gao.resourceloaderapk1-1.apk"这个在下面的宿主程序MainActivity的类加载部分中会用到,注意一下。

三、宿主程序的设计

  • BaseActivity(含有资源访问的解决方案,和上面的代码一样)
public class BaseActivity extends Activity { 
protected AssetManager mAssetManager;
//资源管理器 
protected Resources mResources;
//资源 protected Resources.Theme mTheme;
//主题 
@Override 
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 } 
protected void loadResources(String dexPath) {
 try { 
AssetManager assetManager = AssetManager.class.newInstance(); 
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); 
addAssetPath.invoke(assetManager, dexPath); 
mAssetManager = assetManager; 
} catch (Exception e) { 
e.printStackTrace(); 
} 
Resources superRes = super.getResources(); 
superRes.getDisplayMetrics(); 
superRes.getConfiguration(); 
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration()); 
mTheme = mResources.newTheme(); 
mTheme.setTo(super.getTheme()); 
} 
@Override 
public AssetManager getAssets() { 
return mAssetManager == null ? super.getAssets() : mAssetManager; } 
@Override 
public Resources getResources() { 
return mResources == null ? super.getResources() : mResources; 
} 
@Override 
public Resources.Theme getTheme() { 
return mTheme == null ? super.getTheme() : mTheme;
 }
}
  • MainActivity(其中PathClassLoader加载的代码和第一篇博客差不多,更改了需要加载的apk路径还有intent,注意AndroidManifext.xml中的action需要跟intent一致)
public class MainActivity extends BaseActivity { 
/** 需要替换主题的控件 
*TextView,ImageView 
*/ 
private TextView textV; 
private ImageView imgV; 
// 更换控件的按钮 
private Button btnChange; 
//类加载器 
protected PathClassLoader pc1 = null; 
@Override 
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 textV = (TextView)findViewById(R.id.text); 
imgV = (ImageView)findViewById(R.id.imageview); 
btnChange = (Button)findViewById(R.id.btn1); 
//通过点击按钮,更新TextView和ImageView两个控件 
btnChange.setOnClickListener(new View.OnClickListener(){ 
@Override 
public void onClick(View arg0) { 
/**使用PathClassLoader方法加载类*/ 
/**创建一个意图,用来找到指定的apk:这里的"com.example.gao.resourceloaderapk1"是指定apk中在AndroidMainfest.xml文件中定义的<action name="com.example.gao.resourceloaderapk1"/> */
Intent intent = new Intent("com.example.gao.resourceloaderapk1", null); 
//获得包管理器 
PackageManager pm = getPackageManager();
List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0);
//获得指定的activity的信息 
ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;  
//获得apk的目录,这个目录在第二部分插件程序的设计末尾已得到 
final String apkPath = "/data/app/com.example.gao.resourceloaderapk1-1.apk"; 
//native代码的目录 
String libPath = actInfo.applicationInfo.nativeLibraryDir; 
//创建类加载器,把dex加载到虚拟机中 
//第一个参数:是指定apk安装的路径,这个路径要注意只能是通过actInfo.applicationInfo.sourceDir来获取 
//第二个参数:是C/C++依赖的本地库文件目录,可以为null 
//第三个参数:是上一级的类加载器 pc1 = new PathClassLoader(apkPath,libPath, MainActivity.this.getClassLoader()); //调用父类的loadResources()方法 
loadResources(apkPath); 
setContent(); 
} 
});
}
private void setContent(){ 
try{
 Class clazz = pc1.loadClass("com.example.gao.resourceloaderapk1.UIUtil"); 
/** * 通过反射调用插件中UIUtil类中的getTextString方法 */ 
Method method = clazz.getMethod("getTextString", Context.class); 
String str = (String)method.invoke(null, this); 
//更改宿主程序的TextView控件内容 textV.setText(str);
 /**通过反射调用插件中UIUtil类中的getImageDrawable方法 */ 
method = clazz.getMethod("getImageDrawable", Context.class); 
Drawable drawable = (Drawable)method.invoke(null, this); 
//更改宿主程序的ImageView内容 imgV.setImageDrawable(drawable); }catch(Exception e){ 
e.printStackTrace(); 
} 
}
}
  • activity_main.xml
<RelativeLayout 
xmlns:android="http://schemas.android.com/apk/res/android" 
xmlns:tools="http://schemas.android.com/tools" 
android:layout_width="match_parent" 
android:layout_height="match_parent"
tools:context="com.example.resourceloader.MainActivity" > 
<LinearLayout  
android:layout_width="match_parent" 
android:layout_height="wrap_content" 
android:orientation="vertical">  
//更改TextView和ImageView的按钮 
<Button  android:id="@+id/btn1" 
android:layout_width="wrap_content" 
android:layout_height="wrap_content" 
android:layout_marginRight="10dp"
 android:text="主题1"/> 
//要更改的TextView,原显示“demo” 
<TextView  android:id="@+id/text" 
android:layout_width="wrap_content" 
android:layout_height="wrap_content" 
android:text="demo"/> 
//要更改的ImageView,原显示"ic_launcher图片" 
<ImageView  
android:id="@+id/imageview" 
android:layout_width="wrap_content"
 android:layout_height="wrap_content" 
android:layout_marginTop="20dp" 
android:src="@drawable/ic_launcher"/>  
</LinearLayout>
</RelativeLayout>
  • AndroidManifest.xml
<manifest 
xmlns:android="http://schemas.android.com/apk/res/android" 
package="com.example.gao.resourceloading"> 
<application 
android:allowBackup="true" 
android:label="@string/app_name" 
android:icon="@mipmap/ic_launcher" 
android:theme="@style/AppTheme"> 
<activity android:name=".MainActivity"> 
<intent-filter> 
<action android:name="android.intent.action.MAIN"/> 
//注意这个action,MainActivity中Intent那行代码与这个action保持一致 
<action android:name="com.example.gao.resourceloaderapk1"/>
 <category android:name="android.intent.category.LAUNCHER"/>
 </intent-filter> 
</activity> 
</application>
</manifest>

运行宿主程序,运行结果如下:
这里写图片描述

我们点击一下主题一的按钮,结果如下:
这里写图片描述

结果显示我们成功访问了插件程序的资源,TextView的内容由demo更改为了插件程序的app_name,ImageView的图片由宿主程序的ic_launcher图片改为了插件程序的icon图片。在这之中,loadResources(String dexPath)方法起到了访问插件程序资源的作用,如果我们把MainActivity中按钮点击的代码中的loadResources(apkPath)注释掉呢?相当于我们只是通过动态加载和反射调用了插件程序的UIUtil类的以下两个方法
public static String getTextString(Context ctx){ 
return ctx.getResources().getString(R.string.app_name); 
}  
public static Drawable getImageDrawable(Context ctx){ 
return ctx.getResources().getDrawable(R.drawable.icon);
 }

我们并没有访问到插件程序的资源。那肯定用宿主程序的资源了,那么app_name,我们宿主程序也有,但是icon这个图片我们宿主程序并没有,所以把loadResources(apkPath)方法注释掉后,我们点击按钮,只会更改TextView这个内容,改成我们宿主程序的app_name这个字符串的内容,即ResourcesLoading,而图片并不会变。结果如下:
这里写图片描述

四、总结:

在本文中通过PathClassLoader去动态加载已安装的apk,有一点限制是需要安装插件后才可以加载这个apk,当然,我们也可以通过DexClassLoader去加载这个apk,这样我们可以在插件未安装的情况下就去访问它的资源,那么根据这个,我们做一些更换主题,皮肤等等就有了解决方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容