阅读 1416

【再出发】插件化探索:插件Activity是如何启动的?

系列文章索引

并发系列:线程锁事

  1. 篇一:为什么CountDownlatch能保证执行顺序?

  2. 篇二:并发容器为什么能实现高效并发?

  3. 篇三:从ReentrientLock看锁的正确使用姿势

新系列:Android11系统源码解析

  1. Android11源码分析:Mac环境如何下载Android源码?

  2. Android11源码分析:应用是如何启动的?

  3. Android11源码分析:Activity是怎么启动的?

  4. Android11源码分析:Service启动流程分析

  5. Android11源码分析:静态广播是如何收到通知的?

  6. Android11源码分析:binder是如何实现跨进程的?(创作中)

  7. 番外篇 - 插件化探索:插件Activity是如何启动的?

  8. Android11源码分析: UI到底为什么会卡顿?

  9. Android11源码分析:SurfaceFlinger是如何对vsync信号进行分发的?(创作中)

经典系列:Android10系统启动流程

  1. 源码下载及编译

  2. Android系统启动流程纵览

  3. init进程源码解析

  4. zygote进程源码解析

  5. systemServer源码解析

前言

分析完了Android四大组件的源码,接下来我们来聊一个相关性很强的话题:插件化!

插件化复杂的地方在于,如果一个完整的apk想要用插件的方式去加载到另一个Apk去,需要完成三个核心的需求

  1. 对插件中的代码(即class类)进行加载
  2. 对插件中的资源(即resource)进行加载
  3. 实现对四大组件的调用的支持

今天我们将围绕这三个核心的需求进行分析讲解

如何实现对插件类的加载?

插件类的加载

要实现对插件的类的加载,我们首先要清楚我们正常的应用的类是如何加载的

我们还是从熟悉的使用着手,在获取需要获取classloader时,需要调用context.getClassloader(),查看ContextImpl中的实现,发现其中调用了LoadedApkgetClassLoader()方法,其中根据ActivityThread创建完成后生成的ApplicationInfo获取对应的classloader,即根据APK所在路径去解析加载对应的dex

这里我们需要明白的一点是,当我们在进程中可以调用我们所创建的类,以及系统的类对象,都是JVM通过classloader将静态编译生成的class类映射到内存才可以使用的,而通过classloader找到我们在dex中编译的类,即是我们现在要做的事情

Classloader的加载机制是现在父类中进行查找,找不到后会在当前的classloader中去进行查找

实现对插件类的加载可以采用两种方式:

  1. 隔离方式

要实现隔离方式,可以通过context.getClassLoader().getsuperClassloader()获取到顶级ClassLoader,当我们创建自定的DexClassLoader传入插件的dex目录,并传入superclassloader,这样当我们创建多个插件的ClassLoader时,他们之间即是相互隔离的,不能相互访问

  1. 混合方式

混合方式即是使用context.getClassLoader()对象--即宿主的classloader--作为父加载器,创建我们插件的dexclassloader时将其作为parent参数传递进去,这样当我们使用这个插件的classloader时,既可以调用当前插件的类,也可以调用宿主中的类

这两种方式各有利弊,具体采用哪种方式可以根据业务需要去实现自己的加载机制

在我所经历的插件化开发中,由于业务场景并不复杂,且只有一个插件,所以使用的是第二种的混合模式

小结

对于插件类的加载并不复杂,我们只需要拿到编译时生成的dex(或者用编译生成的apk也可以)文件,使用我们自己创建的DexclassLoader去进行加载即可

另外需要处理的问题就是与宿主类classloader的关系,如果采用混合模式,可以将宿主的classloader作为parent,如果需要隔离则可以将宿主classloader的顶级父类(即Bootclassloader)作为parent,这样每个插件及宿主都有自己独立的classloader去进行加载

如何实现对插件资源的加载?

加载插件资源

对于插件资源的加载,主要涉及的核心类有AssetsManangerRecources,在使用时,我们可以通过宿主的context调用getResources()函数获取到Resources对象

在Android5.0以上可以通过Resources调用getAssets()函数获取到AssetManager对象,所以AssetsManangerRecources是组合的关系()通过在Recources中持有AssetsMananger对象

关于插件资源的管理也有两种方案

  1. 混合方式

刚才已经提到,可以通过宿主的context对象获取到其对应的reources,并拿到AssetsMananger,此时我们就可以调用其addAssetPath()传入我们插件apk的路径,对插件的recoures资源进行加载

此处需要注意的是针对各个手机厂商的适配问题。由于各厂商会对Resources类有自己的子类实现,比如小米的子类实现为MiuiResources,因此在调用ResourcesaddAssetPath()函数时可能会出现调用失败的情况

这里需要针对不同厂商创建不同的Resources对象,并将从宿主端获取到的AssetsMananger作为参数传递给Resources,实现宿主资源和插件资源的合并

当我们需要获取资源时,只需要通过我们新合并的Resources对象即可获取宿主和插件的资源

  1. 插件资源独立

如果使用插件资源独立的方案,那只需要通过反射创建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"
}
复制代码

小结

对资源的加载主要涉及ResourcesAssetsMananger,通过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对象。

具体实现来说,可以通过反射获取ActivityThreadmH,并为mH设置mCallback回调(handler消息处理时,会先处理回调消息),这样我们就可以拿到AMS通过binder调用执行到ActivityThread的代码,即通过mHLAUNCH_ACTIVITY消息进行处理,将此时的intent内容替换成我们需要启动的插件activity的内容,随后再通过Instrumentation反射创建Activity对象时,即创建了我们的插件activity!

小结

以上所述,一图以蔽之:

后记

其实关于四大组件的插件化需求是锦上添花的功能

在简单的业务场景中,我们往往只需要申请一个activity,在这一个activity中去写我们的业务逻辑,我们完全可以避开对activity的使用,通过Dialog获取popwindow的方式去进行开发

好处是简化了插件化方案,同时使我们的插件开发量级更。通过前面四大组件的源码分析,相信大家一定发现了,四大组件之所以,正是由于他们的量级很重,需要通过系统服务处理他们的启动和生命周期逻辑

坏处是开发上可能会有一些繁琐和学习成本

具体如何取舍,就取决于各位真实的业务场景需求了

你的点赞是我创作的最大动力,请多多点赞支持哦!

参考链接

  1. weishu.me
  2. VirtualApk
文章分类
Android
文章标签