安卓之插件化开发使用DexClassLoader&AssetManager实现功能插件化

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

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

Demo项目的效果图:

安卓之插件化开发使用DexClassLoader&AssetManager实现功能插件化_第1张图片

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

一、主应用apk中的逻辑

  1. 因为要读文件进行读写,在清单文件中进行权限注册:

    
    
    
  2. MainActivity中“添加并运行”按钮的点击事件:加载“抢红包的功能”

    public void loadRedPaper(View view) {
    
        dynamicLoader("redpaper"); 
    
    }
    
  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);
    
        }
    
    }
    
  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中的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来访问资源

}

三、功能插件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;
        }
    
    }
    
  2. 注意功能插件的Fragment完整类名的设置,要与主应用的逻辑一致。例:

    这里写图片描述

  3. 皮肤插件不需要启动Activity:可以清除Activity及其布局文件及其注册。

后续问题:

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

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

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

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

Github : Github

你可能感兴趣的:(Android学习)