实现一个最简单,能运行的插件化框架。

352 阅读4分钟

源码:github.com/hqweb/Plugi…

将模块打包成apk,这个apk就叫插件,宿主动态的加载这个插件就是插件化。 虽然插件化已经是很老的技术,但是插件化中用到的技术值得每个Android人学习。

实现一个最简单能运行的插件化功能最少要解决这些问题

  1. 宿主加载插件到进程中,并能调用插件的代码。
  2. activity不用在AndroidManifest注册,就能启动。因为插件是个独立的apk,所以插件的activity是没有在宿主里注册的。
  3. 资源冲突。下面细讲,这个也是最复杂的。
  4. view版本不同发生冲突。得先了解资源冲突,下面细讲

接下来讲下资源冲突。

  • 资源分为res和assets。一般都是res资源发生冲突。打包程序会将res里的所有资源赋予一个资源id,并生成资源索引表Resource.arsc,资源索引R.java。可以看到,Resource.arsc和R.java是相关联。如果插件用宿主的Resource.arsc,自己的R.java,就会发生冲突。或者插件用宿主的R.java,自己的Resource.arsc也会发生冲突。如果都用宿主的或者插件的才不会发生冲突。 下面来说下这两种冲突
  1. 插件用宿主的Resource.arsc,自己的R.java。如果不做任何特殊处理,直接启动插件的activity,那么插件访问资源时,就会发生这种冲突。因为R.java在打包就已经生成了,但Resource.arsc是在运行时加载的,系统启动的是宿主apk,所以加载的就是宿主的Resource.arsc。
  2. 插件用宿主的R.java,自己的Resource.arsc发生冲突。通过构造AssetManager对象,可以让插件用上自己的Resource.arsc。但插件有可能用到宿主的R.java。比如自定义属性,TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CustomView);R.styleable.CustomView是别的模块的,插件和宿主都用到了这个模块,比如系统提供的一些View。但R.styleable.CustomView在内存只有一份,默认是加载宿主的,所以这样也发生了冲突。又比如them,默认加载宿主application设置的them,这个them的值也是R.java里的一个索引。

版本不同的View发生冲突

  • 比如插件用的版本1的view,宿主用的版本2的view,虽然他们包名类名一样,但是用的资源不一样,插件的Resource.arsc和R.java对应的是版本1的view,宿主的对应版本2的view。因为LayoutInflater的sConstructorMap变量会通过类名缓存view,所以有可能插件加载到宿主用过的版本2的view,这样就发生了冲突。

下面讲解如何解决上面的四个问题?

  1. 宿主加载插件到进程中。apk可以通过classLoader加载,构建插件的classLoader就能加载插件的代码,或者更改宿主classLoader里的dexElements,把要加载的类放到这个数组的前面。我代码用到的方法是前者。

  2. activity不用在AndroidManifest注册,就能启动。两种解决方法,第一种hook启动activity的关键函数,欺骗AMW。第二种通过代理的方法,ProxyActivity在宿主注册,然后把PluginActivity当做普通类new出来,让ProxyActivity引用。这样ProxyActivity调用同名的PluginActivity函数,间接的实现了PluginActivit的调用。我用的是第一种hook方法,下面来说细讲下第一种方法。 首先讲下activity的启动流程

graph TD
startActivity --> AWM的代理AWP的startActivity --> AWM进程 --> ActivityThread进程 --> Instrumentation的newActivity --> 启动

检查activity是否注册是在AWM进程进行的,但是AWM没办法hook,我们可以hook AMP,然后伪造一个注册过的StubActivity欺骗AWM,然后hook Instrumentation的newActivity,将StubActivity还原回PluginActivity。

  1. 资源冲突有两种
  • Resource.arsc发生冲突。 activity加载资源是通过Resources中的AssetManager加载的。我们可以构造一个Resources和AssetManager,重写activity的getResources方法。这样插件就会用到自己的Resource.arsc。
  • R.java发生冲突,因为本质上是class类在内存只有一份,默认是加载宿主的,所以导致冲突。可以通过更改classLoader的双亲委派机制,让插件加载自己包内的class,不了解这个机制的可以去网上搜一下。所以我们只要把宿主的classLoader的parent改成插件的classLoader就可以了。但这样会出现一个问题,这样加载class会优先加载插件的,假如我宿主包和插件有一个相同的类,那么宿主加载这个类时,会加载插件到的类,这样我宿主就发生冲突了。解决办法就是把插件在另一个进程启动,这样插件更改了classLoader就不会影响到宿主包的。
  • them发生冲突,因为them这个索引已经变成了一个整型传给插件了,所以更改了classLoader也不行,直接在插件activity手动设置them就可以。setTheme(R.style.Theme_AppCompat);
  1. view版本不同发生冲突。因为插件在另一个进程启动了,也就不会有冲突了。

参考