安卓之插件化開發使用 DexClassLoader&AssetManager 實現功能插件化
-
在360安全衛士一些應用中,有些功能需要添加(下載)后才可以運行,例如360安全衛士中的搶紅包功能。
-
這是因為這些功能被插件化分離出來成一個apk/zip文件,當用戶使用這些功能時,再去下載相應的插件(不安裝插件apk)來實現功能,當然也可以刪除掉插件文件來實現刪除功能的效果,實現了功能模塊的解耦。
Demo項目的效果圖:
【開始時 主應用本身未實現“紅包助手”功能,然后點擊按鈕“添加并運行”按鈕后,下載功能插件(未安裝)后來實現“紅包助手功能”。】
一、主應用apk中的邏輯
-
因為要讀文件進行讀寫,在清單文件中進行權限注冊:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-
MainActivity中“添加并運行”按鈕的點擊事件:加載“搶紅包的功能”
public void loadRedPaper(View view) {
dynamicLoader("redpaper");
}</code></pre> </li>
加載功能插件的函數 dynamicLoader(String pluginName) :
不安裝功能插件apk的情況下,啟動 插件apk中的Activity 的方案一般是不可以的,因為插件中的Activity沒有在我們的主應用的 清單文件mainfests 中注冊過,又因為 Fragment 不需要注冊。所以我們接下來要做的就是獲取插件apk中的Fragment,使它加載在我們主應用的 宿主Activity 中,使用這個宿主Activity專門來裝載功能插件apk的Fragment,在Fragment中實現相應的功能。
private void dynamicLoader(String pluginName) {
// 查找功能插件apk是否存在: String apkPath = findPlugin(pluginName); if(apkPath==null){ // 不存在時可以從網絡上下載,為方便演示這里先忽略 Toast.makeText(this,"請先下載該插件apk",Toast.LENGTH_SHORT).show(); }else { // 啟動裝載Fragment的宿主Activity Intent intent = new Intent(this,LoaderActivity.class); //傳遞功能插件apk的存放路徑 intent.putExtra("apkPath",apkPath); /** 傳遞功能插件apk中的功能Fragment的完整類名 * 注意完整類名的設置與功能插件名有關 */ intent.putExtra("class","com.cxmscb.cxm."+pluginName+".DynamicFragment"); // 啟動宿主Activity: startActivity(intent); }
}</code></pre> </li>
查看功能插件apk是否已被下載:
private String findPlugin(String pluginName) {
//為方便演示,這里直接將插件apk放置在SD卡根目錄 String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator+pluginName+".apk"; File apk = new File(apkPath); if(apk.exists()){ return apkPath; } return null;
}</code></pre> </li> </ol>
二、宿主Activity中的邏輯
宿主Activity專門用來加載功能插件apk/zip中的Fragment。
加載外部功能插件apk/zip使用到了DexClassLoader和AssetManager來構建加載插件apk的類加載器和加載插件資源的Resources對象,具體原理可參考 DexClassLoader&AssetManager 中的介紹。
為加載插件apk中的類,我們需要構造一個自己的DexClassLoader來加載插件apk中的dex文件,這樣插件中的類才能被找到。
下面我們直接使用:
public class LoaderActivity extends Activity {
//宿主Activity,專用于加載插件apk的Fragment private String apkPath;//功能插件apk路徑 private String className;//功能插件中Fragment的完整類名 //功能插件apk的類加載器、資源對象、資源管理器 private DexClassLoader dexClassLoader; private Resources resources; private AssetManager assetManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); apkPath = intent.getStringExtra("apkPath"); className = intent.getStringExtra("class"); try { // 先準備好裝載插件Fragment的容器布局: FrameLayout frameLayout = new FrameLayout(this); frameLayout.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); // 設置布局id,以備Fragment的插入 frameLayout.setId(2); //設置宿主Activity界面 setContentView(frameLayout); // 創建功能插件apk的類加載器 dexClassLoader = new DexClassLoader(apkPath,this.getDir("dex",Context.MODE_PRIVATE).getAbsolutePath(),null,super.getClassLoader()); // 創建功能插件apk的資源管理器 assetManager = AssetManager.class.newInstance(); AssetManager.class.getDeclaredMethod("addAssetPath", String.class) .invoke(assetManager, apkPath); // 創建功能插件apk的資源對象 resources = new Resources(assetManager,this.getResources().getDisplayMetrics(),this.getResources().getConfiguration()); /** 創建好上面三個對象后,重寫宿主Activity的三個方法: * getClassLoader()、getResources()、getAssetManager() * 這樣就可以使用了這三個對象來對功能插件apk中的Fragment進行加載 */ // 通過反射獲取Fragment對象 Fragment fragment = (Fragment) dexClassLoader.loadClass(className).newInstance(); FragmentManager fm = getFragmentManager(); FragmentTransaction fragmentTransaction = fm.beginTransaction(); // 將Fragment對象放入前面定義好的布局當中 fragmentTransaction.add(2,fragment); fragmentTransaction.commit(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } /*catch (ClassNotFoundException e) { e.printStackTrace(); }*/ catch (ClassNotFoundException e) { e.printStackTrace(); } } @Override public ClassLoader getClassLoader() { return dexClassLoader==null?super.getClassLoader():dexClassLoader; } @Override public Resources getResources() { return resources==null?super.getResources():resources; } public AssetManager getAssetManager() { return assetManager==null?super.getAssets():assetManager; } //這樣一來,在apk中的Fragment就可以通過R來訪問資源
}</code></pre>
三、功能插件Apk中的邏輯
-
創建功能插件的Fagment及其布局文件。
public class DynamicFragment extends Fragment {
@Override public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) { //簡單地解析layout文件、獲取控件和設置監聽 View v = inflater.inflate(R.layout.fragment_dynamic, container, false); final Button button = (Button) v.findViewById(R.id.start); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 彈出Toast,注意Context的傳參。 Toast.makeText(getActivity().getApplication(),"開始搶紅包",Toast.LENGTH_SHORT).show(); } }); return v; }
}</code></pre> </li>
注意功能插件的Fragment完整類名的設置,要與主應用的邏輯一致。例:
皮膚插件不需要啟動Activity:可以清除Activity及其布局文件及其注冊。
后續問題:
1.在插件apk打包后可能會對Fragment類名進行混淆,這樣會無法被主應用反射到。
2.上述主應用的邏輯并未完整,為了方便演示省去了皮膚插件的下載(不需要安裝)
3.功能插件apk最好存放在較私密的地方,為了不方便被清理軟件掃描到可更后綴為zip文件
4.既然可以添加插件功能,當然也可以刪除插件功能。再添加一個刪除功能插件apk文件功能即可。