Android系統使用了ClassLoader機制來進行Activity等組件的加載;apk被安裝之后,APK文件的代碼以及資源會被系統存放在固定的目錄(比如/data/app/package_name/1.apk)系統在進行類加載的時候,會自動去這一個或者幾個特定的路徑來尋找這個類;但是系統并不知道存在于插件中的Activity組件的信息,插件可以是任意位置,甚至是網絡,系統無法提前預知,因此正常情況下系統無法加載我們插件中的類;因此也沒有辦法創建Activity的對象,更不用談啟動組件了。這個時候就需要使用動態加載技術了,關于Activity如何插件化,后面系列在說,本文講了一個應用程序換膚的故事,雖然老套,但是對于理解動態加載技術很實用,讀完之后你可以知道如何解決插件之中的資源加載問題。
關于類加載器,請看深入探討 Java 類加載器
一、動態加載dex的技術
Android使用Dalvik虛擬機加載可執行程序,所以不能直接加載基于class的jar,而是需要將class轉化為dex字節碼,從而執行代碼。優化后的字節碼文件可以存在一個.jar中,只要其內部存放的是.dex即可使用。
我們現在要實現的一個需求是:如何調用一個非本應用的java程序,如下:
app 與loutillib兩個模塊沒有任何的依耐關系,在Module App中,我們想調用Loutillib中的LogUitl輸出一條log。LogUitl如下,so easy。
public class LogUitl {
? ? public static final String TAG="LogUitl";
? ? private void? printLog(){
? ? ? ? Log.e(TAG,"這是來自另外一個dex中的log");
? ? }
}
所以我們要在運行時把LogUitl動態加載到app這個進程中, Android支持動態加載的兩種方式是:DexClassLoader和PathClassLoader,DexClassLoader可加載jar/apk/dex,且支持從SD卡加載;PathClassLoader只能加載已經安裝在Android系統內APK文件( /data/app 目錄下),其它位置的文件加載的時候都會出現 ClassNotFoundException。 因為 PathClassLoader 會去讀取 /data/dalvik-cache 目錄下的經過 Dalvik 優化過的 dex 文件,這個目錄的 dex 文件是在安裝 apk 包的時候由 Dalvik 生成的,沒有安裝的時候,自然沒有生成那個文件。
這里我們用DexClassLoader來加載,LogUitl所生成的dex文件。首先用gradle打出LogUtil的jar包。
task makeJar(type:Copy){
? ? delete 'build/libs/log.jar'
? ? from('build/intermediates/bundles/release/')
? ? into('build/libs/')
? ? include('classes.jar')
? ? rename ('classes.jar', 'log.jar')
? ? exclude('test/','BuildConfig.class','R.class')
? ? exclude{it.name.startsWith('R$');}
}
makeJar.dependsOn(build)
注意,這個jar還不能被加載,這個是基于class的jar,Dalvik虛擬機加載的是dex字節碼,所以需要將class轉化為dex字節碼。這個需要用到dx命令,這個可以在Android\sdk\build-tools\23.0.0中找到,把log.jar拷貝到這個目錄下,執行
dx --dex --output=new_log.jar log.jar
在執行
adb? push? new_log.jar? sdcard/
把這個new_log放進SDCARD中,這樣dex的準備工作就OK了。以下是用DexClassLoader動態加載的代碼。
public class MainActivity extends Activity {
? ? @Override
? ? protected void attachBaseContext(Context newBase) {
? ? ? ? super.attachBaseContext(newBase);
? ? }
? ? @Override
? ? protected void onCreate(Bundle savedInstanceState) {
? ? ? ? super.onCreate(savedInstanceState);
? ? ? ? setContentView(R.layout.activity_main);
? ? }
? ? public void start(View view) {
? ? ? ? //dex解壓釋放后的目錄?
? ? ? ? final File dexOutPutDir = getDir("dex", 0);
? ? ? ? //dex所在目錄?
? ? ? ? final String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "new_log.jar";
? ? ? ? //第一個參數:是dex壓縮文件的路徑
? ? ? ? //第二個參數:是dex解壓縮后存放的目錄
? ? ? ? //第三個參數:是C/C++依賴的本地庫文件目錄,可以為null
? ? ? ? //第四個參數:是上一級的類加載器?
? ? ? ? DexClassLoader classLoader=new DexClassLoader(dexPath,dexOutPutDir.getAbsolutePath(),null,getClassLoader());
? ? ? ? try {
? ? ? ? ? ? final Class<?> loadClazz = classLoader.loadClass("zhangwan.wj.com.logutillib.LogUitl");
? ? ? ? ? ? final Object o = loadClazz.newInstance();
? ? ? ? ? ? final Method printLogMethod = loadClazz.getDeclaredMethod("printLog");
? ? ? ? ? ? printLogMethod.setAccessible(true);
? ? ? ? ? ? printLogMethod.invoke(o);
? ? ? ? } catch (Exception e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? }
}
執行結果:
發現成功的調用了printLog方法。有上面的基礎,現在實現一個難一點的,如何給應用程序換膚,這個難體現在資源加載上。通常各種各樣的皮膚都是一個個的apk文件,當用戶需要哪個皮膚,就下載到本地,然后動態加載,但是當宿主程序調起未安裝的皮膚插件apk的時候,插件中以R開頭的資源都不能被訪問,程序會拋出異常,無法找到某某id所對應的資源。這是因為加載資源都是通過Resourse來實現的,Resource對象是由Context得到的,我們知道一個app的工程的資源文件都會隱射到R文件中,而這個R文件的包名則是這個應用的包名,所以一個包名一般對應一個Context。宿主與皮膚插件的包名是不一樣的,所以宿主Context找不到皮膚插件的資源。
二、應用換膚
1、皮膚程序準備
sky.apk
children.apk
準備兩個apk,sky.apk中有一張名字為skin_one的背景圖,顯示的是藍色的天空;children.apk中也有一張名字為skin_one的背景圖,顯示的是一個小孩。將這兩個apk都push到SD里面,兩套皮膚準備完成。
2、資源加載問題怎么解決
通過分析系統資源加載了解到,系統是通過ContextImpl中的getAssets與getResources加載資源的
/**
? ? * Returns an AssetManager instance for the application's package.
? ? * <p>
? ? * <strong>Note:</strong> Implementations of this method should return
? ? * an AssetManager instance that is consistent with the Resources instance
? ? * returned by {@link #getResources()}. For example, they should share the
? ? * same {@link Configuration} object.
? ? *
? ? * @return an AssetManager instance for the application's package
? ? * @see #getResources()
? ? */
? ? public abstract AssetManager getAssets();
? ? /**
? ? * Returns a Resources instance for the application's package.
? ? * <p>
? ? * <strong>Note:</strong> Implementations of this method should return
? ? * a Resources instance that is consistent with the AssetManager instance
? ? * returned by {@link #getAssets()}. For example, they should share the
? ? * same {@link Configuration} object.
? ? *
? ? * @return a Resources instance for the application's package
? ? * @see #getAssets()
? ? */
? ? public abstract Resources getResources();
ContextImpl中,也就是說,只要實現這兩個方法,就可以解決資源問題了。不饒彎子了,直接上代碼,解釋請移步Android動態加載技術三個關鍵問題詳解。
/**
? ? * 獲取AssetManager? 用來加載插件資源
? ? * @param pFilePath? 插件的路徑
? ? * @return
? ? */
? ? private AssetManager createAssetManager(String pFilePath) {
? ? ? ? try {
? ? ? ? ? ? final AssetManager assetManager = AssetManager.class.newInstance();
? ? ? ? ? ? final Class<?> assetManagerClazz = Class.forName("android.content.res.AssetManager");
? ? ? ? ? ? final Method addAssetPathMethod = assetManagerClazz.getDeclaredMethod("addAssetPath", String.class);
? ? ? ? ? ? addAssetPathMethod.setAccessible(true);
? ? ? ? ? ? addAssetPathMethod.invoke(assetManager, pFilePath);
? ? ? ? ? ? return assetManager;
? ? ? ? } catch (Exception e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? ? ? return null;
? ? }
? ? //這個Resources就可以加載非宿主apk中的資源
? ? private Resources? createResources(String pFilePath){
? ? ? ? final AssetManager assetManager = createAssetManager(pFilePath);
? ? ? ? Resources superRes = this.getResources();
? ? ? ? return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
? ? }
3、動態加載皮膚apk
public class MainActivity extends Activity {
? ? private TextView? mSkinTv;
? ? private? boolean mChange=false;
? ? @Override
? ? protected void onCreate(Bundle savedInstanceState) {
? ? ? ? super.onCreate(savedInstanceState);
? ? ? ? setContentView(R.layout.activity_proxy);
? ? ? ? mSkinTv= (TextView) findViewById(R.id.skin_bg);
? ? }
? ? /**
? ? * 獲取未安裝apk的信息
? ? * @param context
? ? * @param pApkFilePath apk文件的path
? ? * @return
? ? */
? ? private String getUninstallApkPkgName(Context context, String pApkFilePath) {
? ? ? ? PackageManager pm = context.getPackageManager();
? ? ? ? PackageInfo pkgInfo = pm.getPackageArchiveInfo(pApkFilePath, PackageManager.GET_ACTIVITIES);
? ? ? ? if (pkgInfo != null) {
? ? ? ? ? ? ApplicationInfo appInfo = pkgInfo.applicationInfo;
? ? ? ? ? ? return appInfo.packageName;
? ? ? ? }
? ? ? ? return "";
? ? }
? ? public void switchSkin(View view) {
? ? ? ? String skinType="";
? ? ? ? if(!mChange){
? ? ? ? ? ? skinType= "sky.apk";
? ? ? ? ? ? mChange=true;
? ? ? ? }else {
? ? ? ? ? ? skinType= "children.apk";
? ? ? ? ? ? mChange=false;
? ? ? ? }
? ? ? ? final String path = Environment.getExternalStorageDirectory() + File.separator + skinType;
? ? ? ? final String pkgName = getUninstallApkPkgName(this, path);
? ? ? ? dynamicLoadApk(path,pkgName);
? ? }
? ? private? void dynamicLoadApk(String pApkFilePath,String pApkPacketName){
? ? ? ? File file=getDir("dex", Context.MODE_PRIVATE);
? ? ? ? //第一個參數:是dex壓縮文件的路徑
? ? ? ? //第二個參數:是dex解壓縮后存放的目錄
? ? ? ? //第三個參數:是C/C++依賴的本地庫文件目錄,可以為null
? ? ? ? //第四個參數:是上一級的類加載器
? ? ? ? DexClassLoader? classLoader=new DexClassLoader(pApkFilePath,file.getAbsolutePath(),null,getClassLoader());
? ? ? ? try {
? ? ? ? ? ? final Class<?> loadClazz = classLoader.loadClass(pApkPacketName + ".R$drawable");
? ? ? ? ? //插件中皮膚的名稱是skin_one
? ? ? ? ? ? final Field skinOneField = loadClazz.getDeclaredField("skin_one");
? ? ? ? ? ? skinOneField.setAccessible(true);
? ? ? ? ? ? //反射獲取skin_one的resousreId
? ? ? ? ? ? final int resousreId = (int) skinOneField.get(R.id.class);
? ? ? ? ? ? //可以加載插件資源的Resources
? ? ? ? ? ? final Resources resources = createResources(pApkFilePath);
? ? ? ? ? ? if (resources != null) {
? ? ? ? ? ? ? ? final Drawable drawable = resources.getDrawable(resousreId);
? ? ? ? ? ? ? ? mSkinTv.setBackground(drawable);
? ? ? ? ? ? }
? ? ? ? } catch (Exception e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? }
}
執行效果
到此換膚成功!