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

第一步、先制做一个有我们需要的图片资源的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/base.apk=com.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<String,String> getUninstallApkInfo(Context context, String apkPath) {  
            Map hashMap = new HashMap<String,String>();
            PackageManager pm = context.getPackageManager();  
            PackageInfo pkgInfo = pm.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);  
            if (null != pkgInfo) {  
                ApplicationInfo appInfo = pkgInfo.applicationInfo;  
                String pkgName = appInfo.packageName;//包名  
                hashMap.put(PKG_NAME, pkgName);
            } else {
                Log.d(TAG, "program don't get apk package information");
            }  
            return hashMap;  
        }  

 

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

 

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

 

        /**
         * @param apkPath  
         * @return 得到对应插件的Resource对象
         */  
        private Resources getPluginResources(String apkPath) {  
            try {  
                AssetManager assetManager = AssetManager.class.newInstance();
                //反射调用方法addAssetPath(String path)
                Method addAssetPath = assetManager.getClass().getMethod(ADDSSETPATH, String.class);  
                //将未安装的Apk文件的添加进AssetManager中,第二个参数是apk的路径  
                addAssetPath.invoke(assetManager, apkPath);
                Resources superRes = this.getResources();  
                Resources mResources = new 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
Android插件化开发之动态加载基础之ClassLoader工作机制   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 optimizedDirectoryFile = getDir(DEX, Context.MODE_PRIVATE);
            //打印路径 理论上是/data/data/package/app_dex
            Log.v(TAG, optimizedDirectoryFile.getPath().toString());  
            //构建DexClassLoader
            DexClassLoader dexClassLoader = new DexClassLoader(apkPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());  
            //通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源id
            Class<?> clazz = dexClassLoader.loadClass(apkPackageName + DRAWABLE);  
            //得到名为about_log的这张图片字段,这个图片是为安装apk里面的图片
            Field field = clazz.getDeclaredField(IMAGE_ID);
            //得到图片id  
            int resId = field.getInt(R.id.class);
            //得到插件apk中的Resource  
            Resources mResources = getPluginResources(apkPath);
            if (mResources != null) {  
                //通过插件apk中的Resource得到resId对应的资源  
                Drawable btnDrawable = mResources.getDrawable(resId);
                mLayout.setBackgroundDrawable(btnDrawable);   
            } else {
                Log.d(TAG, "mResources is null");
            }
        }

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

    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 TAG = "DexClassLoaderApk";
        public static final String PKG_NAME = "pkgName";
        public static final String APK_PATH = "testClassLoader.apk";
        public static final String ADDSSETPATH = "addAssetPath";
        public static final String DEX = "dex";
        //这个IMAGE_ID是只我放入手机里面APK 在代码里面这个图片的ID,这里我们拿到之后,然后去替换北京图片
        public static final String IMAGE_ID = "about_log";
        public static final String DRAWABLE = ".R$drawable";
        public TextView mTextView;
        //背景的布局
        public RelativeLayout mLayout;
        public Map<String, String> apkInfo;
        
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            final String apkPath = Environment.getExternalStorageDirectory().toString() + File.separator + APK_PATH;
            mTextView = (TextView)findViewById(R.id.text);
            mLayout = (RelativeLayout)findViewById(R.id.re_Layout);
            mTextView.setOnClickListener(new OnClickListener(){
                @Override
                public void onClick(View v) {
                    //一定要记得加上android.permission.READ_EXTERNAL_STORAGE权限,不然死活都拿不到数据
                    //我就换了一个这个错误,如果发现代码没问题,网上找也没问题,这个时候应该思考是不是没有加权限
                    apkInfo = getUninstallApkInfo(MainActivity.this, apkPath);
                    String packageName = apkInfo.get(PKG_NAME);
                    if (null != packageName) {
                        try {
                            dynamicLoadApk(apkPath, packageName);
                        } catch (Exception e) {
                            e.printStackTrace();
                            Log.i(TAG, "change image fail");
                        }
                    } else {
                        Log.i(TAG, "package is null");
                    }
                }
            });
        }
        
        /**
         * 获取未安装apk的信息
         * @param context
         * @param apkPath apk文件的path
         * @return
         */  
        private Map<String,String> getUninstallApkInfo(Context context, String apkPath) {  
            Map hashMap = new HashMap<String,String>();
            PackageManager pm = context.getPackageManager();  
            PackageInfo pkgInfo = pm.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);  
            if (null != pkgInfo) {  
                ApplicationInfo appInfo = pkgInfo.applicationInfo;  
                String pkgName = appInfo.packageName;//包名  
                hashMap.put(PKG_NAME, pkgName);
            } else {
                Log.d(TAG, "program don't get apk package information");
            }  
            return hashMap;  
        }  
        
        /**
         * @param apkPath  
         * @return 得到对应插件的Resource对象
         */  
        private Resources getPluginResources(String apkPath) {  
            try {  
                AssetManager assetManager = AssetManager.class.newInstance();  
                //反射调用方法addAssetPath(String path)
                Method addAssetPath = assetManager.getClass().getMethod(ADDSSETPATH, String.class);  
                //将未安装的Apk文件的添加进AssetManager中,第二个参数是apk的路径  
                addAssetPath.invoke(assetManager, apkPath);
                Resources superRes = this.getResources();  
                Resources mResources = new 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 optimizedDirectoryFile = getDir(DEX, Context.MODE_PRIVATE);
            //打印路径 理论上是/data/data/package/app_dex
            Log.v(TAG, optimizedDirectoryFile.getPath().toString());  
            //构建DexClassLoader
            DexClassLoader dexClassLoader = new DexClassLoader(apkPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());  
            //通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源id
            Class<?> clazz = dexClassLoader.loadClass(apkPackageName + DRAWABLE);  
            //得到名为about_log的这张图片字段,这个图片是为安装apk里面的图片
            Field field = clazz.getDeclaredField(IMAGE_ID);
            //得到图片id  
            int resId = field.getInt(R.id.class);
            //得到插件apk中的Resource  
            Resources mResources = getPluginResources(apkPath);
            if (mResources != null) {  
                //通过插件apk中的Resource得到resId对应的资源  
                Drawable btnDrawable = mResources.getDrawable(resId);
                mLayout.setBackgroundDrawable(btnDrawable);   
            } else {
                Log.d(TAG, "mResources is null");
            }
        }  
    }

    dynamicLoadApk(apkPath, packageName);
                    } catch (Exception e) {
                        e.printStackTrace();
                        Log.i(TAG, "change image fail");
                    }
                } else {
                    Log.i(TAG, "package is null");
                }
            }
        });
    }
    
    /**
     * 获取未安装apk的信息
     * @param context
     * @param apkPath apk文件的path
     * @return
     */  
    private Map<String,String> getUninstallApkInfo(Context context, String apkPath) {  
        Map hashMap = new HashMap<String,String>();
        PackageManager pm = context.getPackageManager();  
        PackageInfo pkgInfo = pm.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);  
        if (null != pkgInfo) {  
            ApplicationInfo appInfo = pkgInfo.applicationInfo;  
            String pkgName = appInfo.packageName;//包名  
            hashMap.put(PKG_NAME, pkgName);
        } else {
            Log.d(TAG, "program don't get apk package information");
        }  
        return hashMap;  
    }  
    
    /**
     * @param apkPath  
     * @return 得到对应插件的Resource对象
     */  
    private Resources getPluginResources(String apkPath) {  
        try {  
            AssetManager assetManager = AssetManager.class.newInstance();  
            //反射调用方法addAssetPath(String path)
            Method addAssetPath = assetManager.getClass().getMethod(ADDSSETPATH, String.class);  
            //将未安装的Apk文件的添加进AssetManager中,第二个参数是apk的路径  
            addAssetPath.invoke(assetManager, apkPath);
            Resources superRes = this.getResources();  
            Resources mResources = new 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 optimizedDirectoryFile = getDir(DEX, Context.MODE_PRIVATE);
        //打印路径 理论上是/data/data/package/app_dex
        Log.v(TAG, optimizedDirectoryFile.getPath().toString());  
        //构建DexClassLoader
        DexClassLoader dexClassLoader = new DexClassLoader(apkPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());  
        //通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源id
        Class<?> clazz = dexClassLoader.loadClass(apkPackageName + DRAWABLE);  
        //得到名为about_log的这张图片字段,这个图片是为安装apk里面的图片
        Field field = clazz.getDeclaredField(IMAGE_ID);
        //得到图片id  
        int resId = field.getInt(R.id.class);
        //得到插件apk中的Resource  
        Resources mResources = getPluginResources(apkPath);
        if (mResources != null) {  
            //通过插件apk中的Resource得到resId对应的资源  
            Drawable btnDrawable = mResources.getDrawable(resId);
            mLayout.setBackgroundDrawable(btnDrawable);   
        } else {
            Log.d(TAG, "mResources is null");
        }
    }  
}


点击TextView内容“换皮肤”来触发的,当初背景是设置的一个机器人。
第六步:运行项目爆结果照片
点击换皮护之前背景图片如下
 


点击换图片之后背景图片如下
 

style="text-align:center;">


ok,说明获取到了这种图片资源,换皮肤成功,这里只是代表换皮肤意思,效果比较丑,不要喷哈。
第七步、总结
这样做资源和宿主分离了,减轻了apk负担,同时也有解耦和作用,我们手机一些浏览器换模式(日和夜)、QQ换皮肤、表情包、线上下载线下维护、是项目更加灵活,可扩展性更好,同时也复习了DexClassLoader和反射相关知识。
 
  作者:chen.yu
深信服三年半工作经验,目前就职游戏厂商,希望能和大家交流和学习,
微信公众号:编程入门到秃头 或扫描下面二维码
零基础入门进阶人工智能(链接)