Android插件化之动态加载APK实现

本文详细介绍了Android应用如何实现动态加载插件APK,包括动态加载Plugin的分析、PluginInterface、PluginManager、ProxyActivity的构建过程,以及资源文件的获取。通过ProxyActivity和PluginManager,实现了不安装第三方APK就能运行其页面的功能,降低了应用的维护成本。
摘要由CSDN通过智能技术生成

大家好,我是徐爱卿。博客地址:flutterall.com

网友说笑,中国新四大发明:高铁、支付宝、网购、单车。想想也是这个理,反正我的生活中没离开过这几个东西。现在的支付宝可谓是个全能助手了,集成了外卖、淘票票、天猫超市等等。估计没有那个APP有如此炸天的功能了。问题来了,向外卖、单车、天猫超市这些东西难道说是支付宝APP在发新包中就写死在里面的么?还是只是个H5页面呢?

下面看下支付宝中得天猫超市和淘票票

天猫超市很显然是H5

淘票票很显然是Native

ofo小黄车很显然是Native
天猫超市是H5,没什么以外的,毕竟一个APP中使用H5页面很正常。可是淘票票呢?ofo小黄车呢?爱我去,是个native页面,这就厉害了。难道我支付宝的开发人员还要开发维护你ofo小黄车?又或者说我支付宝要集成你ofo小黄车,不可能 !否则的话,支付宝就炸了。
很显然,支付宝是使用了动态加载apk的解决方案。也就是说,支付宝作为一个宿主apk提前将要集成的apk作为一个插件(plugin)下载到本地,然后当使用该plugin(apk)的时候再去加载对应plugin(apk)的资源文件以及对应的native页面。往大了说,就是不去安装该plugin(apk)就可以直接运行该plugin(apk)中的页面

本博客中得Plugin均指的是第三方apk,也就是相当于支付宝(宿主)中的ofo小黄车(插件-Plugin)。

动态加载Plugin(apk)分析

如何调用一个apk中的页面呢?我们可以动态加载Plugin中的文件资源使其以伪宿主身份运行在宿主apk中。本文以加载一个Activity页面来作为例子进行讲解。
怎么理解呢?
这么理解:如果说系统创建的Activity是一个拥有四肢能动能跳的人的话,那么我们手动创建的Activity只是一个人偶,这个人偶虽然也有四肢,但是他动不了,应为没有对应的掌控者。
这可怎么办?我们可以把这个人偶的四肢与真正的人的四肢绑在一起,这样的话,当真正的人的四肢动了,这个人偶也就动了,看起来人偶分真正的人一样,会动会跳。那么,这里动态加载Plugin中,宿主扮演者控制者,插件扮演者人偶。要让插件中的Activity活起来,我们可以在宿主中创建一个活生的Activity,然后去手动创建插件Activity的实例,然后使用活生的Activity的生命周期去调用插件Activity的生命周期,这样就可以让Plugin中的Activity活了起来。

  • Plugin中Activity生命周期的处理
    我们可以在宿主中使用一个特殊的Activity,这个Activity是一个空壳,没有任何页面。但是它有实际的Activity的生命周期,这样我们可以通过这个Activity的生命周期去调用我们自己创建的Plugin中的Activity中的生命周期,实现了Plugin中的Activity的伪生命周期。这个宿主Activity命名为ProxyActivity。下面来张图:

动态加载Plugin中Activity

  • Plugin中资源文件的获取
    这个就好办了,我们可以使用AssetManager去得到Plugin包中的资源文件。

加载Plugin实现

step1 PluginInterface

我们的宿主要提供一套标准,这套标准用来规范宿主与Plugin之间的上下文以及生命周期关系的标准。我们称之为:PluginInterface。这个标准涉及到Activity生命周期以及上下文,定义如下:

public interface PluginInterface {
   
    void onCreate(Bundle saveInstance);
    void attachContext(FragmentActivity context);

    void onStart();

    void onResume();

    void onRestart();

    void onDestroy();

    void onStop();

    void onPause();
}

我们新建一个依赖库plugin,依赖库plugin中只有一个PluginInterface,这个interface作为一个依赖库的形式存在于宿主与Plugin中。

PluginInterface
宿主gradle与Plugin gradle一致如下:

dependencies {
   
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
   
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:23.0.0'
    testCompile 'junit:junit:4.12'
    compile project(':plugin')//重点
}

为了使得编译起来更方便,我这里将宿主apk,插件plugin(项目中称之为otherapk)与依赖库plugin放在同一个项目下,只不过这个项目有两个module。

项目层级关系

step2 PluginManager

宿主需要一套工具,这个工具用来管理加载Plugin,以及获取Plugin中资源文件等,定义为:PluginManager。

  • 获取Plugin的字节码文件对象
    我们要拿到Plugin中的字节码文件对象,需要拿到Plugin对应的DexClassLoader。可以使用DexClassLoaderDexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)方法。
    • dexPath是Plugin的路径
    • optimizedDirectory是Plugin的缓存路径
    • libraryPath可以为null
    • parent为父类加载器

这样以来伪代码:new DexClassLoader(dexPath, ProxyActivity.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, ProxyActivityContext.getClassLoader());就可以拿到Plugin的DexClassLoader了。然后就可以使用DexClassLoader.loadClass(PluginActivityName);加载到PluginActivity的字节码文件对象了,进而创建PluginActivity的实例。

  • 获取Plugin的Resources
    我们可以使用Resource提供的下面的构造:
 /**
     * Create a new Resources object on top of an existing set of assets in an
     * AssetManager.
     *
     * @param assets Previously created AssetManager.
     * @param metrics Current display metrics to consider when
     *                selecting/computing resource values.
     * @param config Desired device configuration to consider when
     *               selecting/computing resource values (optional).
     */
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
   
        this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO);
    }

由于要获取Plugin中的资源,所以这个assets对象应当是Plugin中的资源对象;而对于一款手机的DisplayMetrics和Configuration来说,无论是宿主还是Plugin获取的值都是一样的,所以可以使用宿主的值。

获取AssetManager对象

/**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public final int addAssetPath(String path) {
   
        synchronized (this) {
   
            int res = addAssetPathNative(path);
            makeStringBlocks(mStringBlocks);
            return res;
        }
    }

这个path也就是Plugin包在手机中的位置,由于这个方法被hide了,我们需要使用反射。

AssetManager assets = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assets, dexPath);

到这里,成功拿到了Plugin的DexClassLoader和Resources。
完整代码如下:

public class PluginManager {
   

    private static PluginManager ourInstance = new PluginManager();
    private Context context;

    private DexClassLoader pluginDexClassLoader;
    private Resources pluginResources;

    public Pack
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值