为什么要用模块化、组件化才能完成 Android 项目中类加载功能?
模块化
模块:
- 最初的目的是将同一类型的代码整合在一起; 所以模块的功能相对复杂,但都同属于一个业务;
- 不同模块之间也会存在依赖关系; 但大部分都是业务性的互相跳转,从地位上来说它们都是平级的
特点:
- 分属同一功能/业务的代码进行隔离(分装)成独立的模块,可以独立运行; 以页面、功能或其他不同粒度划分程度不同的模块,位于业务框架层,模块间通过接口调用,目的是降低模块间的耦合,由之前的主应用与模块耦合,变为主应用与接口耦合,接口与模块耦合;
- 模块就像有多个USB插口的充电宝,可以和多部手机充电,接口可以随意插拔,复用性很强,可以独立管理; 模块就像是独立的功能和项目(如淘宝:注册、登录、购物、直播…),可以调用组件来组成模块,多个模块可以组合成业务框架
组件化
组件:
- 最初的目的是代码重用,功能相对单一或者独立; 在整个系统的代码层次上位于最底层,被其他代码所依赖,所以说组件化是纵向分层
特点:
- 把重复的代码提取出来合并成为一个个组件,组件最重要的就是重用(复用),位于框架最底层,其他功能都依赖于组件,可供不同功能使用,独立性强
- 组件就像一个个小的单位,多个组件可以组合成组件库,方便调用和复用,组件间也可以嵌套,小组件组合成大组件
Android工程的组件一般分为两种:
application组件: 是指该组件本身就可以运行并打包成apk
lib组件: 是指该组件属于app的一部分,可以供其它组件使用但是本身不能打包成apk
正常一个App中可以有多个module(模块),但是一般只会有一个module是设置为application的,其他均设置为library; 组件化开发就是要每个module都可以运行起来,因此在开发期间每个module均设置为application,发布时再进行合并。
为什么要有组件化?
Android项目中代码量达到一定程度,编译将是一件非常痛苦的事情; 短则一两分钟,长则达到五六分钟;随着app业务的壮大,模块越来越多,代码量超10万是很正常的
这个时候我们会遇到以下问题:
- 稍微改动一个模块的一点代码都要编译整个工程,耗时耗力
- 公共资源、业务、模块混在一起耦合度太高
- 不方便测试
组件化开发的优点
组件化开发可以有效降低代码模块的耦合度,使代码架构更加清晰,同时模块化的编译可以有效减少编译时间,当然总的编译时间是不会减少的,只是App模块化之后开发某个模块时,只需要编译特定模块,可以快速编译调试
- 业务模块分开,每个模块可以独立开发编译运行,解耦的同时也降低了项目的复杂度
- 开发单个模块时可以共享资源和工具类
- 可以针对单个模块测试, 开发调试时不需要对整个项目进行编译
- 多人合作时可以只关注自己的业务模块,把某一业务当成单一项目来开发
- 可以灵活的对业务模块进行组装和拆分
组件化与模块化开发项目模块介绍
basic_library模块(基础类库模块)
基础类库主要是将各个组件中都会用到的一些基础库统一进行封装,例如网络请求、图片缓存、sqlite操作、数据加密等基础类库
这样可以避免各个组件都在自己的组件中单独引用,而且引用的版本可能都不一样,导致整个工程外部库混乱,统一了基础类库后,基础类库保持相对的稳定,这样各个组件对外部库的使用是相对可控的,防止出现一些外部库引出的极端问题,而且这样的话对于库的升级也比较好管理
basic_project模块(基础工程模块)
对于每个组件都有一些是公共的抽象,例如我们工程中自己定义的BaseActivity、BaseFragment、自定义控件等,这些对于每个组件都是一样的,每个组件都基于一样的基础工程开发,一方面可以减少开发工作,另一方面也可以让各个组件的开发人员能够统一架构框架
这样每个组件的技术代码框架看起来都是一样的,也便于后期代码维护和人员互备
main_project模块(业务模块)
应用的主要业务逻辑在此实现,上面的几部分都实现以后,剩余的主要体力工作就是实现各个拆分出来的业务模块
app模块(壳工程模块)
壳工程主要用于将各个组件组合起来和做一些工程初始化,初始化包含了后续各个组件会用到的一些库的初始化,也包括ApplicationContext的初始化工作
插件化
在 Android 系统中,应用是以 Apk 的形式存在的,应用都需要安装才能使用; 但实际上 Android 系统安装应用的方式相当简单,其实就是把应用 Apk 拷贝到系统不同的目录下、然后把 so 解压出来而已
常见的应用安装目录有:
- /system/app:系统应用
- /system/priv-app:系统应用
- /data/app:用户应用
Apk 的构成,一个常见的 Apk 会包含如下几个部分:
- classes.dex:Java 代码字节码
- res:资源目录
- lib:so 目录
- assets:静态资产目录
- AndroidManifest.xml:清单文件
其实 Android 系统在打开应用之后,也只是开辟进程,然后使用 ClassLoader 加载 classes.dex 至进程中,执行对应的组件而已
那大家可能会想一个问题,既然 Android 本身也是使用类似反射的形式加载代码执行,凭什么我们不能执行一个 Apk 中的代码呢?
这其实就是插件化的目的: 让 Apk 中的代码(主要是指 Android 组件)能够免安装运行,这样能够带来很多收益,最显而易见的优势其实就是通过网络热更新、热修复
插件化技术难点
- 反射并执行插件 Apk 中的代码(ClassLoader Injection)
- 让系统能调用插件 Apk 中的组件(Runtime Container)
- 正确识别插件 Apk 中的资源(Resource Injection)
热修复
说起热修复,已经是目前Android开发必备技能
我所了解的一种实现方式就是类加载方案,即 dex 插桩; 这种思路在插件化中也会用到;除此之外,还有底层替换方案,即修改替换 ArtMethod;采用类加载方案的主要是以腾讯系为主,包括微信的 Tinker、饿了么的 Amigo;采用底层替换方案主要是阿里系的 AndFix 等热修复的应用场景
热修复就是在APP上线以后,如果突然发现有缺陷了,如果重新走发布流程可能时间比较长,重新安装APP用户体验也不会太好; 热修复就是通过发布一个插件,使APP运行的时候加载插件里面的代码,从而解决缺陷,并且对于用户来说是无感的(用户也可能需要重启一下APP)
认识Java类的加载机制(双亲委派模型)
Java负责加载class文件的就是类加载器(ClassLoader); APP启动的时候,会创建一个自己的ClassLoader实例,我们可以通过下面的代码拿到当前的ClassLoader
<pre mdtype="fences" cid="n108" lang="" class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">ClassLoader classLoader = getClassLoader();
Log.i(TAG, "[onCreate] classLoader" + ":" + classLoader.toString());</pre>
然后我们在看一下构造函数。在ClassLoader 这个类中的 loadClass() 方法,它调用的是另一个2个参数的重载 loadClass() 方法
<pre mdtype="fences" cid="n115" lang="" class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
</pre>
我们点进去深入看一下loadClass这个方法:
<pre mdtype="fences" cid="n120" lang="" class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" > 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) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
</pre>
通过分析loadClass方法,你会发现,ClassLoader加载类的方法就是loadClass,是通过双亲委派模型(Parents Delegation Model)实现类的加载的; 既在加载一个字节码文件时,会询问当前的classLoader是否已经加载过此字节码文件
如果加载过,则直接返回,不再重复加载; 如果没有加载过,则会询问它的Parent是否已经加载过此字节码文件,同样的,如果已经加载过,就直接返回parent加载过的字节码文件,而如果整个继承线路上的classLoader都没有加载过,才由child类加载器(即,当前的子classLoader)执行类的加载工作
整个流程大致可以归纳成如下三步
1、加载流程
检查当前的 classLoader 是否已经加载琮这个 class ,有则直接返回,没有则进行第2步。 调用父 classLoader 的 loadClass() 方法,检查父 classLoader 是否有加载过这个 class ,有则直 接返回,没有就继续检查上上个父 classLoader ,直到顶层 classLoader 。 如果所有的父 classLoader 都没有加载过这个 class ,则最终由当前 classLoader 调用 findClass() 方法,去dex文件中找出并加载这个 class
2、优点
而采用这种类的加载机制的优点就是如果一个类被classLoader继承线路上的任意一个加载器加载过,后续在整个系统的生命周期中,这个类都不会再被加载,大大提高了类的加载效率
作用
类加载的共享功能: 一些Framework层级的类一旦被顶层,classLoader加载过,会缓存到内存中,以后在任何地方用到,都不会去重新加载,大大提高了效率;
类加载的隔离功能: 不同继承线路上的 classLoader 加载的类,肯定不是同一个类,这样可以避免某些开发者自己去写一 些代码冒充核心类库,来访问核心类库中可见的成员变量。如 java.lang.String 在应用程序启动前就 已经被系统加载好了,如果在一个应用中能够简单的用自定义的String类把系统中的String类替换掉 的话,会有严重的安全问题