安卓之使用DexClassLoader&AssetManager启动插件的Activity实现功能插件化

前言: 写完 安卓之插件化开发使用DexClassLoader&AssetManager实现功能插件化 通过在宿主Activity中装载插件Fragment来实现功能插件化后,

在网上又看见了一篇比较插件化的文章 探秘腾讯Android手机游戏平台之不安装游戏APK直接启动法 通过非系统的启动方法(反射生成Activity实例,并调用onCreate方法)启动了插件apk的Activity,并传递过去了主应用的上下文Context。

深受启发,所以就写了这遍文章使用文中的方法实现功能的插件化。

  1. 在360安全卫士一些应用中,有些功能需要添加(下载)后才可以运行,例如360安全卫士中的抢红包功能。

  2. 这是因为这些功能被插件化分离出来成一个apk/zip文件,当用户使用这些功能时,再去下载相应的插件(不安装插件apk)来实现功能,当然也可以删除掉插件文件来实现删除功能的效果,实现了功能模块的解耦。

Demo项目的效果图:

《安卓之使用DexClassLoader&AssetManager启动插件的Activity实现功能插件化》

【开始时 主应用本身未实现“红包助手”功能,然后点击按钮“添加并运行”按钮后,下载功能插件(未安装)后来实现“红包助手功能”。】

一、主应用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"); 
    
    }
    
  3. 加载功能插件的函数 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);
    
        }
    
    }
    
  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;
    
    }       
    

二、宿主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方法传递上下文Context
        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中的逻辑

  1. 功能插件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的上下文:setActivity
        public void setActivity(Activity paramActivity) {
            this.otherActivity = paramActivity;
        }
    }
    
  2. 注意功能插件的Activity完整类名的设置,要与主应用的逻辑一致。

后续问题:

1.在插件apk打包后可能会对Activity类名进行混淆,这样会无法被主应用反射到。

2.上述主应用的逻辑并未完整,为了方便演示省去了皮肤插件的下载(不需要安装)

3.功能插件apk最好存放在较私密的地方,为了不方便被清理软件扫描到可更后缀为zip文件

4.既然可以添加插件功能,当然也可以删除插件功能。再添加一个删除功能插件apk文件功能即可。

Github : Github

    原文作者:cxmscb
    原文地址: https://blog.csdn.net/cxmscb/article/details/52514286
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞