安卓之插件化開發使用 DexClassLoader&AssetManager 實現功能插件化

MayFrayne 7年前發布 | 6K 次閱讀 安卓開發 Android開發 移動開發

  1. 在360安全衛士一些應用中,有些功能需要添加(下載)后才可以運行,例如360安全衛士中的搶紅包功能。

  2. 這是因為這些功能被插件化分離出來成一個apk/zip文件,當用戶使用這些功能時,再去下載相應的插件(不安裝插件apk)來實現功能,當然也可以刪除掉插件文件來實現刪除功能的效果,實現了功能模塊的解耦。

Demo項目的效果圖:

【開始時 主應用本身未實現“紅包助手”功能,然后點擊按鈕“添加并運行”按鈕后,下載功能插件(未安裝)后來實現“紅包助手功能”。】

一、主應用apk中的邏輯

  1. 因為要讀文件進行讀寫,在清單文件中進行權限注冊:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  2. MainActivity中“添加并運行”按鈕的點擊事件:加載“搶紅包的功能”

    public void loadRedPaper(View view) {

    dynamicLoader("redpaper"); 
    
    

    }</code></pre> </li>

  3. 加載功能插件的函數 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>

  4. 查看功能插件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中的邏輯

    1. 創建功能插件的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>

    2. 注意功能插件的Fragment完整類名的設置,要與主應用的邏輯一致。例:

    3. 皮膚插件不需要啟動Activity:可以清除Activity及其布局文件及其注冊。

    4. </ol>

      后續問題:

      1.在插件apk打包后可能會對Fragment類名進行混淆,這樣會無法被主應用反射到。

      2.上述主應用的邏輯并未完整,為了方便演示省去了皮膚插件的下載(不需要安裝)

      3.功能插件apk最好存放在較私密的地方,為了不方便被清理軟件掃描到可更后綴為zip文件

      4.既然可以添加插件功能,當然也可以刪除插件功能。再添加一個刪除功能插件apk文件功能即可。

       

       

 本文由用戶 MayFrayne 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!