面试过程中;问到关于组件化+插件化的问题;这一篇就够了
Posted 初一十五啊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试过程中;问到关于组件化+插件化的问题;这一篇就够了相关的知识,希望对你有一定的参考价值。
前言
之前有小伙伴提到在某大厂面试了4轮面试,其中两轮都被问到组件化和插件化的问题,问到的虽然各不相同,但是所有的问题都汇集到组件化和插件化的问题。
哪今天来总结一下,首先看一下组件化的内容,提纲如下:
(文字+视频版)
android
知识点大全。(音视频;Flutter
,kotlin
;Compose
;Framework
;性能优化;架构等)全文30W+字。200多个知识点,330张图。PDF版。
💡一丶组件化
1.1.组件化和模块化的区别
组件化就是把可以复用的、独立的、基础的、功能专一的代码封装到一个方法或者代码片段里,在未来需要的地方引入使用。用极少的代码实现之前相同的功能,避免了相同功能代码的复写,提高了开发的效率。在未来对改组件功能进行修改的时候只需要修改组件代码就可修改项目里所有的相同功能。组件化属于纵向分块,每个组件就像一个竖直的线永不相交。
模块化是为了单独实现某一功能模块进行封装的方法,一个模块里可能拥有n个基础组件搭配产生。模块化属于横向分块,每个模块像一条横向把n条竖直的线串联起来形成一个整体。
组件相当于库,模块相当于框架。
对比
1.2.组件之间的跳转和组件通信原理机制
如果一个单体项目进行组件化架构改造,应从以下方面入手
- 代码解耦
- 组件单独运行
- 组件间通信
- UI跳转
- 组件生命周期
- 调试
- 代码隔离
众所周知,Android
提供了很多不同的信息的传递方式,比如在四大组件中本地 广播、进程间的 AIDL
、匿名间的内存共享、Intent Bundle
传递等等,那么在这 么多传递方式,哪种类型是比较适合组件与组件直接的传递呢。
-
本地广播,也就是
LoacalBroadcastRecevier
。更多是用在同一个应用内的不同系 统规定的组件进行通信,好处在于:发送的广播只会在自己的 APP 内传播,不 会泄漏给其他的APP
,其他APP
无法向自己的APP
发送广播,不用被其他APP
干扰。本地广播好比对讲通信,成本低,效率高,但有个缺点就是两者通信机制 全部委托与系统负责,我们无法干预传输途中的任何步骤,不可控制,一般在组 件化通信过程中采用比例不高。 -
进程间的
AIDL
。这个粒度在于进程,而我们组件化通信过程往往是在线程中, 况且AIDL
通信也是属于系统级通信,底层以Binder
机制,虽说Android
提供模 板供我们实现,但往往使用者不好理解,交互比较复杂,往往也不适用应用于组 件化通信过程中。 -
匿名的内存共享。比如用
Sharedpreferences
,在处于多线程场景下,往往会线 程不安全,这种更多是存储一一些变化很少的信息,比如说组件里的配置信息等 等。 -
Intent Bundle
传递。包括显性和隐性传递,显性传递需要明确包名路径,组件 与组件往往是需要互相依赖,这背离组件化中 SOP(关注点分离原则),如果走 隐性的话,不仅包名路径不能重复,需要定义一套规则,只有一个包名路径出错, 排查起来也稍显麻烦,这个方式往往在组件间内部传递会比较合适,组件外与其 他组件打交道则使用场景不多。
说了这么多,那组件化通信什么机制比较适合呢?既然组件层中的模块是相互独 立的,它们之间并不存在任何依赖。没有依赖就无法产生关系,没有关系,就无 法传递消息,那要如何才能完成这种交流?
目前主流做法之一就是引入第三者。组件层的模块都依赖于基础层,从而产生第三者联系,这种第三者联系最终会编 译在 APP Module
中,那时将不会有这种隔阂,那么其中的 Base Module
就是 跨越组件化层级的关键,也是模块间信息交流的基础。比较有代表性的组件化开 源框架有得到 阿里 Arouter
等等。
1.3.事件总线
除了这种以通过引入第三者方式,还有一种解决方式是以事件总线方式,但这种 方式目前开源的框架中使用比例不高,如图:
事件总线通过记录对象,使用监听者模式来通知对象各种事件,比如在现实生活 中,我们要去找房子,一般都去看小区的公告栏,因为那边会经常发布一些出租 信息,我们去查看的过程中就形成了订阅的关系,只不过这种是被动去订阅,因 为只有自己需要找房子了才去看,平时一般不会去看。小区中的公告栏可以想象 成一个事件总线发布点,监听者则是哪些想要找房子的人,当有房东在公告栏上 贴上出租房信息时,如果公告栏有订阅信息功能,比如引入门卫保安,已经把之 前来这个公告栏要查看的找房子人一一进行电话登记,那么一旦有新出租消息产 生,则门卫会把这条消息一一进行短信群发,那么找房子人则会收到这条消息进 行后续的操作,是马上过来看,还是延迟过来,则根据自己的实际情况进行处理。
在目前开源库中,有 EventBus
、RxBus
就是采用这种发布/订阅模式,优点是简 化了 Android
组件之间的通信方式,实现解耦,让业务代码更加简洁,可以动态 设置事件处理线程和优先级,缺点则是每个事件需要维护一个事件类,造成事件 类太多,无形中加大了维护成本。那么在组件化开源框架中有 ModuleBus
等 等。
事件总线,又可以叫做组件总线,以 ModuleBus
框架的源码为例,这个方案特别之处在于其借鉴了 EventBus
的思想,组件的注册/注销和组件调用的事件发送都跟 EventBus
类似,能够传递一些 基础类型的数据,而并不需要在 Base Moudel
中添加额外的类。所以不会影响 Base
模块的架构,但是无法动态移除信息接收端的代码,而自定义的事件信息 类型还是需要添加到 Base Module
中才能让其他功能模块索引。
其中的核心代码是在与 ModuleBus
类,其内部维护了两个 ArrayMap
键对值列 表,如下:
private static ArrayMap<Object,ArrayMap<String,MethodInfo>>
moduleEventMethods = new ArrayMap<>();
private static ArrayMap<Class<?>,ArrayMap<String,ArrayList<Object>>>
moduleMethodClient = new ArrayMap<>()
在使用方法上,在 onCreate()
和 onDestroy()
中需要注册和解绑,比如
ModuleBus.getInstance().register(this);
ModuleBus.getInstance().unregister(this);
最终使用类似 EventBus
中 post
方法一样,进行两个组件间的通信。这个框架 的封装的 post
方法如下
public void post(Class<?> clientClass,String methodName,Object...args)
if(clientClass == null || methodName == null ||methodName.length() == 0) return;
ArrayList<Object> clientList = getClient(clientClass,methodName)
for(Object c: clientList) ArrayMap<String,MethodInfo> methods = moduleEventMethods.get(c);
Method method = methods.get(methodName).m;
method.invoke(c,args);
可以看到,它是通过遍历之前内部的 ArrayMap
,把注册在里面的方法找出,根据传入的参数进行匹配,使用反射调用。
1.4.接口+路由
相对于事件总线的方式,组件间通信更多使用的还是基于 Base Module
的接口+路由的方式
接口+路由实现方式则相对容易理解点,我之前实践的一个项目就是通过这种方式实现的。实现思路是专门抽取一个 LibModule
作为路由服务,每个组件声明自己提供的服务 Service API
,这些 Service
都是一些接口,组件负责将这些 Service 实现并注册到一个统一的路由 Router
中去,如果要使用某个组件的功能,只需要向 Router
请求这个 Service
的实现,具体的实现细节我们全然不关心,只要能返回我们需要的结果就可以了。比如定义两个路由地址,一个登陆组件,一个设置组件,核心代码:
public class RouterPath
public static final String ROUTER_PATH_TO_LOGIN_SERVICE = "/login/service";
public static final String ROUTER_PATH_TO_SETTING_SERVICE = "/setting/service";
那么就相应着就有两个接口 API
,如下:
public interface ILoginProvider extends IProvider
void goToLogin(Activity activity);
public interface ISettingProvider extends IProvider
void goToSetting(Activity activity);
这两个接口 API
对应着是向外暴露这两个组件的能提供的通信能力,然后每个组 件对接口进行实现,如下:
@Override
public void init(Context context)
@Override
public void goToLogin(Activity activity)
Intent loginIntent = new Intent(activity, LoginActivity.class);
activity.startActivity(loginIntent);
这其中使用的到了阿里的 ARouter
页面跳转方式,内部本质也是接口+实现方式 进行组件间通信。调用则很简单了,如下:
ILoginProvider loginService = (ILoginProvider)
ARouter.getInstance().build(RouterPath.ROUTER_PATH_TO_LOGIN_SERVICE).naviga tion();
if(loginService != null)
loginService.goToLogin(MainActivity.this);
还有一个组件化框架,就是 ModularizationArchitecture
,它本质实现方式也是 接口+实现,但是封装形式稍微不一样点,它是每个功能模块中需要使用注解建立 Action
事件,每个 Action
完成一个事件动作。invoke
只是方法名为反射,并 未用到反射,而是使用接口方式调用,参数是通过 HashMap
传递的,无法传递 对象。具体详解可以看这篇文章 Android
架构思考(模块化、多进程)。
1.5.页面跳转
页面跳转也算是一种组件间的通信,只不过它相对粒度更细化点,之前我们描述 的组件间通信粒度会更抽象点,页面跳转则是定位到某个组件的某个页面,可能 是某个 Activity
,或者某个 Fragment
,要跳转到另外一个组件的 Activity
或 Fragment
,是这两者之间的通信。甚至在一般没有进行组件化架构的工程项目 中,往往也会封装页面之间的跳转代码类,往往也会有路由中心的概念。不过一 般 UI 跳转基本都会单独处理,一般通过短链的方式来跳转到具体的 Activity
。
每个组件可以注册自己所能处理的短链的 Scheme
和 Host
,并定义传输数据的 格式,然后注册到统一的 UIRouter
中,UIRouter
通过 Scheme
和 Host 的匹 配关系负责分发路由。但目前比较主流的做法是通过在每个 Activity
上添加注 解,然后通过 APT 形成具体的逻辑代码。下面简单介绍目前比较主流的两个框架核心实现思路:
ARouter
ARouter
核心实现思路是,我们在代码里加入的 @Route
注解,会在编译时期通 过 apt
生成一些存储 path
和 activityClass
映射关系的类文件,然后 app
进程启 动的时候会拿到这些类文件,把保存这些映射关系的数据读到内存里(保存在 map
里),然后在进行路由跳转的时候,通过 build()
方法传入要到达页面的路由 地址,ARouter
会通过它自己存储的路由表找到路由地址对应的 Activity.class(activity.class = map.get(path))
,然后 new Intent()
,当调用 ARouter
的 withString()
方法它的内部会调用 intent.putExtra(String name, String value)
, 调用 navigation()
方法,它的内部会调用 startActivity(intent)
进行跳转,这样便可 以实现两个相互没有依赖的 module
顺利的启动对方的 Activity
了。
ActivityRouter
核心实现思路是,它是通过路由 + 静态方法来实现,在静态方 法上加注解来暴露服务,但不支持返回值,且参数固定位(context
, bundle
),基 于 apt 技术,通过注解方式来实现 URL
打开 Activity
功能,并支持在 WebView 和外部浏览器使用,支持多级 Activity
跳转,支持 Bundle
、Uri
参数注入并转换 参数类型。它实现相对简单点,也是比较早期比较流行的做法,不过学习它也是 很有参考意义的。
💡二丶插件化
提纲如下:
在 Android
系统中,应用是以 Apk
的形式存在的,应用都需要安装才能使用。但实际上 Android
系统安装应用的方式相当简单,其实就是把应用 Apk
拷贝到系统不同的目录下、然后把 so 解压出来而已。
常见的应用安装目录有:
/system/app
:系统应用/system/priv-app
:系统应用/data/app
:用户应用
那可能大家会想问,既然安装这个过程如此简单,Android
是怎么运行应用中的代码的呢,我们先看 Apk
的构成,一个常见的 Apk
会包含如下几个部分:
classes.dex
:Java
代码字节码res
:资源文件lib
:so 文件assets
:静态资产文件AndroidManifest.xm
l:清单文件
其实 Android
系统在打开应用之后,也只是开辟进程,然后使用 ClassLoader 加载 classes.dex
至进程中,执行对应的组件而已。
那大家可能会想一个问题,既然 Android
本身也是使用类似反射的形式加载代码执行,凭什么我们不能执行一个 Apk
中的代码呢?
2.1.插件化的优势
- 适应并行开发,解耦各个模块,避免模块之间的交叉依赖,加快编译速度, 从而提高并行开发效率。
- 满足产品随时上线的需求
- 修复因为我们对自己要求不严格而写出来的 bug。
- 插件化的结果:分为稳定的
release
版本和不稳定的snapshot
版本,每 个模块都高度解耦,没有交叉依赖,不会出现一个模块依赖了另一个模块, 其中一个人改了这个模块的代码,对另一个模块造成影响.
2.2.插件化的难点
想让插件的Apk
真正运行起来,首先要先能找到插件Apk
的存放位置,然后我们要能解析加载Apk
里面的代码。
但是光能执行Java
代码是没有意义的,在Android
系统中有四大组件是需要在系统中注册的,具体来说是在 Android
系统的 ActivityManagerService
(AMS) 和 PackageManagerService
(PMS) 中注册的,而四大组件的解析和启动都需要依赖 AMS
和 PMS
,如何欺骗系统,让他承认一个未安装的 Apk
中的组件,如何让宿主动态加载执行插件Apk
中 Android
组件(即 Activity
、Service
、BroadcastReceiver
、ContentProvider
、Fragment
)等是插件化最大的难点。
另外,应用资源引用(特指 R 中引用的资源,如 layout
、values
等)也是一大问题,想象一下你在宿主进程中使用反射加载了一个插件 Apk
,代码中的 R 对应的 id 却无法引用到正确的资源,会产生什么后果。
总结一下,其实做到插件化的要点就这几个:
- 如何加载并执行插件
Apk
中的代码(ClassLoader Injection
) - 让系统能调用插件
Apk
中的组件(Runtime Container
) - 正确识别插件
Apk
中的资源(Resource Injection
)
2.3.ClassLoader Injection
ClassLoader
是插件化中必须要掌握的,因为我们知道Android
应用本身是基于魔改的 Java 虚拟机的,而由于插件是未安装的 apk
,系统不会处理其中的类,所以需要使用 ClassLoader
加载 Apk
,然后反射里面的代码。
java
中的 ClassLoader
-
BootstrapClassLoader
负责加载JVM
运行时的核心类,比如JAVA_HOME/lib/rt.jar
等等 -
ExtensionClassLoader
负责加载JVM
的扩展类,比如JAVA_HOME/lib/ext
下面的jar
包 -
AppClassLoader
负责加载classpath
里的jar
包和目录
Android
中的ClassLoader
在Android
系统中ClassLoader
是用来加载dex文件的,有包含 dex
的 apk
文件以及 jar
文件,dex
文件是一种对class
文件优化的产物,在Android
中应用打包时会把所有class
文件进行合并、优化(把不同的class文件重复的东西只保留一份),然后生成一个最终的class.dex
文件
PathClassLoader
用来加载系统类和应用程序类,可以加载已经安装的apk
目录下的dex
文件
public class PathClassLoader extends BaseDexClassLoader
public PathClassLoader(String dexPath, ClassLoader parent)
super(dexPath, null, null, parent);
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent)
super(dexPath, null, libraryPath, parent);
DexClassLoader
用来加载dex
文件,可以从存储空间加载dex
文件。
public class DexClassLoader extends BaseDexClassLoader
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent)
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
我们在插件化中一般使用的是 DexClassLoader
。
2.3.双亲委派机制
每一个 ClassLoader
中都有一个 parent
对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent
为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。 下面是 ClassLoader
的 loadClass
方法的具体实现。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null)
try
if (parent != null)
// 先从父类加载器中进行加载
c = parent.loadClass(name, false);
else
c = findBootstrapClassOrNull(name);
catch (ClassNotFoundException e)
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
if (c == null)
// 没有找到,再自己加载
c = findClass(name);
return c;
2.4.如何加载插件中的类
要加载插件中的类,我们首先要创建一个 DexClassLoader
,先看下 DexClassLoader
的构造函数需要哪些参数。
public class DexClassLoader extends BaseDexClassLoader
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
// ...
构造函数需要四个参数:
dexPath
是需要加载的 dex / apk / jar
文件路径
optimizedDirectory
是 dex
优化后存放的位置,在 ART
上,会执行 oat
对 dex
进行优化,生成机器码,这里就是存放优化后的 odex
文件的位置
librarySearchPath
是 native
依赖的位置
parent
就是父类加载器,默认会先从 parent
加载对应的类
创建出 DexClassLaoder
实例以后,只要调用其 loadClass(className)
方法就可以加载插件中的类了。具体的实现在下面:
// 从 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)
2.5.执行插件类的方法
通过反射来执行类的方法
val loadClass = pluginClassLoader.loadClass(activityName)
loadClass.getMethod("test",null).invoke(loadClass)
我们称这个过程叫做 ClassLoader
注入。完成注入后,所有来自宿主的类使用宿主的 ClassLoader
进行加载,所有来自插件 Apk
的类使用插件 ClassLoader
进行加载,而由于 ClassLoader
的双亲委派机制,实际上系统类会不受 ClassLoader
的类隔离机制所影响,这样宿主 Apk
就可以在宿主进程中使用来自于插件的组件类了。
💡三丶组件化和插件化的区别
- 组件化:是将一个
App
分成多个模块,每个模块都是一个组件(module
),开发过程中可以让这些组件相互依赖或独立编译、调试部分组件,但是这些组件最终会合并成一个完整的Apk
去发布到应用市场。 - 插件化:是将整个
App
拆分成很多模块,每个模块都是一个Apk
(组件化的每个模块是一个lib
),最终打包的时候将宿主Apk
和插件Apk
分开打包,只需发布宿主Apk
到应用市场,插件Apk通过动态按需下发到宿主Apk
。
(文字+视频版)
Android
知识点大全。(音视频;Flutter
,kotlin
;Compose
;Framework
;性能优化;架构等)全文30W+字。200多个知识点,330张图。PDF版
以上是关于面试过程中;问到关于组件化+插件化的问题;这一篇就够了的主要内容,如果未能解决你的问题,请参考以下文章
Android面试老生常谈的 View 事件分发机制,看这一篇就够了!
Android面试老生常谈的 View 事件分发机制,看这一篇就够了