Android插件化開發(fā)之用DexClassLoader加載未安裝的APK資源文件來實(shí)現(xiàn)app切換背景皮膚
第一步、先制做一個有我們需要的圖片資源的APK
如下圖,這里有個about_log.png,我們需要生成apk文件。
生成的apk文件如果你不到項(xiàng)目的文件夾里面去取apk,想通過命令放到手機(jī)里面去可以快速用下面命令
1)、在手機(jī)里面通過包名找到apk路徑,一定不要忘記有 -f
adb shell pm list package -f | grep com.example.testclassloader
得到如下結(jié)果
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放到手機(jī)里面去,命令如下
adb shell push testClassLoader.apk /sdcard/
4)、去手機(jī)文件管理器里面找看是否有testClassLoader.apk文件
第二步、獲取為安裝apk包名的信息(假設(shè)前提不知道)
我們可以通過這個方法得到
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
因?yàn)闆]有安裝,所以不能得到context,所以我們需要未安裝apk的Resource,我們可以通過反射來獲取,代碼如下
/**
* @param apkPath
* @return 得到對應(yīng)插件的Resource對象
*/
private Resources getPluginResources(String apkPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
//反射調(diào)用方法addAssetPath(String path)
Method addAssetPath = assetManager.getClass().getMethod(ADDSSETPATH, String.class);
//將未安裝的Apk文件的添加進(jìn)AssetManager中,第二個參數(shù)是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插件化開發(fā)之DexClassLoader動態(tài)加載dex、jar小Demo http://blog.csdn.net/u011068702/article/details/53263442
Android插件化開發(fā)之動態(tài)加載基礎(chǔ)之ClassLoader工作機(jī)制 http://blog.csdn.net/u011068702/article/details/53248960
代碼如下:
/**
* 加載apk獲得內(nèi)部資源,并且替換背景
* @param apkDir apk目錄
* @param apkName apk名字,帶.apk
* @throws Exception
*/
private void dynamicLoadApk(String apkPath, String apkPackageName) throws Exception {
//在應(yīng)用安裝目錄下創(chuàng)建一個名為app_dex文件夾目錄,如果已經(jīng)存在則不創(chuàng)建,這個目錄主要是最優(yōu)化目錄,用于緩存dex文件
File optimizedDirectoryFile = getDir(DEX, Context.MODE_PRIVATE);
//打印路徑 理論上是/data/data/package/app_dex
Log.v(TAG, optimizedDirectoryFile.getPath().toString());
//構(gòu)建DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(apkPath,
optimizedDirectoryFile.getPath(), null,
ClassLoader.getSystemClassLoader());
//通過使用apk自己的類加載器,反射出R類中相應(yīng)的內(nèi)部類進(jìn)而獲取我們需要的資源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對應(yīng)的資源
Drawable btnDrawable = mResources.getDrawable(resId);
mLayout.setBackgroundDrawable(btnDrawable);
} else {
Log.d(TAG, "mResources is null");
}
}
第五步、爆出所有代碼(為了詳細(xì)點(diǎn))
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是只我放入手機(jī)里面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權(quán)限,不然死活都拿不到數(shù)據(jù)
//我就換了一個這個錯誤,如果發(fā)現(xiàn)代碼沒問題,網(wǎng)上找也沒問題,這個時候應(yīng)該思考是不是沒有加權(quán)限
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 得到對應(yīng)插件的Resource對象
*/
private Resources getPluginResources(String apkPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
//反射調(diào)用方法addAssetPath(String path)
Method addAssetPath = assetManager.getClass().getMethod(ADDSSETPATH, String.class);
//將未安裝的Apk文件的添加進(jìn)AssetManager中,第二個參數(shù)是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獲得內(nèi)部資源,并且替換背景
* @param apkDir apk目錄
* @param apkName apk名字,帶.apk
* @throws Exception
*/
private void dynamicLoadApk(String apkPath, String apkPackageName) throws Exception {
//在應(yīng)用安裝目錄下創(chuàng)建一個名為app_dex文件夾目錄,如果已經(jīng)存在則不創(chuàng)建,這個目錄主要是最優(yōu)化目錄,用于緩存dex文件
File optimizedDirectoryFile = getDir(DEX, Context.MODE_PRIVATE);
//打印路徑 理論上是/data/data/package/app_dex
Log.v(TAG, optimizedDirectoryFile.getPath().toString());
//構(gòu)建DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(apkPath,
optimizedDirectoryFile.getPath(), null,
ClassLoader.getSystemClassLoader());
//通過使用apk自己的類加載器,反射出R類中相應(yīng)的內(nèi)部類進(jìn)而獲取我們需要的資源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對應(yīng)的資源
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 得到對應(yīng)插件的Resource對象
*/
private Resources getPluginResources(String apkPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
//反射調(diào)用方法addAssetPath(String path)
Method addAssetPath = assetManager.getClass().getMethod(ADDSSETPATH, String.class);
//將未安裝的Apk文件的添加進(jìn)AssetManager中,第二個參數(shù)是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獲得內(nèi)部資源,并且替換背景
* @param apkDir apk目錄
* @param apkName apk名字,帶.apk
* @throws Exception
*/
private void dynamicLoadApk(String apkPath, String apkPackageName) throws Exception {
//在應(yīng)用安裝目錄下創(chuàng)建一個名為app_dex文件夾目錄,如果已經(jīng)存在則不創(chuàng)建,這個目錄主要是最優(yōu)化目錄,用于緩存dex文件
File optimizedDirectoryFile = getDir(DEX, Context.MODE_PRIVATE);
//打印路徑 理論上是/data/data/package/app_dex
Log.v(TAG, optimizedDirectoryFile.getPath().toString());
//構(gòu)建DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(apkPath,
optimizedDirectoryFile.getPath(), null,
ClassLoader.getSystemClassLoader());
//通過使用apk自己的類加載器,反射出R類中相應(yīng)的內(nèi)部類進(jìn)而獲取我們需要的資源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對應(yīng)的資源
Drawable btnDrawable = mResources.getDrawable(resId);
mLayout.setBackgroundDrawable(btnDrawable);
} else {
Log.d(TAG, "mResources is null");
}
}
}
點(diǎn)擊TextView內(nèi)容“換皮膚”來觸發(fā)的,當(dāng)初背景是設(shè)置的一個機(jī)器人。
第六步:運(yùn)行項(xiàng)目爆結(jié)果照片
點(diǎn)擊換皮護(hù)之前背景圖片如下
點(diǎn)擊換圖片之后背景圖片如下
style="text-align:center;">
ok,說明獲取到了這種圖片資源,換皮膚成功,這里只是代表換皮膚意思,效果比較丑,不要噴哈。
第七步、總結(jié)
這樣做資源和宿主分離了,減輕了apk負(fù)擔(dān),同時也有解耦和作用,我們手機(jī)一些瀏覽器換模式(日和夜)、QQ換皮膚、表情包、線上下載線下維護(hù)、是項(xiàng)目更加靈活,可擴(kuò)展性更好,同時也復(fù)習(xí)了DexClassLoader和反射相關(guān)知識。
作者:chen.yu
深信服三年半工作經(jīng)驗(yàn),目前就職游戲廠商,希望能和大家交流和學(xué)習(xí),
微信公眾號:編程入門到禿頭 或掃描下面二維碼
零基礎(chǔ)入門進(jìn)階人工智能(鏈接)