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

大家好,我是徐爱卿。博客地址: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 PackageInfo getPluginPackageArchiveInfo() {
        return pluginPackageArchiveInfo;
    }

    private PackageInfo pluginPackageArchiveInfo;

    public static PluginManager getInstance() {
        return ourInstance;
    }

    private PluginManager() {
    }

    public void setContext(Context context) {
        this.context = context.getApplicationContext();
    }

    public void loadApk(String dexPath) {
        //(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
        pluginDexClassLoader = new DexClassLoader(dexPath, context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, context.getClassLoader());

        pluginPackageArchiveInfo = context.getPackageManager().getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES);

        //Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        AssetManager assets = null;
        try {
            assets = AssetManager.class.newInstance();
            Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assets, dexPath);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        pluginResources = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
    }


    public DexClassLoader getPluginDexClassLoader() {
        return pluginDexClassLoader;
    }

    public Resources getPluginResources() {
        return pluginResources;
    }


}

step3 ProxyActivity

ProxyActivity是宿主的Activity,这个ProxyActivity只是一个空壳,提供一套生命周期和上下文给我们自己创建的PluginActivity的的实例用的。

再次重申!我们自己加载的PluginActivity实例只是一个对象,没有任何意义的,要给它套上生命周期,给他的上下文赋值

具体实现思路

启动PluginActivity时,先去启动ProxyActivity,然后再ProxyAcitivity中的oCreate方法中去创建PluginActivity的实例,然后去调用PluginActivity的onCreate方法。在ProxyActivity的onResume方法中调用PluginActivity的onResume方法等等。
具体实现:

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //拿到要启动的Activity
        String className = getIntent().getStringExtra("className");
        try {
            //加载该Activity的字节码对象
            Class<?> aClass = PluginManager.getInstance().getPluginDexClassLoader().loadClass(className);
            //创建该Activity的示例
            Object newInstance = aClass.newInstance();
            //程序健壮性检查
            if (newInstance instanceof PluginInterface) {
                pluginInterface = (PluginInterface) newInstance;
                //将代理Activity的实例传递给三方Activity
                pluginInterface.attachContext(this);
                //创建bundle用来与三方apk传输数据
                Bundle bundle = new Bundle();
                //调用三方Activity的onCreate,
                pluginInterface.onCreate(bundle);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

注意:记得重写ProxyActivity的getResources,因为这个时候要拿到的getResources是Plugin的。

/**
     * 注意:三方调用拿到对应加载的三方Resources
     * @return
     */
    @Override
    public Resources getResources() {
        return PluginManager.getInstance().getPluginResources();
    }

完整的ProxyActivity

public class ProxyActivity extends FragmentActivity {

    private PluginInterface pluginInterface;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //拿到要启动的Activity
        String className = getIntent().getStringExtra("className");
        try {
            //加载该Activity的字节码对象
            Class<?> aClass = PluginManager.getInstance().getPluginDexClassLoader().loadClass(className);
            //创建该Activity的示例
            Object newInstance = aClass.newInstance();
            //程序健壮性检查
            if (newInstance instanceof PluginInterface) {
                pluginInterface = (PluginInterface) newInstance;
                //将代理Activity的实例传递给三方Activity
                pluginInterface.attachContext(this);
                //创建bundle用来与三方apk传输数据
                Bundle bundle = new Bundle();
                //调用三方Activity的onCreate,
                pluginInterface.onCreate(bundle);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }


    /**
     * 注意:三方调用拿到对应加载的三方Resources
     * @return
     */
    @Override
    public Resources getResources() {
        return PluginManager.getInstance().getPluginResources();
    }

    @Override
    public void startActivity(Intent intent) {
        Intent newIntent = new Intent(this, ProxyActivity.class);
        newIntent.putExtra("className", intent.getComponent().getClassName());
        super.startActivity(newIntent);
    }

    @Override
    public void onStart() {
        pluginInterface.onStart();
        super.onStart();
    }

    @Override
    public void onResume() {
        pluginInterface.onResume();
        super.onResume();
    }

    @Override
    public void onRestart() {
        pluginInterface.onRestart();
        super.onRestart();
    }

    @Override
    public void onDestroy() {
        pluginInterface.onDestroy();
        super.onDestroy();
    }

    @Override
    public void onStop() {
        pluginInterface.onStop();
        super.onStop();
    }

    @Override
    public void onPause() {
        pluginInterface.onPause();
        super.onPause();
    }

}

step4 Plugin的BaseActivity的构建

构建Plugin的BaseActivity的原因是统一上下文为ProxyActivity的实例,关于上下文的各种操作均是调用ProxyActivity的实例去进行操作。

public class BaseActivity extends FragmentActivity implements PluginInterface {

    //注意:这里命名为protected,以便于子类使用
    protected FragmentActivity thisContext;

    @Override
    public void onCreate(Bundle savedInstanceState) {

    }

    @Override
    public void setContentView(int layoutResID) {
        thisContext.setContentView(layoutResID);
    }

    @Override
    public void setContentView(View view) {
        thisContext.setContentView(view);
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        thisContext.setContentView(view, params);
    }

    @Override
    public LayoutInflater getLayoutInflater() {
        return thisContext.getLayoutInflater();
    }

    @Override
    public Window getWindow() {
        return thisContext.getWindow();
    }

    @Override
    public View findViewById(int id) {
        return thisContext.findViewById(id);
    }

    @Override
    public void attachContext(FragmentActivity context) {
        thisContext = context;
    }

    @Override
    public ClassLoader getClassLoader() {
        return thisContext.getClassLoader();
    }

    @Override
    public WindowManager getWindowManager() {
        return thisContext.getWindowManager();
    }


    @Override
    public ApplicationInfo getApplicationInfo() {
        return thisContext.getApplicationInfo();
    }

    @Override
    public void finish() {
        thisContext.finish();
    }


    public void onStart() {

    }

    public void onResume() {

    }

    @Override
    public void onRestart() {

    }

    public void onPause() {

    }

    public void onStop() {

    }

    public void onDestroy() {

    }

    public void onSaveInstanceState(Bundle outState) {

    }

    public boolean onTouchEvent(MotionEvent event) {
        return false;
    }

    public void onBackPressed() {
        thisContext.onBackPressed();
    }

    @Override
    public void startActivity(Intent intent) {
        thisContext.startActivity(intent);
    }


}

PluginMainActivity

public class PluginMainActivity extends BaseActivity implements View.OnClickListener {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_plugin_main);
        findViewById(R.id.btn).setOnClickListener(this);
    }


    @Override
    public void onClick(View v) {
        startActivity(new Intent(thisContext,SecondActivity.class));
    }
}```
 

![PluginMainActivity布局](http://upload-images.jianshu.io/upload_images/3884536-0481f91191c0b343.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

##step5 在宿主中启动PluginMainActivity
```java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void loadApk(View view) {
        //注意:使用运行时权限
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
    }

    public void startApk(View view) {
        Intent intent = new Intent(this, ProxyActivity.class);
        String otherApkMainActivityName = PluginManager.getInstance().getPluginPackageArchiveInfo().activities[0].name;
        intent.putExtra("className", otherApkMainActivityName);
        startActivity(intent);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        PluginManager.getInstance().setContext(this);
        PluginManager.getInstance().loadApk(Environment.getExternalStorageDirectory().getAbsolutePath()+"/otherapk-debug.apk");
    }
}

别忘记在XML中添加读写SD卡权限了。

最后验证,我们将build的otherapk放到SD卡中(模拟下载),然后点击加载plugin,如下:

大功告成

结束语

万事开头难,这个Activity的启动以及点击事件的启蒙篇完工了后,其他的三大组件也是慢慢可以类推的。

GitHub地址,欢迎多多start,你的start就是我的动力,谢谢!

fashionlife.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容