第一步、先制做一个有我们需要的图片资源的APK

如下图,这里有个about_log.png,我们需要生成apk文件。

生成的apk文件如果你不到项目的文件夹里面去取apk,想通过命令放到手机里面去可以快速用下面命令

 

1)、在手机里面通过包名找到apk路径,一定不要忘记有 -f

- adb shell pm list package -f | grep com.example.testclassloader

得到如下结果

- package:/data/app/com.example.testclassloader-2/<span class="attribute">base.apk</span>=<span class="attribute-value">com</span>.example.testclassloader

2)、把base.apk拉到本地然后改名字,命令如下

- adb shell pull /data/app/com.example.testclassloader-2/base.apk  testClassLoader.apk

3)、把testClassLoader.apk放到手机里面去,命令如下

- adb shell push testClassLoader.apk  /sdcard/

4)、去手机文件管理器里面找看是否有testClassLoader.apk文件

 

第二步、获取为安装apk包名的信息(假设前提不知道)

我们可以通过这个方法得到

 public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags)

具体方法如下

- /**

- * 获取未安装apk的信息

- * @param context

- * @param apkPath apk文件的path

- * @return

- */

- private Map<span class="tag"><</span><span class="tag-name">String</span>,String<span class="tag">></span> getUninstallApkInfo(Context context, String apkPath) {

- Map <span class="attribute">hashMap</span> = <span class="attribute-value">new</span> HashMap<span class="tag"><</span><span class="tag-name">String</span>,String<span class="tag">></span>();

- PackageManager <span class="attribute">pm</span> = <span class="attribute-value">context</span>.getPackageManager();

- PackageInfo <span class="attribute">pkgInfo</span> = <span class="attribute-value">pm</span>.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);

- if (null != pkgInfo) {

- ApplicationInfo <span class="attribute">appInfo</span> = <span class="attribute-value">pkgInfo</span>.applicationInfo;

- String <span class="attribute">pkgName</span> = <span class="attribute-value">appInfo</span>.packageName;//包名

- hashMap.put(PKG_NAME, pkgName);

- } else {

- Log.d(TAG, &#8220;program don&#8217;t get apk package information&#8221;);

- }

- return hashMap;

- }

第三步、获取未安装apk(插件)的Resource

因为没有安装,所以不能得到context,所以我们需要未安装apk的Resource,我们可以通过反射来获取,代码如下

- /**

- * @param apkPath

- * @return 得到对应插件的Resource对象

- */

- private Resources getPluginResources(String apkPath) {

- try {

- AssetManager <span class="attribute">assetManager</span> = <span class="attribute-value">AssetManager</span>.class.newInstance();

- //反射调用方法addAssetPath(String path)

- Method <span class="attribute">addAssetPath</span> = <span class="attribute-value">assetManager</span>.getClass().getMethod(ADDSSETPATH, String.class);

- //将未安装的Apk文件的添加进AssetManager中,第二个参数是apk的路径

- addAssetPath.invoke(assetManager, apkPath);

- Resources <span class="attribute">superRes</span> = <span class="attribute-value">this</span>.getResources();

- Resources <span class="attribute">mResources</span> = <span class="attribute-value">new</span> Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());

- return mResources;

- } catch (Exception e) {

- e.printStackTrace();

- }

- return null;

- }

 

第四步、用DexClassLoader加载apk资源文件替换背景

如果你多DexClassLoader用法和原理不熟悉,可以参考我之前的博客
Android插件化开发之DexClassLoader动态加载dex、jar小Demo  [http://blog.csdn.net/u011068702/article/details/53263442](http://blog.csdn.net/u011068702/article/details/53263442)
Android插件化开发之动态加载基础之ClassLoader工作机制  [http://blog.csdn.net/u011068702/article/details/53248960](http://blog.csdn.net/u011068702/article/details/53248960)
代码如下:
  - /**
  
  - * 加载apk获得内部资源,并且替换背景
  
  - * @param apkDir apk目录
  
  - * @param apkName apk名字,带.apk
  
  - * @throws Exception
  
  - */
  
  - private void dynamicLoadApk(String apkPath, String apkPackageName) throws Exception {
  
  - //在应用安装目录下创建一个名为app_dex文件夹目录,如果已经存在则不创建,这个目录主要是最优化目录,用于缓存dex文件
  
  - File <span class="attribute">optimizedDirectoryFile</span> = <span class="attribute-value">getDir</span>(DEX, Context.MODE_PRIVATE);
  
  - //打印路径 理论上是/data/data/package/app_dex
  
  - Log.v(TAG, optimizedDirectoryFile.getPath().toString());
  
  - //构建DexClassLoader
  
  - DexClassLoader <span class="attribute">dexClassLoader</span> = <span class="attribute-value">new</span> DexClassLoader(apkPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
  
  - //通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源id
  
  - Class<span class="tag"><?</span><span class="tag">></span> <span class="attribute">clazz</span> = <span class="attribute-value">dexClassLoader</span>.loadClass(apkPackageName + DRAWABLE);
  
  - //得到名为about_log的这张图片字段,这个图片是为安装apk里面的图片
  
  - Field <span class="attribute">field</span> = <span class="attribute-value">clazz</span>.getDeclaredField(IMAGE_ID);
  
  - //得到图片id
  
  - int <span class="attribute">resId</span> = <span class="attribute-value">field</span>.getInt(R.id.class);
  
  - //得到插件apk中的Resource
  
  - Resources <span class="attribute">mResources</span> = <span class="attribute-value">getPluginResources</span>(apkPath);
  
  - if (mResources != null) {
  
  - //通过插件apk中的Resource得到resId对应的资源
  
  - Drawable <span class="attribute">btnDrawable</span> = <span class="attribute-value">mResources</span>.getDrawable(resId);
  
  - mLayout.setBackgroundDrawable(btnDrawable);
  
  - } else {
  
  - Log.d(TAG, &#8220;mResources is null&#8221;);
  
  - }
  
  - }

第五步、爆出所有代码(为了详细点)

  - package com.chenyu.dexclassloaderapk;
  
  - 
  - import java.io.File;
  
  - import java.lang.reflect.Field;
  
  - import java.lang.reflect.Method;
  
  - import java.util.HashMap;
  
  - import java.util.Map;
  
  - 
  - import android.content.Context;
  
  - import android.content.pm.ApplicationInfo;
  
  - 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.os.Bundle;
  
  - import android.os.Environment;
  
  - import android.support.v7.app.ActionBarActivity;
  
  - import android.util.Log;
  
  - import android.view.View;
  
  - import android.view.View.OnClickListener;
  
  - import android.widget.ImageView;
  
  - import android.widget.RelativeLayout;
  
  - import android.widget.TextView;
  
  - 
  - import com.example.dexclassloaderapk.R;
  
  - 
  - import dalvik.system.DexClassLoader;
  
  - 
  - public class MainActivity extends ActionBarActivity {
  
  - 
  - public static final String <span class="attribute">TAG</span> = <span class="attribute-value">&#8220;DexClassLoaderApk&#8221;</span>;
  
  - public static final String <span class="attribute">PKG_NAME</span> = <span class="attribute-value">&#8220;pkgName&#8221;</span>;
  
  - public static final String <span class="attribute">APK_PATH</span> = <span class="attribute-value">&#8220;testClassLoader.apk&#8221;</span>;
  
  - public static final String <span class="attribute">ADDSSETPATH</span> = <span class="attribute-value">&#8220;addAssetPath&#8221;</span>;
  
  - public static final String <span class="attribute">DEX</span> = <span class="attribute-value">&#8220;dex&#8221;</span>;
  
  - //这个IMAGE_ID是只我放入手机里面APK 在代码里面这个图片的ID,这里我们拿到之后,然后去替换北京图片
  
  - public static final String <span class="attribute">IMAGE_ID</span> = <span class="attribute-value">&#8220;about_log&#8221;</span>;
  
  - public static final String <span class="attribute">DRAWABLE</span> = <span class="attribute-value">&#8220;.R$drawable&#8221;</span>;
  
  - public TextView mTextView;
  
  - //背景的布局
  
  - public RelativeLayout mLayout;
  
  - public Map<span class="tag"><</span><span class="tag-name">String</span>, String<span class="tag">></span> apkInfo;
  
  - 
  - @Override
  
  - protected void onCreate(Bundle savedInstanceState) {
  
  - super.onCreate(savedInstanceState);
  
  - setContentView(R.layout.activity_main);
  
  - final String <span class="attribute">apkPath</span> = <span class="attribute-value">Environment</span>.getExternalStorageDirectory().toString() + File.separator + APK_PATH;
  
  - <span class="attribute">mTextView</span> = (TextView)findViewById(R.id.text);
  
  - <span class="attribute">mLayout</span> = (RelativeLayout)findViewById(R.id.re_Layout);
  
  - mTextView.setOnClickListener(new OnClickListener(){
  
  - @Override
  
  - public void onClick(View v) {
  
  - //一定要记得加上android.permission.READ_EXTERNAL_STORAGE权限,不然死活都拿不到数据
  
  - //我就换了一个这个错误,如果发现代码没问题,网上找也没问题,这个时候应该思考是不是没有加权限
  
  - <span class="attribute">apkInfo</span> = <span class="attribute-value">getUninstallApkInfo</span>(MainActivity.this, apkPath);
  
  - String <span class="attribute">packageName</span> = <span class="attribute-value">apkInfo</span>.get(PKG_NAME);
  
  - if (null != packageName) {
  
  - try {
  
  - <span class="tag"><</span><span class="tag-name">span</span> <span class="attribute">style</span>=<span class="attribute-value">&#8220;white-space:pre&#8221;</span><span class="tag">></span>  <span class="tag"></</span><span class="tag-name">span</span><span class="tag">></span>dynamicLoadApk(apkPath, packageName);
  
  - } catch (Exception e) {
  
  - e.printStackTrace();
  
  - Log.i(TAG, &#8220;change image fail&#8221;);
  
  - }
  
  - } else {
  
  - Log.i(TAG, &#8220;package is null&#8221;);
  
  - }
  
  - }
  
  - });
  
  - }
  
  - 
  - /**
  
  - * 获取未安装apk的信息
  
  - * @param context
  
  - * @param apkPath apk文件的path
  
  - * @return
  
  - */
  
  - private Map<span class="tag"><</span><span class="tag-name">String</span>,String<span class="tag">></span> getUninstallApkInfo(Context context, String apkPath) {
  
  - Map <span class="attribute">hashMap</span> = <span class="attribute-value">new</span> HashMap<span class="tag"><</span><span class="tag-name">String</span>,String<span class="tag">></span>();
  
  - PackageManager <span class="attribute">pm</span> = <span class="attribute-value">context</span>.getPackageManager();
  
  - PackageInfo <span class="attribute">pkgInfo</span> = <span class="attribute-value">pm</span>.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);
  
  - if (null != pkgInfo) {
  
  - ApplicationInfo <span class="attribute">appInfo</span> = <span class="attribute-value">pkgInfo</span>.applicationInfo;
  
  - String <span class="attribute">pkgName</span> = <span class="attribute-value">appInfo</span>.packageName;//包名
  
  - hashMap.put(PKG_NAME, pkgName);
  
  - } else {
  
  - Log.d(TAG, &#8220;program don&#8217;t get apk package information&#8221;);
  
  - }
  
  - return hashMap;
  
  - }
  
  - 
  - /**
  
  - * @param apkPath
  
  - * @return 得到对应插件的Resource对象
  
  - */
  
  - private Resources getPluginResources(String apkPath) {
  
  - try {
  
  - AssetManager <span class="attribute">assetManager</span> = <span class="attribute-value">AssetManager</span>.class.newInstance();
  
  - //反射调用方法addAssetPath(String path)
  
  - Method <span class="attribute">addAssetPath</span> = <span class="attribute-value">assetManager</span>.getClass().getMethod(ADDSSETPATH, String.class);
  
  - //将未安装的Apk文件的添加进AssetManager中,第二个参数是apk的路径
  
  - addAssetPath.invoke(assetManager, apkPath);
  
  - Resources <span class="attribute">superRes</span> = <span class="attribute-value">this</span>.getResources();
  
  - Resources <span class="attribute">mResources</span> = <span class="attribute-value">new</span> Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
  
  - return mResources;
  
  - } catch (Exception e) {
  
  - e.printStackTrace();
  
  - }
  
  - return null;
  
  - }
  
  - 
  - /**
  
  - * 加载apk获得内部资源,并且替换背景
  
  - * @param apkDir apk目录
  
  - * @param apkName apk名字,带.apk
  
  - * @throws Exception
  
  - */
  
  - private void dynamicLoadApk(String apkPath, String apkPackageName) throws Exception {
  
  - //在应用安装目录下创建一个名为app_dex文件夹目录,如果已经存在则不创建,这个目录主要是最优化目录,用于缓存dex文件
  
  - File <span class="attribute">optimizedDirectoryFile</span> = <span class="attribute-value">getDir</span>(DEX, Context.MODE_PRIVATE);
  
  - //打印路径 理论上是/data/data/package/app_dex
  
  - Log.v(TAG, optimizedDirectoryFile.getPath().toString());
  
  - //构建DexClassLoader
  
  - DexClassLoader <span class="attribute">dexClassLoader</span> = <span class="attribute-value">new</span> DexClassLoader(apkPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
  
  - //通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源id
  
  - Class<span class="tag"><?</span><span class="tag">></span> <span class="attribute">clazz</span> = <span class="attribute-value">dexClassLoader</span>.loadClass(apkPackageName + DRAWABLE);
  
  - //得到名为about_log的这张图片字段,这个图片是为安装apk里面的图片
  
  - Field <span class="attribute">field</span> = <span class="attribute-value">clazz</span>.getDeclaredField(IMAGE_ID);
  
  - //得到图片id
  
  - int <span class="attribute">resId</span> = <span class="attribute-value">field</span>.getInt(R.id.class);
  
  - //得到插件apk中的Resource
  
  - Resources <span class="attribute">mResources</span> = <span class="attribute-value">getPluginResources</span>(apkPath);
  
  - if (mResources != null) {
  
  - //通过插件apk中的Resource得到resId对应的资源
  
  - Drawable <span class="attribute">btnDrawable</span> = <span class="attribute-value">mResources</span>.getDrawable(resId);
  
  - mLayout.setBackgroundDrawable(btnDrawable);
  
  - } else {
  
  - Log.d(TAG, &#8220;mResources is null&#8221;);
  
  - }
  
  - }
  
  - }
点击TextView内容“换皮肤”来触发的,当初背景是设置的一个机器人。

第六步:运行项目爆结果照片

点击换皮护之前背景图片如下
![](http://img.blog.csdn.net/20161123212919989?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
点击换图片之后背景图片如下
![](http://img.blog.csdn.net/20161123213141068?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
ok,说明获取到了这种图片资源,换皮肤成功,这里只是代表换皮肤意思,效果比较丑,不要喷哈。

第七步、总结

这样做资源和宿主分离了,减轻了apk负担,同时也有解耦和作用,我们手机一些浏览器换模式(日和夜)、QQ换皮肤、表情包、线上下载线下维护、是项目更加灵活,可扩展性更好,同时也复习了DexClassLoader和反射相关知识。
转载:http://blog.csdn.net/u011068702/article/details/53311437