Android · 2016年11月24日 0

Android插件化开发之用DexClassLoader加载未安装的APK来实现app切换背景皮肤

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

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

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

 

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

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

得到如下结果

  1. package:/data/app/com.example.testclassloader-2/base.apk=com.example.testclassloader

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

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

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

  1. adb shell push testClassLoader.apk  /sdcard/

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

 

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

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

 public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags)

具体方法如下

  1. /**
  2.  * 获取未安装apk的信息
  3.  * @param context
  4.  * @param apkPath apk文件的path
  5.  * @return
  6.  */
  7. private Map<String,String> getUninstallApkInfo(Context context, String apkPath) {
  8.     Map hashMap = new HashMap<String,String>();
  9.     PackageManager pm = context.getPackageManager();
  10.     PackageInfo pkgInfo = pm.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);
  11.     if (null != pkgInfo) {
  12.         ApplicationInfo appInfo = pkgInfo.applicationInfo;
  13.         String pkgName = appInfo.packageName;//包名
  14.         hashMap.put(PKG_NAME, pkgName);
  15.     } else {
  16.         Log.d(TAG, “program don’t get apk package information”);
  17.     }
  18.     return hashMap;
  19. }

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

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

  1. /**
  2.  * @param apkPath
  3.  * @return 得到对应插件的Resource对象
  4.  */
  5. private Resources getPluginResources(String apkPath) {
  6.     try {
  7.         AssetManager assetManager = AssetManager.class.newInstance();
  8.         //反射调用方法addAssetPath(String path)
  9.         Method addAssetPath = assetManager.getClass().getMethod(ADDSSETPATH, String.class);
  10.         //将未安装的Apk文件的添加进AssetManager中,第二个参数是apk的路径
  11.         addAssetPath.invoke(assetManager, apkPath);
  12.         Resources superRes = this.getResources();
  13.         Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
  14.         return mResources;
  15.     } catch (Exception e) {
  16.         e.printStackTrace();
  17.     }
  18.     return null;
  19. }

 

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

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

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

  1. package com.chenyu.dexclassloaderapk;
  2. import java.io.File;
  3. import java.lang.reflect.Field;
  4. import java.lang.reflect.Method;
  5. import java.util.HashMap;
  6. import java.util.Map;
  7. import android.content.Context;
  8. import android.content.pm.ApplicationInfo;
  9. import android.content.pm.PackageInfo;
  10. import android.content.pm.PackageManager;
  11. import android.content.res.AssetManager;
  12. import android.content.res.Resources;
  13. import android.graphics.drawable.Drawable;
  14. import android.os.Bundle;
  15. import android.os.Environment;
  16. import android.support.v7.app.ActionBarActivity;
  17. import android.util.Log;
  18. import android.view.View;
  19. import android.view.View.OnClickListener;
  20. import android.widget.ImageView;
  21. import android.widget.RelativeLayout;
  22. import android.widget.TextView;
  23. import com.example.dexclassloaderapk.R;
  24. import dalvik.system.DexClassLoader;
  25. public class MainActivity extends ActionBarActivity {
  26.     public static final String TAG = “DexClassLoaderApk”;
  27.     public static final String PKG_NAME = “pkgName”;
  28.     public static final String APK_PATH = “testClassLoader.apk”;
  29.     public static final String ADDSSETPATH = “addAssetPath”;
  30.     public static final String DEX = “dex”;
  31.     //这个IMAGE_ID是只我放入手机里面APK 在代码里面这个图片的ID,这里我们拿到之后,然后去替换北京图片
  32.     public static final String IMAGE_ID = “about_log”;
  33.     public static final String DRAWABLE = “.R$drawable”;
  34.     public TextView mTextView;
  35.     //背景的布局
  36.     public RelativeLayout mLayout;
  37.     public Map<String, String> apkInfo;
  38.     @Override
  39.     protected void onCreate(Bundle savedInstanceState) {
  40.         super.onCreate(savedInstanceState);
  41.         setContentView(R.layout.activity_main);
  42.         final String apkPath = Environment.getExternalStorageDirectory().toString() + File.separator + APK_PATH;
  43.         mTextView = (TextView)findViewById(R.id.text);
  44.         mLayout = (RelativeLayout)findViewById(R.id.re_Layout);
  45.         mTextView.setOnClickListener(new OnClickListener(){
  46.             @Override
  47.             public void onClick(View v) {
  48.                 //一定要记得加上android.permission.READ_EXTERNAL_STORAGE权限,不然死活都拿不到数据
  49.                 //我就换了一个这个错误,如果发现代码没问题,网上找也没问题,这个时候应该思考是不是没有加权限
  50.                 apkInfo = getUninstallApkInfo(MainActivity.this, apkPath);
  51.                 String packageName = apkInfo.get(PKG_NAME);
  52.                 if (null != packageName) {
  53.                     try {
  54.                     <span style=“white-space:pre”>  </span>dynamicLoadApk(apkPath, packageName);
  55.                     } catch (Exception e) {
  56.                         e.printStackTrace();
  57.                         Log.i(TAG, “change image fail”);
  58.                     }
  59.                 } else {
  60.                     Log.i(TAG, “package is null”);
  61.                 }
  62.             }
  63.         });
  64.     }
  65.     /**
  66.      * 获取未安装apk的信息
  67.      * @param context
  68.      * @param apkPath apk文件的path
  69.      * @return
  70.      */
  71.     private Map<String,String> getUninstallApkInfo(Context context, String apkPath) {
  72.         Map hashMap = new HashMap<String,String>();
  73.         PackageManager pm = context.getPackageManager();
  74.         PackageInfo pkgInfo = pm.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);
  75.         if (null != pkgInfo) {
  76.             ApplicationInfo appInfo = pkgInfo.applicationInfo;
  77.             String pkgName = appInfo.packageName;//包名
  78.             hashMap.put(PKG_NAME, pkgName);
  79.         } else {
  80.             Log.d(TAG, “program don’t get apk package information”);
  81.         }
  82.         return hashMap;
  83.     }
  84.     /**
  85.      * @param apkPath
  86.      * @return 得到对应插件的Resource对象
  87.      */
  88.     private Resources getPluginResources(String apkPath) {
  89.         try {
  90.             AssetManager assetManager = AssetManager.class.newInstance();
  91.             //反射调用方法addAssetPath(String path)
  92.             Method addAssetPath = assetManager.getClass().getMethod(ADDSSETPATH, String.class);
  93.             //将未安装的Apk文件的添加进AssetManager中,第二个参数是apk的路径
  94.             addAssetPath.invoke(assetManager, apkPath);
  95.             Resources superRes = this.getResources();
  96.             Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
  97.             return mResources;
  98.         } catch (Exception e) {
  99.             e.printStackTrace();
  100.         }
  101.         return null;
  102.     }
  103.     /**
  104.      * 加载apk获得内部资源,并且替换背景
  105.      * @param apkDir apk目录
  106.      * @param apkName apk名字,带.apk
  107.      * @throws Exception
  108.      */
  109.     private void dynamicLoadApk(String apkPath, String apkPackageName) throws Exception {
  110.         //在应用安装目录下创建一个名为app_dex文件夹目录,如果已经存在则不创建,这个目录主要是最优化目录,用于缓存dex文件
  111.         File optimizedDirectoryFile = getDir(DEX, Context.MODE_PRIVATE);
  112.         //打印路径 理论上是/data/data/package/app_dex
  113.         Log.v(TAG, optimizedDirectoryFile.getPath().toString());
  114.         //构建DexClassLoader
  115.         DexClassLoader dexClassLoader = new DexClassLoader(apkPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
  116.         //通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源id
  117.         Class<?> clazz = dexClassLoader.loadClass(apkPackageName + DRAWABLE);
  118.         //得到名为about_log的这张图片字段,这个图片是为安装apk里面的图片
  119.         Field field = clazz.getDeclaredField(IMAGE_ID);
  120.         //得到图片id
  121.         int resId = field.getInt(R.id.class);
  122.         //得到插件apk中的Resource
  123.         Resources mResources = getPluginResources(apkPath);
  124.         if (mResources != null) {
  125.             //通过插件apk中的Resource得到resId对应的资源
  126.             Drawable btnDrawable = mResources.getDrawable(resId);
  127.             mLayout.setBackgroundDrawable(btnDrawable);
  128.         } else {
  129.             Log.d(TAG, “mResources is null”);
  130.         }
  131.     }
  132. }

点击TextView内容“换皮肤”来触发的,当初背景是设置的一个机器人。

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

点击换皮护之前背景图片如下
点击换图片之后背景图片如下
ok,说明获取到了这种图片资源,换皮肤成功,这里只是代表换皮肤意思,效果比较丑,不要喷哈。

第七步、总结

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

Share this: