在前面的文章中我們成功的加載了外部的Dex(Apk)並執行了插件的Bean代碼。這時我們會想,能不能加載並運行插件Apk的Activity。答案當然是能,否則后續我們的研究就沒意義了,但是想實現Activity的插件化運行,我們必須要解決一個問題——如何使用插件中的資源。
本文我們就講一下插件的資源加載機制,並講述一下如何實現資源的插件化。
一、資源的加載機制
Android的資源文件分為兩類:
第一類是res目錄下存放的可編輯的資源文件,這類文件在編譯時系統會自動在R文件中生成資源文件的16進制值。
例如:
public final class R { public static final class anim { public static final int abc_fade_in=0x7f050000; public static final int abc_fade_in=0x7f050000; ... } }
我們在平時的開發時,訪問這類資源比較簡單,使用Context的getResource方法即可得到res下的各種資源。如下面代碼所示:
String content = mContext.getResource().getString(R.string.content);
第二類是assets目錄下存放的原始資源文件。apk在編譯時不會編譯assets下的文件,我們不能使用R.的方式訪問,只能使用AssetsManager類的open方法來獲取assets目錄下的文件資源。
而AssetsManager又源於Resources類的getAssets方法,如下面代碼所示:
Resource resource = getResource(); AssetsManager am = getResource().getAssets(); InputStream is = getResource().getAssets().open("filename");
通過上面的分析,我們可以初步做出一個結論:我們能使用Resources類是一個很重要的類,通過此類提供的相關API,我們能操作資源的加載。
二、資源插件化的解決方案
談及資源插件化,我們不得不對AssetsManager的API多說一些。
AssetsManager中有一個addAssetsPath(String Path)方法,App啟動的時候就會將當前的apk路徑傳進去,接下來AssetsManager和Resources就能訪問當前apk的所有資源了。
AssetsManager的addAssetsPath方法不對外,但是我們可以通過反射的方式,把插件apk的路徑傳到這個方法,這樣就把插件的資源添加到一個資源池中了。App有幾個插件,我們就調用幾次addAssetsPath方法,把插件的資源都塞到池子里。
這里我們以加載插件Apk里面的字符串資源為目標,實戰一下資源插件化:
首先我們在插件app的string.xml里面定義字符串資源:
<string name="myplugin1_hello_world">Hello Plugin</string>
然后我們在宿主app編寫如下代碼:
public class MainActivity extends Activity { private AssetManager mAssetManager; private Resources mResources; private Resources.Theme mTheme; private String dexPath = null; //apk文件地址 private File fileRelease = null; //釋放目錄 protected DexClassLoader classLoader = null; private String pluginName = "plugin1.apk"; TextView mTextView; @Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(newBase); extractAssets(newBase, pluginName); } @SuppressLint("NewApi") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); File extractFile = this.getFileStreamPath(pluginName); dexPath = extractFile.getPath(); fileRelease = getDir("dex", 0); classLoader = new DexClassLoader(dexPath, fileRelease.getAbsolutePath(), null, getClassLoader()); mTextView = findViewById(R.id.tv); //帶資源文件的調用 findViewById(R.id.btn_6).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { loadResources(); try { Class mLoadClassDynamic = classLoader.loadClass("com.plugin1.Dynamic"); Object dynamicObject = mLoadClassDynamic.newInstance(); IDynamic dynamic = (IDynamic) dynamicObject; String content = dynamic.getStringForResId(MainActivity.this); mTextView.setText(content); Toast.makeText(getApplicationContext(), content + "", Toast.LENGTH_LONG).show(); } catch (Exception e) { e.printStackTrace(); } } }); } protected void loadResources() { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, dexPath); mAssetManager = assetManager; } catch (Exception e) { e.printStackTrace(); } Resources superRes = super.getResources(); superRes.getDisplayMetrics(); superRes.getConfiguration(); mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration()); mTheme = mResources.newTheme(); mTheme.setTo(super.getTheme()); } @Override public AssetManager getAssets() { return mAssetManager == null ? super.getAssets() : mAssetManager; } @Override public Resources getResources() { return mResources == null ? super.getResources() : mResources; } @Override public Resources.Theme getTheme() { return mTheme == null ? super.getTheme() : mTheme; } /** * 把Assets里面得文件復制到 /data/data/files 目錄下 * * @param context * @param sourceName */ public static void extractAssets(Context context, String sourceName) { AssetManager am = context.getAssets(); InputStream is = null; FileOutputStream fos = null; try { is = am.open(sourceName); File extractFile = context.getFileStreamPath(sourceName); fos = new FileOutputStream(extractFile); byte[] buffer = new byte[1024]; int count = 0; while ((count = is.read(buffer)) > 0) { fos.write(buffer, 0, count); } fos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { closeSilently(is); closeSilently(fos); } } private static void closeSilently(Closeable closeable) { if (closeable == null) { return; } try { closeable.close(); } catch (Throwable e) { e.printStackTrace(); } } }
以上代碼分為四個邏輯部分:
1). loadResources方法。通過反射創建AssetManager對象,調用addAssetPath方法,把插件Plugin1路徑添加到這個AssetManager對象中。從此這個AssetManager就只為插件Plugin1服務了。在這個AssetManager對象的基礎上,創建對應的Resource和Theme對象。
2). 重寫Activity的getAsset,getResource和getTheme方法。重寫邏輯見上面的代碼。
3). 加載外部的插件,生成這個插件對應的ClassLoader。
4). 通過反射,獲取插件中的類,構造出插件類的對象,然后就可以讓插件類讀取插件中的資源了。
上述內容代碼倉庫地址為:https://github.com/renhui/RHPluginProgramming/tree/master/DexResAccess-master
基於這個思路,我們可以嘗試使用插件資源替換當前顯示的內容,實現換膚效果,核心思想是一樣的,這里就不過多贅述了。