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

代码(已适配android10)已上传github中,亲测可用,对你有用的话,记得star,谢谢

先上效果图

ezgif.com-video-to-gif_gaitubao_598x1023.gif

前言

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

插件化概念

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

插件化优点

  • 提高编译速度:开发过程中,每个模块都是独立开发的,编译的时候每次运行不需要都编译所有的业务逻辑代码,所以会适当的提高我们的开发速度;

  • 业务模块完全解耦:每个业务都是完全独立的,这样开发过程中每个模块的功能改变和其他模块没有任何关系,甚至可以随意的去掉某一部分功能;

  • 利于团队开发:插件开发是团队开发中用的最多的一种开发模式,可以更加的去分工,每个组只需要负责自己的功能,减少沟通成本提高开发效率;

  • 动态更新插件,按需下载模块:对于一些不怎么常用的功能,可以让用户按需下载模块,从而减少工程的大小,让用户在下载的时候能够节省流量以及等待时间,而且功能升级的时候可以不更新主应用只更新插件;

  • 解决android 655535问题。

插件化诞生

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


image.png
image.png

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


image.png

实现插件化的方式:

  • 插桩式(本篇文章讲的就是这种方式)
  • Hook方式,这个到时也会学习一个Hook的效果。
  • 反射,但是在Android9.0中有很多反射是用不了了,所以这种基本上不会用了。
插桩式原理

一图胜千言,看图


结构流程图.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
中,亲测可用

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

推荐阅读更多精彩内容