Android进阶之路Android开发Android开发经验谈

插件化——插桩式实现Activity跳转

2020-06-30  本文已影响0人  Jsonjia

代码(已适配android10)已上传github
中,亲测可用

先上效果图

ezgif.com-video-to-gif_gaitubao_598x1023.gif

前言

关于插件化网上比比皆是,但很遗憾之前开发一直没有真正遇到过插件化的公司项目。由于疫情原因换了家新公司并且提前转正,这个项目也是我们用组件化从0开始重构,目前已开发完成。最近领导说apk包体积太大了,而且里面有个模块,可以根据接口类型动态加载,所以这篇文章诞生了。

插件化概念

将整个app拆分成很多模块,每个模块都是一个apk,最终打包的时候将宿主apk和插件apk分开打包,插件apk通过动态下发到宿主apk,实现了动态加载插件并大大减少了包体积。

插件化优点

插件化诞生

举个美团的例子,你就懂了


image.png image.png

美食页面有那么多应用,如果单纯的用webview实现那里面的支付,地图和图片浏览等有点不切实际,或者全部写一个app里面,那包体积少说也有200M,可是你去应用市场看到,也才80M左右,这时插件化出场了


image.png

实现插件化的方式:

插桩式原理

一图胜千言,看图


结构流程图.png

上图右边美团外卖是以一个单独的apk(可以这样理解:一个apk就是一个插件)存在的,宿主App(美团)想要打开插件(美团外卖)中的某一个Activity,但是美团外卖这个插件很显然是没有上下文对象的【原因:因为此插件没有安装到手机上】,要想启动Activity必须要解决上下文这个东西,所以此时就需要在宿主APP中插一个桩,声明一个代理的Activity,如下:

流程图.png
此时ProxyActivity是一个空壳,可是没有显示插件的东西呀,怎么办?其实是这样的:
流程图.png
如何将一个未安装的插件apk的Activity能显示在这个代理ProxyActivity中呢?其实要想插件Activity显示出来肯定得要调用它里面的生命周期方法,而对于插件而言就是将自己Activity中的各种生命周期方法通过接口对外暴露给宿主的ProxyActivity,然后插件Activity中需要的Context则是借用ProxyActivity,这样最终就能达到我们调用的目的,目的达成最终插件化也就这实现了。
所以实现宿主Activity跳转插件化Activity,需要2样东西:
①暴露代理Activity的生命周期给插件化;
②提供上下文给插件化

开干

新建项目

宿主是app,插件是orderfood,这里注意orderfood也是application

image.png

宿主app插件orderfood新建完成,此时需要一个接口来暴露插件orderfood Activity的生命周期,所以还需要定义一个宿主app插件orderfood之间公共的library,里面会定义各种公共接口,这里起名library为:lib_plugin ,如下:

image.png

添加依赖

然后添加对它的依赖


宿主app的依赖.png 插件app的依赖.png

①暴露生命周期

然后在library中定义Activity生命周期的公共接口,如下:


image.png

然后插件Activity得要将其生命周期方法对外暴露,所以需要实现这个接口:


image.png
但是如图并未对接口中的方法进行重写,因为这样写是不合适的,插件中肯定会有n个Activity的,所以需要抽取一个BaseActivity出来,然后再由它来实现抽象接口才靠谱,所以: image.png
image.png
image.png

②提供上下文给插件化app
上面提到过,插件是不会装在手机上的apk,那么插件中的Activity是没有上下文的

image.png
所以需要在BaseActivity中来先重写一个这个方法
image.png
我们已经在插件化把相应的方法进行重写,此时需要把代理的上下文传给插件app,我们在宿主App新建一个代理Activity,并在清单文件注册:
image.png
image.png
接下来就是把代理Activity上下文传给插件化Activity中去,也就是如何调用BaseActivity中的attach()方法,这里就要用到反射了,这里需要知道要跳转插件Activity的全类名,所以这里通过Intent的参数传进来,如下:
image.png
然后通过反射来获取到要跳转插件Activity的对象,由于插件的所有Activity都继承了BaseActivity了,而BaseActivity又实现了公共模块的PluginInterface接口,所以最终就可以调用attach方法,如下:
image.png
所以代理Activity中改成如下:
image.png
我只重写了onStart() 和 onReusme(),剩余的方法是一样的。

在宿主中加载插件

对于加载插件一般有2种:内置和外置。
内置:就是插件的apk放在assert文件目录中
外置:从服务器进行下载到手机sd卡上
不管哪种方式,都需要将插件的类加载进来才行,所以对宿主app的进行修改:

image.png
image.png

接下来就是加载插件了,新建一个插件管理器

 public class PluginManager {
      private Context mContext;//插件的资源对象
      private Resources pluginResource;

      //插件的类加载器
      private DexClassLoader dexClassLoader;

      //插件的包信息类
      private PackageInfo packageInfo;

      private static PluginManager pluginManager = new PluginManager();

      private PluginManager() {
      }

      public static PluginManager getInstance() {
          return pluginManager;
      }

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

      //加载插件apk
      public void loadPlugin(String pluginPath) {
          //获取包管理器
          PackageManager packageManager = mContext.getPackageManager();
          //获取插件的包信息类
          packageInfo = packageManager.getPackageArchiveInfo(pluginPath, PackageManager.GET_ACTIVITIES);

          //插件解压后的目录
          File pluginFile = mContext.getDir("plugin", Context.MODE_PRIVATE);

          //获取到类加载器
          dexClassLoader = new DexClassLoader(pluginPath, pluginFile.getAbsolutePath(), null, mContext.getClassLoader());

          //获取到插件的资源对象
          try {
              AssetManager assetManager = AssetManager.class.newInstance();
              Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
              addAssetPath.invoke(assetManager, pluginPath);
              pluginResource = new Resources(assetManager,mContext.getResources().getDisplayMetrics(),mContext.getResources().getConfiguration());
          } catch (Exception e) {
              e.printStackTrace();
          }
      }

      public Resources getPluginResource() {
          return pluginResource;
      }

      public DexClassLoader getDexClassLoader() {
          return dexClassLoader;
      }

      public PackageInfo getPackageInfo() {
          return packageInfo;
      }

}

接下来打包插件,放到sd卡中:


image.png

打包成功后,如下:


image.png

然后改个名字为:orderfood.apk上传到sd卡根目录下:

image.png
image.png
接下来在宿主Activity中实现跳转插件代码
 public class MainActivity extends AppCompatActivity {

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


      //跳转插件
      public void skipPlugin(View view) {
          PluginManager.getInstance().setContext(this);
          PluginManager.getInstance().loadPlugin(Environment.getExternalStorageDirectory() + "/orderfood.apk");
          PackageInfo packageInfo = PluginManager.getInstance().getPackageInfo();
          Intent intent = new Intent(MainActivity.this, ProxyActivity.class);
          //由于插件只有一个activity,所以取数组第0个
          intent.putExtra("className", packageInfo.activities[0].name);
          startActivity(intent);
      }
  }

记得加权限


image.png

接下来运行app,由于我的手机是Android10.0,所以对于sdcard的权限得要主动申请一下,这里就不写申请的代码了,主动到权限管理中先将其打开,如下:

image.png

代码(已适配android10)已上传github
中,亲测可用

上一篇下一篇

猜你喜欢

热点阅读