插件化摘要

参照《Android 插件化开发指南》做的摘要。

将某一模块打包成 APK 作为插件,在宿主 APP 中根据需要下载并加载。

加载插件Dex

  1. 根据插件的 dex 文件路径为每一个插件 DexClassLoader,宿主 APP 使用相应插件的 DexClassLoader 加载类来反射使用,插件中怎么加载类?

  2. 把宿主 APP 和所有插件的 dex 文件合到一个 DexClassLoader 中,即反射宿主APP的 BaseDexClassLoader 中的 PathList 的 dexElement 数组,把插件的dex构建成 Element 插入数组中,这样就可以同时加载宿主和插件中的类了;

  3. 把系统的 ClassLoader 替换为我们自己写的 TestClassLoader,在 TestClassLoader 保存了插件和宿主的所有 ClassLoader,加载类的时候依次遍历每个 ClassLoader,直到找到一个能加载类的 ClassLoader。

加载插件资源

访问资源都是通过 AssetManager 来进行的

  1. 为每一个插件创建一个 AssetManager,并通过反射调用 addAssetPath 方法添加资源,然后将插件中的 AssetManager 替换为新创建的 AssetManager即可;宿主 APP 要使用插件中的资源就通过该插件新创建的 AssetManager 来读取;

  2. 将所有插件的资源与宿主 APP 的资源通过 addAssetPath 合并到一个 AssetManager 中,插件和宿主APP共用这个AssetManager,同时要处理好资源冲突的问题;

因为系统默认的 id 是以 0x7f 开头的,所以我们只要修改插件中的资源 id 不以 0x7f 开头即可解决资源冲突,具体的办法有以下两种:

  1. 修改 Android 打包流程中使用到的 AAPT 命令,为插件资源指定如 0x71 之类的 id 即可;

  2. 仍然是修改插件资源 id,只不过改为在 Android 打包生产 resource.arsc 文件(保存了apk中所有的资源名称和对应的id值)之后,对这个 resource.arsc 文件进行修改。

加载插件so库

加载插件so库是跟生成 DexClassLoader 的方案搭配使用的:

  • 当为每个插件单独创建一个 DexClassLoader 时,可以将插件中的so库文件路径通过 DexClassLoader 的构造函数传入,然后在插件中即可直接通过 System.load 加载对应文件名的so库

  • 当插件和宿主共用一个 DexClassLoader 时,将插件的so库复制到应用的某个目录,然后插件即可通过 System.loadLibrary 去动态加载对应文件路径的so库

注意:一般建议只使用一个目录的so库如: armeabi-v7a,当宿主APP中没有so库时,需要创建一个简单的so文件放在 armeabi-v7a 目录下,然后再应用中通过 System.load 去加载,这样加载插件时系统就知道插件中的so库也是 armeabi-v7a 架构的,需要用 32 位的虚拟机,这个属于占位思想。

四大组件插件化

程序安装的时候 PMS 对解析 AndroidManifest 文件中注册的四大组件,保存在 AMS 中,当启动四大组件时 AMS 会校验是否已注册。

Activity 插件化解决方案
  1. 动态替换方案:预先定义一个占位 StubActivity,然后对 Activity 的启动流程进行 Hook,主要的 Hook 点是 Instrumentation 和 H 类的 mCallback 字段进行 Hook。Hook 之后就能在启动插件 Activity 的时候动态替换为占位 Activity 瞒过 AMS 的校验。对于其它非标准的三种启动模式,需要预先注册很多相应启动模式的占位 Activity,此时占位 Activity 和插件中的 Activity 只能是一一对应,不能像标准模式那样一对多,注意理解。同时对于 singleTask 和 singleTop 要注意对 onNewIntent 的回调进行Hook 替换为插件 Activity 的 onNewIntent。一一对应关系可以存放在服务器或者加载插件时从插件中读取,例如放在插件的 Assets 文件中。

  2. 基于静态代理的插件化方案(that框架):在主APP中设计一个代理类 ProxyActivity,这是一个 Activity,它是所有插件 Activity 的代理,想打开插件中的任何一个页面,都是打开 ProxyActivity,只是传递的参数(传递的是插件的路径和插件 Activity 的路径)不一样。ProxyActivity 和插件中的 Activity 相互持有对方的引用,ProxyActivity 在回调生命周期的时候会掉插件 Activity 的同名函数,而插件 Activity 则会使用 ProxyActivity 的引用 -> that 来做 View的初始化已经一些其它依赖 Activity 的操作。
    在插件 Activity 中可以重写接口和方法封装 that 调用,不过对于 final 类型的方法只能使用 that.setResult 这样来调用了,同时可以将可封装的方法提取为接口,放入公共 library 来引用,则 ProxyActivity 反射插件 Activity 的时候就可以直接当做接口来调用了,而不是需要一个个方法去反射。代理方案对于启动模式的解决办法就是设计一个单例栈 CJBackStack,用来装载所有打开的插件 Activity。

    • Standard 模式:不需要处理
    • SingleTop 模式:看 CJBackStack 中倒数第二个是否是将要打开的插件 Activity,如果是,那就把倒数第二个从 CJBackStack 栈中删除掉,同时执行倒数第二个元素的 finish 方法,相当于把这个 Activity 关掉了。(这里并没有重用呢,只是把它删掉了新建一个,系统的模式是直接置栈顶重用的)
    • SingleTask 模式: 跟 SingleTop 差不多,只是会遍历所有的插件 Activity,找到相同的就把它以及在它之上的其它 Activity 都删除。
    • SingleInstance 模式:会遍历所有的插件 Activity,找到相同的就把它删除掉。

注意:当插件 Activity 和宿主APP的 Activity 相互混在一起时,上面的启动模式方案就无法奏效了,因为 CJBackStack 并没有对宿主APP的 Activity 做管理。这个方案我不太喜欢,缺点比较多,写代码也比较麻烦,侵入性太高。

Service 插件化解决方案
  1. 动态替换方案:由于 Service 跟 Activity 不同,多次调用 startService 并不会启动多个 Service 实例,所以占位 StubService 必须和插件中的 Service 一一对应,好在一般APP的 Service 并不会很多,基本预定义10个 StubService 即可。对应关系统一可以实时从服务器拉取或者从插件的读取。注意启动 Service 有 startService 和 bindService 两种方式,所以要分别根据他们的启动流程进行 Hook,然后动态替换。

  2. 基于静态代理的插件化方案(that框架):其实跟 Activity 的思想类似,不过 Service 需要注意 BindService 和 StartService 两种情况。不同的是代理的方案有两种:一种是预先定义多个 ProxyService,然后根据传入的插件服务路径一一对应进行代理;另一种是只是用一个 ProxyService ,然后根据不同路径调用不同插件的 Service,不过我觉得这种方案属于把所有服务集中到一个里面,会有性能瓶颈。

BroadcastReceiver 插件化解决方案

广播分为动态和静态注册,对于动态的并不影响使用。而对于静态的,由于 PMS 在应用安装的时候就读取了 AndroidManifest 把静态广播解析出来注册,由于这个注册是在 PMS 上的(动态的是注册在 AMS 上的),所以应用进程没有启动也能收到广播。

由于插件的 AndroidManifest 文件并不会被读取,所以加载插件的时候需要通过反射系统的 PackageParser 对插件的 AndroidManifest 进行解析,将解析出来的静态广播当做一个接口。注意要连 Action 也解析出来,因为后面分发要用。
在宿主APP中静态注册 ProxyReceiver 广播作为中转站,统一接受发给插件的静态广播,然后在 ProxyReceiver 中根据 Action 调用对应的插件 Receiver 的 onReceiver 方法。不过这样子可能需要为 ProxyReceiver 配置几百个 Action。

ContentProvider 插件化解决方案

ContentProvider的插件化跟广播的基本类似,也是在宿主APP中配置一个 StupContentProvider 作为中转站,根据传入的不同 URL 调用相应插件的 ContentProvider。

公共库混淆

  1. 方案一:对于插件和宿主都引用的公共库 CommonLibrary 不进行混淆,插件的引用使用 provided ,这样打包就不会包括公共库

  2. 方案二:混淆公共库 CommonLibrary,使用 multidex 的手动拆包技术,将插件 Plugin 拆成两个包,Plugin 的代码都放在主 dex中,而其他代码都放在 classes2.dex 中,包括 CommonLibrary。然后用一个空的 classes2.dex 文件,替换插件 Plugin 中的 classes2.dex。最后,让 Plugin 和 宿主 APP 使用相同的混淆规则(Plugin 混淆公共库的规则会存放在 mapping.txt 中,将它配置到宿主APP混淆文件中即可共用混淆规则)。

降级

每一个原生页面都有对应的 HTML5 页面,当原生页面出现 bug 时可以临时切换到 HTML5 页,并实时修复页面 bug ,带到原生页面更新后在切回来。不过工作量会很大,页面之前的交互也会很麻烦。

APP 打包流程

  1. aapt:为 res 目录下的资源生成 R.java 文件,同时为 AndroidManifest.xml 生成 Manifest.java 文件;
  2. aidl:把项目中自定义的 aidl 文件生成相应的 Java 代码文件;
  3. javac:把项目中所有的 Java 代码编译成 class 文件,包括三部分 Java 代码,自己写的业务逻辑, aapt 生成的 Java 文件,aidl 生成的 Java 文件;
  4. proguard:混淆同时生成 proguardMapping.txt。这一步是可选的;
  5. dex:把所有的 class 文件(包括第三方库的 class 文件)转换成 dex 文件;
  6. aapt:还是使用 aapt,这里使用它的另一个功能,即打包,把 res 目录下的资源、assets 目录下的文件,打包成一个 .ap_ 文件;
  7. apkbuilder:将所有的 dex 文件、ap_ 文件、AndroidManife.xml 打包为 .apk 文件,这是一个未签名的 apk 包;
  8. jarsigner:对 apk 进行签名;
  9. zipalign:对要发布的 apk 文件进行对齐操作,以便在运行时节省内存。

你可能感兴趣的:(插件化摘要)