美文网首页android深入浅出AndroidAndroid知识
Android插件化之动态加载APK实现

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

作者: 徐爱卿 | 来源:发表于2017-07-26 07:30 被阅读1365次

    网友说笑,中国新四大发明:高铁、支付宝、网购、单车。想想也是这个理,反正我的生活中没离开过这几个东西。现在的支付宝可谓是个全能助手了,集成了外卖、淘票票、天猫超市等等。估计没有那个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:https://img.haomeiwen.com/i3884536/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

    相关文章

      网友评论

      • Android_wfq:你好,从github拉下来代码,运行otherapk后将apk拷贝到sd卡,加载plugin没问题,运行后,跳转到插件时闪退,debug跟踪是
        Class<?> aClass = PluginManager.getInstance().getPluginDexClassLoader().loadClass(className);走了ClassNotFoundException,怎样才能解决呢
        Android_wfq:@蓝雁南飞 已经解决了这个问题,是因为as开了instantrun,这样的话生成的apk会有两个dex,关闭就可以了
        蓝雁南飞:初步估计是权限问题。很多手机到了6.0之后需要用户手动选择权限的。可以先看看报这个错之前有没有IO之类的异常(级别是警告的),如果有的话,你加入这个判断再运行看看。
      • 偶滴神丫:我按照你的文章自己code的代码,插件中findviewById报空指针你觉得应该是哪个地方出了问题呢?
        偶滴神丫:@徐爱卿 可以了 我重新写了一遍 ,好了。还是不知道原来哪出了问题?
        偶滴神丫:@徐爱卿 Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.View.setOnClickListener(android.view.View$OnClickListener)' on a null object reference
        at com.beijing.zzu.pluginapk.PluginMainActivity.onCreate(PluginMainActivity.java:14)
        at com.beijing.zzu.plugindemo.ProxyActivity.onCreate(ProxyActivity.java:35)

        就是这一句话 findViewById(R.id.btn).setOnClickListener(this);
        徐爱卿:@偶滴神丫 能方便看下error么
      • 81c17c76d562:插件公用的基础库(类似网络请求)是每个插件都包含的?资源共用要怎么办?
      • IAM四十二:这个markdown 写的真是醉了,完全没有格式!
        徐爱卿:@IAM四十二 已经修复
        IAM四十二:@徐爱卿 嗯,不要因为格式的问题,使得可读性下降了
        徐爱卿:@IAM四十二 不好意思 我记得以前改过 下班后重新改下
      • Jey欧巴:那么问题来了,如果加载的apk需要更新,需要开发人员手动更改吗
        徐爱卿:@Jey欧巴 不用啊 这个就看你自己更新插件的逻辑了 在某种情况下进行更新插件 看业务需求
      • db83ecb6a69e:我运行你的demo 了,将otherapk-debug.apk放在外置卡中,加载apk是没有问题的,为啥跳转到插件ProxyActivity 执行 //加载该Activity的字节码对象
        Class<?> aClass = PluginManager.getInstance().getPluginDexClassLoader().loadClass(className);就会报:java.lang.ClassNotFoundException: Didn't find class "xu.otherapk.PluginMainActivity" on path: DexPathList[[zip file "/storage/sdcard1/otherapk-debug.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib, /data/datalib]],没有办法找到这个类?
        徐爱卿:@战国吃熊 我刚才从github上拉了代码 又跑了一遍 没问题 你重新build下otherapk然后重新放到SD卡中 再试一下:smile:
        徐爱卿:@战国吃熊 应该没问题的 我有时间看下
      • slimsallen:确实 安卓可以这样搞 其实很想知道ios 是怎么搞的 插件化 貌似不可行了
        徐爱卿:@音乐君 iOS目前不知晓 目前没有做过iOS开发 :sweat:
      • 纳兰寒明:get到新姿势,点个赞,加油!我的沙发~~~
        徐爱卿:@纳兰寒明 :smile:

      本文标题:Android插件化之动态加载APK实现

      本文链接:https://www.haomeiwen.com/subject/asknkxtx.html