作者:newki
转载地址:https://juejin.cn/post/7098915987311263781
对于整个Android开发来说,整体的开发架构离不开三化这个名词:模块化,组件化,插件化。
下面一起来看看它们是如何一步一步演变成今天的模样。
一. 模块化
软件开发不管是哪一个方向,大家都知道高内聚,低耦合的思想。
模块化也是每一门语言,各个方向的开发者都会处理的问题。在Android开发方向来说,模块化就是把常用的UI,网络请求,DB操作,第三方服务等公共的部分抽取封装成模块,方便复用。
再复杂一点,就是把大的业务拆分为小的业务模块,小的业务模块又依赖于公共的基础模块。打包成Apk的时候又会将不同的模块组合为一个整体,打包完成一个项目。看到这里,是不是就有一点组件化的雏形了。
模块化的好处和弊端
好处:
- 复用,基础业务为业务模块复用,子业务为父业务复用。
- 解耦,降低模块间的耦合,避免出现一个功能修改,需要到处找代码的尴尬局面。
- 协作,团队的人员多了之后,不同的人员维护不同的模块,同步开发效率更高。
好处说破了天,它还是在单一的项目架构下,业务越来越复杂,业务模块越来越多,开发人员越来越多,模块化出现了一些问题。 弊端:
- 编译慢,随着代码量越来越大,修改一个位置都需要几分钟甚至是十几分钟的编译,极大的降低开发效率。
- 耦合,业务模块越来越多之后,小模块的功能修改,都需要对修改的代码耦合进行严谨的测试。
- 冲突,开发人员不了解其他模块的功能,修改其中代码导致其他模块出问题,提交代码也容器出现冲突。
由此项目组件化走上舞台并成为当前开发项目的主流架构。
二. 组件化
组建化的核心思想就是把业务模块封装成一个一个的组件,可以供宿主依赖,也可以独立运行。而各个不同的组件由于是在不同的项目中,则通过路由来通信。 组件化的结构如下:
各模块的跳转和业务通信通过路由转发:
组件的配置一般分为两种方案: 方案一 在项目的build.gradle文件中配置是否作为application
/**
* module开关统一声明在此处
* true:module作为application,可单独打包为apk
* false:module作为library,可作为宿主application的组件
*/
isNorthModule = false
isSouthModule = false
然后在组件的build.gradle中配置plugin和AndroidManifest的路径:
if (Boolean.valueOf(rootProject.ext.isModule_North)) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
/*
* java插件引入了一个概念叫做SourceSets,通过修改SourceSets中的属性,
* 可以指定哪些源文件(或文件夹下的源文件)要被编译,
* 哪些源文件要被排除。
* */
sourceSets {
main {
if (Boolean.valueOf(rootProject.ext.isModule_North)) {//apk
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
//library模式下,排除java/debug文件夹下的所有文件
exclude '*module'
}
}
}
}
方案二 组件就是Lib,想要独立运行模块,则建立对应的runalone模块为application再依赖指定的模块。无需配置开关,无需编译。 大致项目结构如下:
运行的时候可以选择运行哪一个模块:
组件化的出现,解决了模块化的痛点。带来如下优势:
- 加快业务迭代速度,各个业务模块组件更加独立,不再出现业务耦合情况。
- 稳定的公共模块采用依赖库方式,提供给各个业务线使用,减少重复开发和维护工作量。
- 迭代频繁的业务模块采用组件方式,各业务研发可以互不干扰、提升协作效率,并控制产品质量。
- 为新业务随时集成提供了基础,所有业务可上可下,灵活多变。
- 降低团队成员熟悉项目的成本,降低项目的维护难度。
- 加快编译速度,提高开发效率。
- 控制代码权限,将代码的权限细分到更小的粒度。
PS.这里只是简单了解一下组件化的架构,关于组件化,后面我会出一期单独讲一下。
组件化已经如此方便,但是在快速迭代的今天,如果出现Bug修复,出了新功能急需上线,还要等待应用商店审核才能上线,实在是让各大厂难受,也是由于对业务组件/模块的热更新提出了更高的要求,由此插件化登上了舞台。
二. 插件化
插件化的本质就是加载一些没有注册到本地apk的一些插件代码。
插件化是2012年提出到2014年成熟,至今插件化已经迭代几代了。技术更成熟了。主要分为2个流派 代理派和Hook派
以RePlugen和Shadow为首的代理派。 以DroidPlugin为首的Hook派。
代理派的原理: DexClassLoader加载插件dex代码。反射生成AssetManager实例,调用 Resources构造函数生成实例。使用接口的方式手动回调插件的生命周期。 伪代码如下: 生成DexClassLoader:
// 从 assets 中拿出插件 apk 放到内部存储空间
private fun extractPlugin() {
var inputStream = assets.open("plugin.apk")
File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
}
private fun init() {
extractPlugin()
pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
// 生成 DexClassLoader 用来加载插件类
pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
}
处理Resource:
private fun handleResources() {
try {
// 首先通过反射生成 AssetManager 实例
pluginAssetManager = AssetManager::class.java.newInstance()
// 然后调用其 addAssetPath 把插件的路径添加进去。
val addAssetPathMethod = pluginAssetManager?.javaClass?.getMethod("addAssetPath", String::class.java)
addAssetPathMethod?.invoke(pluginAssetManager, pluginPath)
} catch (e: Exception) {
}
// 调用 Resources 构造函数生成实例
pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration)
}
通过接口调用插件Activity生命周期:
interface IPluginActivity {
fun attach(proxyActivity: Activity)
fun onCreate(savedInstanceState: Bundle?)
fun onStart()
fun onResume()
fun onPause()
fun onStop()
fun onDestroy()
}
复制代码
插件Activity实现接口
open class BasePluginActivity : Activity(), IPluginActivity {
var proxyActivity: Activity? = null
override fun attach(proxyActivity: Activity) {
this.proxyActivity = proxyActivity
}
override fun onCreate(savedInstanceState: Bundle?) {
if (proxyActivity == null) {
super.onCreate(savedInstanceState)
}
}
// ...
}
在 StubActivity 通过接口调用插件 Activity 生命周期
open class StubBaseActivity : Activity() {
protected var activityClassLoader: ClassLoader? = null
protected var activityName = ""
private var pluginPath = ""
private var pluginAssetManager: AssetManager? = null
private var pluginResources: Resources? = null
private var pluginTheme: Resources.Theme? = null
private var nativeLibDir: String? = null
private var dexOutPath: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
pluginPath = intent.getStringExtra("pluginPath")
activityName = intent.getStringExtra("activityName")
// 生成插件 ClassLoader
activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
// 加载插件 Activity 类并转化成 IPluginActivity 接口
activity = activityClassLoader?.loadClass(activityName)?.newInstance() as IPluginActivity?
activity?.attach(this)
// 通过接口直接调用对应的生命周期方法
activity?.onCreate(savedInstanceState)
handleResources()
}
override fun getResources(): Resources? {
// 这里返回插件的资源,这样插件 Activity 中使用的就是插件资源了
return pluginResources ?: super.getResources()
}
override fun getAssets(): AssetManager {
return pluginAssetManager ?: super.getAssets()
}
override fun getClassLoader(): ClassLoader {
return activityClassLoader ?: super.getClassLoader()
}
private fun handleResources() {
try {
// 生成 AssetManager
pluginAssetManager = AssetManager::class.java.newInstance()
// 添加插件 apk 路径
val addAssetPathMethod = pluginAssetManager?.javaClass?.getMethod("addAssetPath", String::class.java)
addAssetPathMethod?.invoke(pluginAssetManager, pluginPath)
} catch (e: Exception) {
}
// 生成插件资源
pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration)
}
}
而腾讯Shadow和上面的有点区别,它做了一个非常简单事,通过运用AOP思想,利用字节码编辑工具,在编译期把插件中的所有Activity的父类都改成一个普通类,然后让壳子持有这个普通类型的父类去转调它就不用Hack任何系统实现了。虽然说是非常简单的事,实际上这样修改后还带来一些额外的问题需要解决,比如getActivity()方法返回的也不是Activity了。不过Shadow的实现中都解决了这些问题。
Hook派的原理: 先了解Activity的启动流程
Activity.startActivity -> Instrumentation.execStartActivity -> Binder -> AMS.startActivity -> ActivityStarter.startActivityMayWait
-> startActivityLocked -> startActivityUnChecked -> ActivityStackSupervisor.resumeFocusedStackTopActivityLocked
-> ActivityStatk.resumeTopAcitivityUncheckLocked -> resumeTopActivityInnerLocked -> ActivityStackSupervisor.startSpecificActivityLocked
-> realStartActivityLocked -> Binder -> ApplictionThread.scheduleLauchActivity -> H -> ActivityThread.scheduleLauchActivity
-> handleLaunchActivity -> performLaunchActivity -> Instrumentation.newActivity 创建 Activity -> callActivityOnCreate 一系列生命周期
我们可以把 AMS 理解为一个公司的背后「大 Boss」,Activity 相当于「小职员」,没有权限直接和大 Boss 说话,想做什么事情都必须经过「秘书」向上汇报,然后秘书再把大 Boss AMS 的命令传达下来。而且大 Boss 那里有所有职员的名单,如果想要混入非法职员时不可能的。而我们想让没有在大 Boss 那里注册的编外人员执行任务,只有两种方法,一种是正式职员领取任务,再分发给编外人员,另一种就是欺骗 Boss,让 Boss 以为这个职员是已经注册的。
对应到实际的解决方法就是:
1.我们手动去调用插件 Activity 的生命周期。
2.欺骗系统,让系统以为 Activity 是注册在 Manifest 中的。
大致步骤:实现一个我们自己的 Instrumentation
class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) :
Instrumentation() {
private val KEY_COMPONENT = "commontec_component"
companion object {
fun inject(activity: Activity, pluginContext: PluginContext) {
// hook 系统,替换 Instrumentation 为我们自己的 AppInstrumentation,Reflect 是从 VirtualApp 里拷贝的反射工具类,使用很流畅~
var reflect = Reflect.on(activity)
var activityThread = reflect.get<Any>("mMainThread")
var base = Reflect.on(activityThread).get<Instrumentation>("mInstrumentation")
var appInstrumentation = AppInstrumentation(activity, base, pluginContext)
Reflect.on(activityThread).set("mInstrumentation", appInstrumentation)
Reflect.on(activity).set("mInstrumentation", appInstrumentation)
}
}
override fun newActivity(cl: ClassLoader, className: String, intent: Intent): Activity? {
// 创建 Activity 的时候会调用这个方法,在这里需要返回插件 Activity 的实例
val componentName = intent.getParcelableExtra<ComponentName>(KEY_COMPONENT)
var clazz = pluginContext.classLoader.loadClass(componentName.className)
intent.component = componentName
return clazz.newInstance() as Activity?
}
private fun injectIntent(intent: Intent?) {
var component: ComponentName? = null
var oldComponent = intent?.component
if (component == null || component.packageName == realContext.packageName) {
// 替换 intent 中的类名为占位 Activity 的类名,这样系统在 Manifest 中查找的时候就可以找到 Activity
component = ComponentName("com.zy.commontec", "com.zy.commontec.activity.hook.HookStubActivity")
intent?.component = component
intent?.putExtra(KEY_COMPONENT, oldComponent)
}
}
fun execStartActivity(
who: Context,
contextThread: IBinder,
token: IBinder,
target: Activity,
intent: Intent,
requestCode: Int
): Instrumentation.ActivityResult? {
// 启动 activity 的时候会调用这个方法,在这个方法里替换 Intent 中的 ClassName 为已经注册的宿主 Activity
injectIntent(intent)
return Reflect.on(base)
.call("execStartActivity", who, contextThread, token, target, intent, requestCode).get()
}
// ...
}
在 AppInstrumentation 中有两个关键点,「execStartActivity 和 newActivity」。「execStartActivity」 是在启动 Activity 的时候必经的一个过程,这时还没有到达 AMS,所以,在这里把 Activity 替换成宿主中已经注册的 StubActivity,这样 AMS 在检测 Activity 的时候就认为已经注册过了。「newActivity」 是创建 Activity 实例,这里要返回真正需要运行的插件 Activity,这样后面系统就会基于这个 Activity 实例来进行对应的生命周期的调用。
因为我们 hook 了 Instrumentation 的实现,还是把 Activity 生命周期的调用交给了系统,所以我们的资源处理方式和手动调用生命周期不太一样,这里我们生成 Resources 以后,直接反射替换掉 Activity 中的 mResource 变量即可。下面是具体代码:
class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) : Instrumentation() {
private fun injectActivity(activity: Activity?) {
val intent = activity?.intent
val base = activity?.baseContext
try {
// 反射替换 mResources 资源
Reflect.on(base).set("mResources", pluginContext.resources)
Reflect.on(activity).set("mResources", pluginContext.resources)
Reflect.on(activity).set("mBase", pluginContext)
Reflect.on(activity).set("mApplication", pluginContext.applicationContext)
// for native activity
val componentName = intent!!.getParcelableExtra<ComponentName>(KEY_COMPONENT)
val wrapperIntent = Intent(intent)
wrapperIntent.setClassName(componentName.packageName, componentName.className)
activity.intent = wrapperIntent
} catch (e: Exception) {
}
}
override fun callActivityOnCreate(activity: Activity?, icicle: Bundle?) {
// 在这里进行资源的替换
injectActivity(activity)
super.callActivityOnCreate(activity, icicle)
}
}
public class PluginContext extends ContextWrapper {
private void generateResources() {
try {
// 反射生成 AssetManager 实例
assetManager = AssetManager.class.newInstance();
// 调用 addAssetPath 添加插件路径
Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
method.invoke(assetManager, pluginPath);
// 生成 Resources 实例
resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
}
}
虽然插件化的原理大家都了解了,但是实战中还是推荐大家使用开源框架来实现如以RePlugen Shadow DroidPlugin 等。各个框架各有利弊,还望大家体验之后再做选择。
四. 总结
总的来说,目前的开发是以组件化开发为主,组件内部的功能也离不开模块化的思想,在组件化的结构上,很方便的实现插件化,以各个业务组件为一个一个的单独插件,方便修复问题上线新功能。组合起来,这也是当前主流的Android开发架构了。