Android外掛程式化(三)載入外掛程式apk中的Resource資源
如何載入未安裝apk中的資源檔呢?我們從android.content.res.AssetManager.java的源碼中發現,它有一個私人方法addAssetPath,只需要將apk的路徑作為參數傳入,我們就可以獲得對應的AssetsManager對象,然後我們就可以使用AssetsManager對象,建立一個Resources對象,然後就可以從Resource對象中訪問apk中的資源了。總結如下:
1.建立一個AssetManager對象 2.通過反射調用addAssetPath方法 3.以AssetsManager對象為參數,建立Resources對象即可。
代碼如下:
package net.mobctrl.hostapk;import java.io.File;import android.content.Context;import android.content.res.AssetManager;import android.content.res.Resources;/** * @Author Zheng Haibo * @PersonalWebsite http://www.mobctrl.net * @version $Id: LoaderResManager.java, v 0.1 2015年12月11日 下午7:58:59 mochuan.zhb * Exp $ * @Description 動態載入資源的管理器 */public class BundlerResourceLoader { private static AssetManager createAssetManager(String apkPath) { try { AssetManager assetManager = AssetManager.class.newInstance(); try { AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke( assetManager, apkPath); } catch (Throwable th) { System.out.println("debug:createAssetManager :"+th.getMessage()); th.printStackTrace(); } return assetManager; } catch (Throwable th) { System.out.println("debug:createAssetManager :"+th.getMessage()); th.printStackTrace(); } return null; } /** * 擷取Bundle中的資源 * @param context * @param apkPath * @return */ public static Resources getBundleResource(Context context){ AssetsManager.copyAllAssetsApk(context); File dir = context.getDir(AssetsManager.APK_DIR, Context.MODE_PRIVATE); String apkPath = dir.getAbsolutePath()+"/BundleApk.apk"; System.out.println("debug:apkPath = "+apkPath+",exists="+(new File(apkPath).exists())); AssetManager assetManager = createAssetManager(apkPath); return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration()); }}
DEMO
注意:我們使用Resources對象,擷取資源時,傳遞的ID必須是離線apk中R檔案對應的資源的ID。如果使用getIdentifier方法,第一個參數是資源名稱,第二個參數是資源類型,第三個參數是離線apk的包名,切記第三個參數。
Resources resources = BundlerResourceLoader.getBundleResource(getApplicationContext()); imageView = (ImageView)findViewById(R.id.image_view_iv); if(resources != null){ String str = resources.getString(resources.getIdentifier("test_str", "string", "net.mobctrl.normal.apk")); String strById = resources.getString(0x7f050001);//注意,id參照Bundle apk中的R檔案 System.out.println("debug:"+str); Toast.makeText(getApplicationContext(),strById, Toast.LENGTH_SHORT).show(); Drawable drawable = resources.getDrawable(0x7f020000);//注意,id參照Bundle apk中的R檔案 imageView.setImageDrawable(drawable); }
上述代碼是載入離線apk中的字串和Drawable資源,那麼layout資源呢?
問題引入
我們使用LayoutInflate對象,一般使用方法如下:
View view = LayoutInflater.from(context).inflate(R.layout.main_fragment, null);
其中,R.layout.main_fragment我們可以通過上述方法擷取其ID,那麼關鍵的一步就是如何產生一個context?直接傳入當前的context是不行的。
解決方案有2個:
- 1.建立一個自己的ContextImpl,Override其方法。
- 2.通過反射,直接替換當前context的mResources私人成員變數。<>br
當然,我們是使用第二種方案:
@Override protected void attachBaseContext(Context context) { replaceContextResources(context); super.attachBaseContext(context); } /** * 使用反射的方式,使用Bundle的Resource對象,替換Context的mResources對象 * @param context */ public void replaceContextResources(Context context){ try { Field field = context.getClass().getDeclaredField("mResources"); field.setAccessible(true); field.set(context, mBundleResources); System.out.println("debug:repalceResources succ"); } catch (Exception e) { System.out.println("debug:repalceResources error"); e.printStackTrace(); } }
我們在Activity的attachBaseContext方法中,對Context的mResources進行替換,這樣,我們就可以載入離線apk中的布局了。
資源檔的打包過程
如果想要做到外掛程式化,需要瞭解Android資源檔的打包過程,這樣可以為每一個外掛程式進行編號,然後按照規則產生R檔案。例如,以攜程DynamicAPK為例,它將外掛程式的R檔案按照如下規則:
1.R檔案為int型,前8位代表外掛程式的Id,其中兩個特殊的Id:Host是0x7f,android系統內建的是以0x01開頭. 2.緊跟著的8位是區分資源類型的,比如layout,id,string,dimen等 3.後面16位是資源的編號
按照上述規則產生對應的外掛程式apk。然後在運行時,我們可以寫一個ResourceManager類,它繼承自Resource對象,然後所有的Activity,都將其context的mResource成員變數修改為ResourceManager類,然後Override其方法,然後在載入資源時,根據不同的id的首碼,尋找對應外掛程式的Resource即可。也就是說,用一個類做分發。