“广告时间
宝沃汽车-智能系统部正在招聘Android开发工程师,感兴趣的可以发送简历到 liyilin7@borgward.com, 期待跟你成为同事!
“系列文章索引
并发系列:线程锁事
新系列:Android11系统源码解析
-
Android11源码分析:binder是如何实现跨进程的?(创作中)
-
Android11源码分析:SurfaceFlinger是如何对vsync信号进行分发的?(创作中)
经典系列:Android10系统启动流程
“前言
分析完了Android四大组件的源码,接下来我们来聊一个相关性很强的话题:插件化!
插件化复杂的地方在于,如果一个完整的apk想要用插件
的方式去加载到另一个Apk去,需要完成三个核心的需求
-
对插件中的代码(即class类)进行加载
-
对插件中的资源(即resource)进行加载
-
实现对四大组件的调用的支持
今天我们将围绕这三个核心的需求进行分析讲解
如何实现对插件类的加载?
插件类的加载
要实现对插件的类的加载,我们首先要清楚我们正常的应用的类是如何加载的
我们还是从熟悉的使用着手,在获取需要获取classloader时,需要调用context.getClassloader()
,查看ContextImpl
中的实现,发现其中调用了LoadedApk
的getClassLoader()
方法,其中根据ActivityThread
创建完成后生成的ApplicationInfo
获取对应的classloader,即根据APK
所在路径去解析加载对应的dex
这里我们需要明白的一点是,当我们在进程中可以调用我们所创建的类,以及系统的类对象,都是JVM通过classloader将静态编译生成的class类映射到内存才可以使用的,而通过classloader找到我们在dex中编译的类,即是我们现在要做的事情
Classloader的加载机制是现在父类中进行查找,找不到后会在当前的classloader中去进行查找
实现对插件类的加载可以采用两种方式:
-
隔离方式
要实现隔离方式,可以通过context.getClassLoader().getsuperClassloader()获取到顶级ClassLoader,当我们创建自定的DexClassLoader
传入插件的dex目录,并传入superclassloader,这样当我们创建多个插件的ClassLoader时,他们之间即是相互隔离的,不能相互访问
-
混合方式
混合方式即是使用context.getClassLoader()对象--即宿主的classloader--作为父加载器,创建我们插件的dexclassloader时将其作为parent参数传递进去,这样当我们使用这个插件的classloader时,既可以调用当前插件的类,也可以调用宿主中的类
这两种方式各有利弊,具体采用哪种方式可以根据业务需要去实现自己的加载机制
在我所经历的插件化开发中,由于业务场景并不复杂,且只有一个插件,所以使用的是第二种的混合模式
“小结
对于插件类的加载并不复杂,我们只需要拿到编译时生成的dex(或者用编译生成的apk也可以)文件,使用我们自己创建的DexclassLoader去进行加载即可
另外需要处理的问题就是与宿主类classloader的关系,如果采用混合模式,可以将宿主的classloader作为parent,如果需要隔离则可以将宿主classloader的顶级父类(即Bootclassloader)作为parent,这样每个插件及宿主都有自己独立的classloader去进行加载
如何实现对插件资源的加载?
加载插件资源
对于插件资源的加载,主要涉及的核心类有AssetsMananger
和Recources
,在使用时,我们可以通过宿主的context
调用getResources()
函数获取到Resources
对象
在Android5.0以上可以通过Resources
调用getAssets()
函数获取到AssetManager
对象,所以AssetsMananger
和Recources
是组合的关系()通过在Recources
中持有AssetsMananger
对象
关于插件资源的管理也有两种方案
-
混合方式
刚才已经提到,可以通过宿主的context对象获取到其对应的reources,并拿到AssetsMananger
,此时我们就可以调用其addAssetPath()
传入我们插件apk的路径,对插件的recoures资源进行加载
此处需要注意的是针对各个手机厂商的适配问题。由于各厂商会对Resources
类有自己的子类实现,比如小米的子类实现为MiuiResources
,因此在调用Resources
的addAssetPath()
函数时可能会出现调用失败的情况
这里需要针对不同厂商创建不同的Resources
对象,并将从宿主端获取到的AssetsMananger
作为参数传递给Resources
,实现宿主资源和插件资源的合并
当我们需要获取资源时,只需要通过我们新合并的Resources
对象即可获取宿主和插件的资源
-
插件资源独立
如果使用插件资源独立的方案,那只需要通过反射创建AssetsMananger
,并调用addAssetPath()
将APK目录中的资源加载进来,并创建Resources
对象持有该AssetsMananger
的引用
处理资源id冲突
在使用混合方式加载资源时,由于插件的编译和宿主编译都是单独编译的,因此可能会产生资源id冲突的问题
对资源进行编译的工具是aapt
,因此在最早的方案中,我们是使用修改aapt
源码的方式,使其在编译期生成id字段不同的插件resourse(默认编译id为0x7fxxxx
,只需要保证前面的id段不重复即可)
现在对资源id段的自定义已经是Gradle
插件自带的功能,远不需要修改aapt
源码那么麻烦,只需要在gradle文件中添加一下代码即可
aaptOptions {
additionalParameters "--allow-reserved-package-id", "--package-id"," 0x50"
}
“小结
对资源的加载主要涉及Resources
和AssetsMananger
,通过context.getAssets()
可以获取到对应的AssetsMananger
我们进行插件和宿主资源的合并即是通过同一个AssetsMananger
对象去调用addAssetPath()
添加插件资源实现的。另外需要注意的就是各个厂商的适配问题
到此,关于插件的类加载和资源加载方案就介绍完毕了
如何启动一个插件的Activitiy?
搞定了核心的功能--加载类和资源后,我们还希望在插件中能像正常的应用开发一样使用四大组件
现在我们就来分析下,如何启动一个插件的activity
流程简述
当我们需要启动一个普通的activity时,首先需要在清单文件中进行注册,然后调用context
中的startActivity()
,传递一个intent对象,设置我们需要启动的activity的class对象,这个class对象最终会被ActivityThread
中的Instrumentation
对象通过反射的方式进行实例化
要启动一个activity,唯一绕不开的一个点就是要在清单文件中进行注册,插件中的activity显然没有办法注册到清单文件当中,宿主也必然不会知道插件中会实现哪些activity对象
解决方案
解决方案其实就两个字:占坑
!
在宿主端进行activity预埋,我们称之为StubActivity
,在启动插件activity时,我们必然要传入插件activity的intent对象,此时AMS
对发现其并没有在清单文件注册,肯定会抛出异常。
所以我们此时需要将intent对象中的activity替换成占坑的activity
,并将原来的intent中的内容(即插件activity对应的报名和类名)保存到intent中,并标记当前的intent为插件的intent
当AMS执行完一系列流程后,在通过binder调用执行到ActivityThread中的mH的LAUNCH_ACTIVITY
消息分支进行处理,最终通过Instrumentation
去反射创建activity对象
在这个过程中,我们需要把占坑的activity再替换会我们真正需要启动的activity对象。
具体实现来说,可以通过反射获取ActivityThread
mH,并为mH设置mCallback回调(handler消息处理时,会先处理回调消息),这样我们就可以拿到AMS通过binder调用执行到ActivityThread的代码,即通过mH
对LAUNCH_ACTIVITY
消息进行处理,将此时的intent内容替换成我们需要启动的插件activity的内容,随后再通过Instrumentation
反射创建Activity对象时,即创建了我们的插件activity!
“小结
以上所述,一图以蔽之:
后记
其实关于四大组件的插件化需求是锦上添花的功能
在简单的业务场景中,我们往往只需要申请一个activity,在这一个activity中去写我们的业务逻辑,我们完全可以避开对activity的使用,通过Dialog获取popwindow的方式去进行开发
好处是简化了插件化方案,同时使我们的插件开发量级更轻
。通过前面四大组件的源码分析,相信大家一定发现了,四大组件之所以大
,正是由于他们的量级很重,需要通过系统服务处理他们的启动和生命周期逻辑
坏处是开发上可能会有一些繁琐和学习成本
具体如何取舍,就取决于各位真实的业务场景需求了
你的点赞是我创作的最大动力,请多多点赞支持哦!
“参考链接