Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 安卓之使用DexClassLoader&AssetManager啟動插件的Activity實現功能插件化

安卓之使用DexClassLoader&AssetManager啟動插件的Activity實現功能插件化

編輯:關於Android編程

深受啟發,所以就寫了這遍文章使用文中的方法實現功能的插件化(雖用不同方法來實現的相同功能,但會從(1)中摘錄許多)。


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

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

Demo項目的效果圖:

這裡寫圖片描述

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

一、主應用apk中的邏輯

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



MainActivity中“添加並運行”按鈕的點擊事件:加載“搶紅包的功能”

public void loadRedPaper(View view) {

    dynamicLoader("redpaper"); 

}

加載功能插件的函數 dynamicLoader(String pluginName)

不安裝功能插件apk的情況下,我們接下來要做的就是獲取插件apk中的Activity,使它我們主應用的宿主Activity偷換,使用這個宿主Activity專門來替換功能插件apk的Activity,在插件apk的Activity中實現相關的功能。

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中的Activity的完整類名
         * 注意完整類名的設置與功能插件名有關
         */                                 intent.putExtra("class","com.cxmscb.cxm."+pluginName+".DynamicActivity");

        // 啟動宿主Activity:
        startActivity(intent);

    }

}

查看功能插件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;

}       

二、宿主Activity中的邏輯

宿主Activity專門用來替換功能插件apk/zip中的Activity,但插件Activity銷毀時宿主Activity也會銷毀。

加載外部功能插件apk/zip使用到了DexClassLoader和AssetManager來構建加載插件apk的類加載器和加載插件資源的Resources對象,具體原理可參考 DexClassLoader&AssetManager中的介紹。下面我們直接使用:

public class LoaderActivity extends Activity {


    //宿主Activity,專用於加載插件apk的Activity

    private String apkPath;//功能插件apk路徑
    private String className;//功能插件中Activity的完整類名

    //功能插件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 {


            // 創建功能插件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中的資源文件進行加載
             */ 


        // 加載插件apk的Activity類    
        Class activityClass = dexClassLoader.loadClass(className);

        // 獲取構造方法並創建Actitiy對象
        Constructor localConstructor = activityClass.getConstructor(new Class[]{});
        Object instance = localConstructor.newInstance(new Object[]{});

        // 調用插件Actitiy中定義的setActivity方法
        Method localMethodSetActivity = activityClass.getDeclaredMethod(
                "setActivity", new Class[] { Activity.class });
        localMethodSetActivity.setAccessible(true);
        localMethodSetActivity.invoke(instance, new Object[] { this });

        // 再調用插件Activity中的onCreate方法
        Method methodOnCreate = activityClass.getDeclaredMethod(
                "onCreate", new Class[] { Bundle.class });
        methodOnCreate.setAccessible(true);

        // 調用時傳入所需的參數:bundle對象
        Bundle paramBundle = new Bundle();
        paramBundle.putBoolean("KEY_START_FROM_OTHER_ACTIVITY", true);
        methodOnCreate.invoke(instance, new Object[] { paramBundle });


        } 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中的Activity就可以通過R來訪問資源

}

三、功能插件Apk中的邏輯

功能插件apk中的Activity。

public class DynamicAcitivty extends Activity {

    private Activity otherActivity; // 偷換過來的宿主Activity
    private View v;  // Activity界面View
    private Button button; // 界面中的按鈕
    private RelativeLayout relativeLayout;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        boolean b = false;
        if (savedInstanceState != null) {
            b = savedInstanceState.getBoolean("KEY_START_FROM_OTHER_ACTIVITY", false);
            if (b) {

                // 簡單的解析布局文件,設置Activity界面和按鈕的設置
                // 不過注意這裡的上下文Context使用的是偷換過來的Activity

                v = LayoutInflater.from(this.otherActivity).inflate(R.layout.fragment_dynamic,null, false);
                this.otherActivity.setContentView(v);
                button  = (Button) v.findViewById(R.id.start);
                relativeLayout = (RelativeLayout) v.findViewById(R.id.rl);

                button.setOnClickListener(new View.OnClickListener() {
                    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
                    @Override
                    public void onClick(View v) {
                        Toast.makeText(otherActivity,"開始搶紅包",Toast.LENGTH_SHORT).show();

                        // 使用資源設置背景
                        relativeLayout.setBackground(otherActivity.getResources().getDrawable(R.drawable.zz));
                    }
                });
            }
        }
        if (!b) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.fragment_dynamic);
        }
    }

    // 在宿主Activity中偷換Activity的方法:setActivity
    public void setActivity(Activity paramActivity) {
        this.otherActivity = paramActivity;
    }
}

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

後續問題:

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

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

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

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

Github : Github

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved