Android插件化之DexClasssLoader动态加载apk(Java代码和资源文件)

DexClassLoader介绍

DexClassLoader可以载入一个含有classes.dex文件的压缩包,可以是jar,可以是apk,也可以是含有dex文件的zip。

构造器DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

  • dexPath: 包含dex文件的压缩文件(jar,apk,zip)的绝对路径,不能为空。

  • optimizedDirectory: 解压后存储路径,建议放在程序私有路径/data/data/com.xxx.xxx下。

  • libraryPath:os库的存放路径,可以为空,若有os库,必须填写。

  • parent:父类加载器,一般为context.getClassLoader()

接下来,开始动手,编码实战。


一个假设性的需求

假设有一个这样的需求,宿主需要加载Plugin.apk,获取到插件Plugin相关的信息或资源。

1. 新建个名为Plugins项目

新建一个Plugin项目,该项目放入一些需要被调用的java代码和资源文件,如下图所示:
这里写图片描述

先放入一种图片资源,供宿主调用,路下图所示:
这里写图片描述

例如:宿主获取到Plugin中的某个实体的信息。

这里,先创建一个Bean实体:

public class Bean implements BeanProvider{
    private String name="根根";

    @Override
    public void setName(String name) {
        this.name=name;
    }

    @Override
    public String getName() {
        return name;
    }
}

这里思考一下,宿主又该入如何获取到该实体信息呢?

因Plugin和宿主是属于不同的Module,无法直接通过new方式创建。

在Java中,跨Jar调用,一般考虑反射。

反射调用Plugin中某个类的方法或者静态方法,比较容易实现,这里省略不说。

可能存在异步业务处理逻辑,考虑面向接口方式,回调方式获取。

2. 新建一个通用CommmonLibrary项目

新建一个宿主和Plugin都依赖的通用类库,提供对应接口,如下图所示:
这里写图片描述

在实际开发中,采用面向接口编程方式来调用Plugin中的功能逻辑,会有很多个回调接口,为了方便统一管理。

先创建一个动态调用的接口,用作统一的入口:

public interface IDynamic {
   /**
    * 涉及插件中回调
    * @param callBack
    */
   void  invokeCallback(CallBack callBack);
}

接下来,创建一个回调接口,用于传递信息实体:

public interface CallBack {
    void callback(BeanProvider beanProvider);
}

在提出一个Bean实体的操作接口:

public interface BeanProvider {
    void setName(String name);
    String getName();
}

又返回到Plugin项目中去,去添加具体的的Bean业务逻辑。

在实际开发中,该bean对象可能是从数据库中读取,或者从网络上获取,也可能需要经过一系列的逻辑操作才产生。

创建一个IDynamic接口的实现类。

public class Dynamic implements IDynamic {
    @Override
    public void invokeCallback(CallBack callBack) {
        //具体的业务逻辑操作
        BeanProvider provider=new Bean();
        provider.setName("根根");
        callBack.callback(provider);
    }
}

注意点Plugins项目和宿主App项都需要依赖该通用库。

3. 宿主加载Plugins项目生成的apk

3.1 准备步骤

通过AndroidStudio中Build-->Build APK(s)生成对应的apk。

将Plugin项目产生apk拷贝到宿主中assets文件夹下,如下图所示:
这里写图片描述

接下来,编写Assets文件的Utils工具类:

public class Utils {
    public static String copyFiles(Context context, String fileName) {
        File dir = getCacheDir(context);
        String filePath = dir.getAbsolutePath() + File.separator + fileName;
        try {
            File desFile = new File(filePath);
            if (!desFile.exists()) {
                desFile.createNewFile();
                copyFiles(context, fileName, desFile);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return filePath;

    }
    public static void copyFiles(Context context, String fileName, File desFile) {
        InputStream in = null;
        OutputStream out = null;
        try {
            in = context.getApplicationContext().getAssets().open(fileName);
            out = new FileOutputStream(desFile.getAbsolutePath());
            byte[] bytes = new byte[1024];
            int i;
            while ((i = in.read(bytes)) != -1)
                out.write(bytes, 0, i);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null)
                    in.close();
                if (out != null)
                    out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
    public static boolean hasExternalStorage() {
        return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
    }

    /**
     * 获取缓存路径
     *
     * @param context
     * @return 返回缓存文件路径
     */
    public static File getCacheDir(Context context) {
        File cache;
        if (hasExternalStorage()) {
            cache = context.getExternalCacheDir();
        } else {
            cache = context.getCacheDir();
        }
        if (!cache.exists())
            cache.mkdirs();
        return cache;
    }


}

将assets中plugin.apk拷贝到手机磁盘中,方便DexClassLoader加载:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private String dexPath;
    private String fileName = "plugin-debug.apk";
    private String cacheDir;

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        this.dexPath = Utils.copyFiles(newBase, fileName);
        this.cacheDir = Utils.getCacheDir(newBase).getAbsolutePath();
    }

}

3.2 插件中代码接口回调

在onCreate()中创建DexClassLoader对象:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
  private DexClassLoader dexClassLoader;

   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dexClassLoader = new DexClassLoader(dexPath, cacheDir, null, getClassLoader());
        findViewById(R.id.main_test).setOnClickListener(this);

    }

}

然后按钮点击调用,插件中回调返回Bean实体信息:

    @Override
    public void onClick(View v) {
        try {
           //通过dexClassLoader加载指定包名的类
            Class<?> mClass = dexClassLoader.loadClass("com.xingen.plugin.Dynamic");
            IDynamic iDynamic = (IDynamic) mClass.newInstance();
            iDynamic.invokeCallback(new CallBack() {
                @Override
                public void callback(BeanProvider beanProvider) {
                    //插件回调宿主
                    Toast.makeText(getApplicationContext()," 插件回调宿主 ,获取的Bean实体的字段是 "+beanProvider.getName(),Toast.LENGTH_LONG).show();
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

3.3加载插件中图片资源

思路:先反射方式获取apk的AssetManager,在创建apk的Resource对象,通过Resource获取到资源文件。

先创建一个工具类:

public class AssertsDexLoader {
    /**
     * 获取指定apk的AssetManager
     *
     * 例如:获取插件的AssetManager
     *
     * @param apkPath
     * @return
     */
    public static AssetManager createAssetManager(String apkPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(
                    assetManager, apkPath);
            return assetManager;
        } catch (Throwable th) {
            th.printStackTrace();
        }
        return null;
    }

    /**
     * 获取到插件中的Resource
     * @param context
     * @param apkPath
     * @return
     */
    public static Resources getResource(Context context, String apkPath){
        AssetManager assetManager = createAssetManager(apkPath);
        return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
    }

}

接下来,加载显示到ImageView中:

    /**
     * 加载插件中的resource资源
     */
    private void loadImage(){
        ImageView imageView=findViewById(R.id.main_iv);
        File file = new File(dexPath);
        Log.d("xingen", "file exist " + file.exists()+" "+dexPath);
       Resources resources=AssertsDexLoader.getResource(getApplicationContext(),dexPath);

        try {
            //加载Drawable下的bd_logo1图片
            Drawable drawable=resources.getDrawable(resources.getIdentifier("bd_logo1","drawable","com.xingen.plugin"));
           imageView.setImageDrawable(drawable);
        }catch (Exception e){
           e.printStackTrace();
        }
    }

4. 运行效果如下

这里写图片描述

项目代码地址:https://github.com/13767004362/DexProject

猜你喜欢

转载自blog.csdn.net/hexingen/article/details/80555634
今日推荐