一、什么是插件化
谈到插件化,很多人会想到热修复,热更新,组件化这些名词。最近有网友在热补丁介绍及Andfix的使用 看了这篇文章给我发私信问,想把Andfix集成在自身项目里面。因为阿里出品,而且比较火。其实我当时也有过这个考虑,后面真正深入后,发现其实还有很多坑,而且github上面也很久没有代码更新了。综合各种原因,后面放弃了。(这里并不是诋毁Andfix,因为我自己也投入很多精力去研究,我敬佩每一个开源贡献者!但是至少它确实还存在很多问题,现在还不适合)。其实准确的说,热修复应该算是插件化过程中的一种附属产物。另外,我们自己的项目中也开始组件化,组件化和插件化其实大体思路是差不多,但是还是有本质上的区别。组件化开发就是将一个项目app拆分成多个模块,每个模块都是一个组件,组件化开发过程中相互依赖或单独调试,最终发布的时候是将这些组件合并统一成一个apk。插件化开发也是将一个项目app拆分成多个模块,这些模块包括宿主和插件。每个模块相当于一个apk,而组件化相当于一个lib。最终发布的时候将宿主apk和插件apk单独打包或者联合打包。
二、插件化的作用
1.并发开发
当公司发展越来越大,项目越来越多,如何提高开发效率。这时插件化就将启动巨大的作用,就想前面说的,可以把开发人员分成很多组,每个组负责一个插件,彼此之间没有过多的依赖,可以单独调试打包。有时发版其实就相当于发插件。
2.动态更新插件或远端调试
app如果出现问题,不会像后台或者web那样,去改下线上的代码就可以了。因为app发版是在多个应用市场里面,你很难有权利或者很麻烦去应用市场把有bug的包替换。这时就需要插件。app每次启动回去校验是否有插件更新,有的话,就去服务器上下载最新的插件替换掉已有的。另外还有在实际工作会出现的场景,就是其他用户没有问题,某个用户,某个版本出现有问题,这时用户的情况开发人员大概知道什么问题,修复好之后,却找不到场景重现。这时可以通过给特定用户推送插件更新,已确保确实已经修复成功。
3.按需下载模块
app的设计,为了增加用户量,可能会偏向于“全功能”,但是有些用户又不需要太多功能,这时就可以按需下载。用户需要使用到的时候,才去下载相应模块。
4.方法数或变量数爆棚
Android应用方法数不能超过65k一直是硬伤,尤其是项目越来越大的情况。关于为什么方法数不能超过65k,自行谷歌。我就不过多介绍。插件化可以完美解决。想想如果有10个插件,每个插件60k的方法数,话说,你项目真的用得了这么多吗?
5.数据统计
主要是A/B test,比如产品经理同一套逻辑,可能有两种不同的交互体验,那究竟哪种用户体验更好呢?插件化可以给不同的用户更新不同的插件,然后去观察他们的相关数据。
三、Small
前面提到放弃了Andfix,那么插件化应该是以后的主流趋势,我该如何抉择?我查了很多资料,也单独去把插件化开源项目,一个个下载编译研究,里面的血泪就省略很多字。然后发现了Small,顿时仿佛看到了曙光!!先上一张图看看。
开源插件化项目对比
可以看到项目开发常用需要更新的地方,Small都支持,关于Service动态注册不支持,small作者的解释,我个人理解是因为sevice相关直接放在宿主apk里面就行了,service的更新不是特别频繁,没有必要花费精力去做不是特别有用的事情。也可以顺便减少代码量。关于AppCompat,是AndroidStudio默认添加的主题包,Google主推的Metrial Design包也依赖于此。未来的趋势。
其实前面同事提出是否考虑携程的DyAPK即DynamicAPK,因为携程的,毕竟大公司。我后面也确实去看了下。后面发现release版本没有,2个贡献者,从去年11低到现在大半年都没有更新维护过,而且我自己表示花了很长时间没有编过,或许是我自身的问题。反观Small,加了一个群,作者积极的帮助开发者去了解small,release版本也有,代码更新维护也快,另外small慢慢也得到更多人的青睐。代码的设计也很科学。我不是帮small作者打广告,他可能也不需要我打广告。我只是个人感受,实话实说。另外,对于携程的开源插件化,并不是一点用没有,里面有很多设计还是值得学习的,还有也算是对插件化的一种促进作用。
一、Small的原理
1.动态加载class
Android类由DexClassLoader 加载,如果直接在编译搜索这个类的时候出现下面这种情况,我开始以为是没有关联源码,还折腾了好一会,后面发现,应该是特意搜不到了。不过开源直接在https://android.googlesource.com 搜。请自备梯子,我一般是用lantern+谷歌浏览器。
DexClassLoader的构造方法里面调用了父类的构造方法,我们跟进去可以看到BaseDexClassLoader 里面有个dexPath参数,这个通常是应用储存apk的目录/data/../*.apk。现在用来作为创建pathList。我们再看看new DexPathList里面做了什么。
DexPathList里面调用makeDexElements方法创建dexElements。我们跟进去看看做了什么。
可以看到,DexClassLoader不支持".so"后缀。那么small是怎么做的呢,看看伪代码。
为了让应用启动时能自动复制插件包到应用存储目录,需要支持".so"后缀。做法就是模拟压缩包加载代码块,创建一个dex元素,再反射添加到宿主class loader里的dexPathList。后面的演示过程中,你会看到small更新的所谓插件,就是编译过后的so文件。
2.动态加载resources
安卓资源由AssetManager加载。应用启动时,系统会为其创建一个AssetManager实例,并由addAssetPath方法添加资源搜索路径,默认添加:"/framework/base.apk"- Android base resources (base),"/data/app/*.apk"- The launching apk resources (host)
所有的资源需要通过一个唯一的id来访问,通常是形如0xPPTTNNNN,每个字段PPT的那个图有相应的介绍,我就不多说了。那么如何处理这个资源id分配的问题呢?有下面几种方案
但是这几种方案都有问题,为什么有问题的方案还会提?我是觉得这样可以养成一种思考的能力。第一种会导致插件之间不能访问资源。2.1字段有限,不好维护;2.2插件间无法访问资源;2.3修改aapt源码,不好维护。如果修改aapt生成产物,无缝连接,支持极致剪裁。资源包进行重新打包,重设资源id,small就是采取这种方式。具体深入的实现,这里就不提了。感兴趣的可以继续查看源码看看。
3.动态注册activities
每一个activity由Activity的startActivityForResult方法启动,通过instrumentation的execStartActivity方法激活生命周期。这个地方请大家留意下,后面也会提到这里。
在ActivityThread的performLaunchActivity方法中通过instrumentation的newActivity方法实例化。
如果要动态注册Activity,首先在宿主manifest中注册一个命名特殊的占坑activity来欺骗mInstrumentation.execStartActivity以获得生命周期,再欺骗mInstrumentation.newActivity来获得插件activity实例。Small封装一个instrumentation,来替换掉宿主。可以看到small里面的占坑。
一、Small的使用
关于Small的使用,Small的使用 这里讲的很详细,关于这里提下我当时遇到的问题和解决办法。
1.Small作者应该用的是mac,推荐的是使用android studio的工程模板,上面的命令好像是不适应,至少对于我(使用windows),你得自己手动创建工程模板,就是把作者的那个模板复制到studio的工程模板配置下面,然后重启打开就可以看到。可以先谷歌下,不行的话,可以发消息和我说下。我帮忙看看。
2.如果选择自己手动创建项目的话,得留意第四步ConfigureUI route中,右键assets目录->New->File,新建bundle.json文件。对于Android Studio这样创建不会去解析assets目录的。应该在app模块下面的main目录,右键->New->Folder->AssetsFolder,新建assets目录。就是把assets目录换个位置。还有就是自己弄的时候,有问题可以多看看里面的issue和FAQ很多问题,一般已经有人帮你踩了。
3.Small项目需要编译的时候需要使用build lib,build bundle命令,我现在还不清楚为什么我使用命令不行,可能因为那个命令是针对mac。后面也不去深究了。直接在Gradle任务导航里运行,效果一样。
二、Small的升级和更新
这个是大家很关心的一个地方,Small的Demo里面有个功能测试升级,代码的位置如图所示,里面有个checkUpgrade()方法。
如果你直接使用demo的话,会看不出升级的效果的,因为uir是small作者自己的,基本都没有更新。后面将介绍如何升级更新!
1.更换成你自己服务器的地址,如下图所示,注释的是demo的url,下面的url是我自己的。你需要在bundles.json里面配置你要更新的插件pgk名字和下载的地址。下图是我自己的。
2.build lib,build bundle之后编译程序,在手机上运行一个。然后比如我的main插件有问题,我要更改。把代码更改好之后。一定要记得更改版本号。demo里面默认的是1。需要我们再app.main下面的build.gradle把versionCode由1更换成2,name由1.0换成1.1。
然后再build lib,build bundle之后,你会看到宿主app的smallLibs下面的armeabi有很多so文件,就是我们加载的。选择main.so文件放到你的服务器上面。在上面的那个demo里面点击Check upgrade按钮,等待更新完成。然后退出后台,重启。你会发现插件更新成功了!!!
三、看看更新的代码
主要调用checkUpgrade方法,我们看看requestUpgradeInfo
里面主要是通过http请求获取服务器bundles.json这个文件,然后去解析json.把信息存储到UpgradeInfo里面。mResponseHandler完成回调。
然后就是调用upgradeBundles方法。我们跟进去看看,这个方法具体做什么。
这个方法主要是校验服务器上bundles.json的信息,然后开始下载插件和加载插件。现在插件已经下载到你的应用储存目录了!!
四、Small的加载
原谅下好久没有使用UML画图工具了,很多概念都忘了,画了一个大概的流程图,方便直观的看。
五、Small的启动
代码我就不深入说了,感兴趣的同学,可以按照这个图一步步看看源码,Small作者写的挺不错的,很多地方值得学习和借鉴
七、插件化的问题
插件化的实施一定不是一个人的事情,这是一个团队的事情,很多东西都必须考虑好。这是查阅资料总结的几个问题。后面再继续讲解初步的方案。