Android应用热启动重用模型设计与实现
Android应用开发中,通常采用热启动的方式,来实现应用的快速启动。所谓热启动,就是在应用退出时,只finish主Activity,而不杀掉应用进程。这样在应用退出后热启时可以重用应用资源,比如Application,静态实例,线程等。这里牵涉到一个生命周期的问题。
1 生命周期
这里讨论的生命周期包括四个方面:
-应用的生命周期
-Activity的生命周期
-线程的生命周期
-静态实例的生命周期
应用的生命周期
应用的生命周期,简单来说就是Android应用进程的生命周期,应用进程由系统管理。启动是由其他应用触发,如Launcher。进程结束,可主动调用(Process.killPocess
或System.exit
),也可能由系统杀死。
应用进程结束后,应用占用资源将被回收,再次启动应用相当于全新启动,所有资源重新加载,与热启动对应,这种方式是冷启动。
Activity的生命周期
Activity的启动由系统AMS(ActivityManagerService)进程管理,Activity生命周期的结束由应用调用finish
触发。
线程的生命周期
Android应用中线程的生命周期由应用进行控制,如果应用没有结束线程,那么它将与应用的生命周期一致。通常在应用中可能存在这样的全局线程。
静态实例的生命周期
静态实例的生命周期与应用的生命周期一致。
可以看到,在热启动模型中,全局线程,静态实例的生命周期是与应用的生命周期一致的,独立于Activity的生命周期。
基于这一点,可以在热启动时,重用应用的资源,缩短启动过程,提升启动速度。这里的资源是指应用程序中所用到的对象实例,尤其是与启动相关的实例。这里的实例就是上面讲到的静态实例,全局线程本质上也需要静态实例来实现重用。
2 静态实例的重用
静态实例通常会持有很多其他实例(这里统称为资源)的引用,在重用需要注意的一点,是与Context相关资源的重用。热启动模型中,主Activity在退出时会销毁,重新启动应用时Activity重新创建,因此,静态实例中与Context相当的资源是不能重用的。
可以将静态实例的资源分为Context相关和Context无关,Context相关资源在实例重用时需要重新创建,Context无关资源直接重用。
另外,Context相关资源必须在应用退出时进行释放,否则静态实例始终持有Context引用,导致Activity无法释放。
这样重用设计的雏形就已经有了,但是重用模型牵涉到静态实例的创建、释放和重用,对于大型应用来讲,各个模块都来进行这些处理,显得比较繁琐。因此,可以基于模块化前提,抽象实现重用模型,这样对于应用模块开发就轻松了许多。
3 应用模块化
在大型Android应用当中,一种敏捷的设计模型是模块化模型,应用由若干模块组成,模块中采用MVC模型。模块MVC模型中,Controller由Manager角色和Bridger角色组成,Manager角色负责模块内部管理和提供对外接口,Bridger角色负责对外调用。对于小的模块,Manager角色和Bridger角色可以统一在Manager中,作为模块的管理者,负责整个模块内部管理,以及对外交互。
按照模块在应用生命周期中使用的频率,可将模块分为常驻模块和即用模块。常驻模块是指经常会被其他模块调用的模块;即用模块是指使用频率低,在调用时再进行即时创建的模块。
由于常驻模块会频繁被调用,因此常驻模块的管理者(Manager)通常设计为单例,即常驻模块存在一个常驻的静态实例。这里就涉及到前面所说的静态实例的重用, 这里重用其实就是模块的重用。
4 模块化模型重用设计
大型应用中模块本身的功能实现已经比较复杂,我们更倾向于在模块中只完成业务功能的实现,而将重用的实现抽象为基础框架。建立在重用框架之上的模块,可以把更专注于业务功能的实现。
重用框架的核心是定义一个BasicManager,用于实现生命周期管理,包括重用逻辑。模块Manager仅需继承BasicManager,即可实现模块的重用。上文中提到的静态实例的重用,就是BasicManager实例的重用。
BasicManager状态机
在应用生命周期中,BasicManager存在若干状态,生命周期的管理就是对这些状态的管理,也就是一个状态机。
BasicManager包含以下状态:
- Dead。应用从未启动或者启动后强制杀掉进程后的状态。
- Just Created。BasicManager实例刚创建的状态。
- Already Created。BasicManager实例已经创建的状态。
- Released。BasicManager实例释放后的状态。
- Just Reused。BasicManager刚重用的状态。
- Already Reused。BasicManager已经重用的状态。
转换转换图如下
BasicManger状态机.jpg状态由4个维度组成:
- 实例是否存在(
instance
) - 是否创建(
isCreated
) - 是否重用(
isReused
) - 是否释放(
isReleased
)
状态转换中,关键点有以下几个:
- Do exit节点。如果应用不采用热启动模型,那么执行Cold exit,直接退出应用程序,进入Dead状态。如果使用热启动模型,执行Hot exit,Hot exit执行Do release,Do release中需要重用的模块保持instance,不需要重用的模块直接销毁instance。
-
Check release节点。Check release节点有5个状态入口,用于检查是否已经释放(
isReleased
),如果未释放需要先释放。释放以后或者已经释放,再检查是否已经重用(isReused
),如果未重用,则执行重用逻辑Do reuse,进入Just Reused状态,否则进入Already Reused状态。
之所以要在重用之前检查是否释放,是因为某些模块的释放在主Activity的onDestroy
中调用,而onDestroy
的回调时机是不确认的,它可能在下一次启动应用之后再调用,因此会出现前一次没有未来得及释放,就重新进入了重用逻辑。因此,需要在重用之前强制进行释放。
BasicManager提供三个接口进行状态转换触发:
-
Constructor
。触发实例创建。调用时机:在实例化时调用。 -
reuse
。触发重用。调用时机:在实例存在时调用。
reuse的实现逻辑相对复杂,在实例存在(instance != null
)时,可能进入Already Created状态,也可能进入Just Reused状态,也可能进入Already Reused状态。在创建态(isCreated
为true)时进入Already Created状态;在非创建态且非重用态(isReused
为false)时,执行重用逻辑,进入Just Reused状态;在非创建态且重用态时,进入Already Reused状态。reuse
提供onReuse
接口供子类实现重用逻辑。 -
release
。触发释放。调用时机:在应用退出即主Activity结束时调用。reuse
提供onRelease
接口供子类实现释放逻辑。
Constructor
和reuse
在子类的getInstance方法中调用,实现如下:
public static ModuleManager getInstance() {
if (sInstance == null) {
synchronized (ModuleManager.class) {
if (sInstance == null) {
sInstance = new ModuleManager();
}
}
} else {
sInstance.reuse();
}
}
release
在主Activity退出时调用。但是如果每个模块都需要主动去调用,显得过于繁琐。因此BasicManager还实现了所有子类实例的统一释放管理。
BasicManager统一管理
要统一管理,首先需要将子类实例保存在列表中,在创建和重用时加入到列表,在统一释放时,遍历列表进行释放,并清空列表。这里的列表作为静态列表保存在BasicManager中。
对列表的访问牵涉到一个同步的问题,当然,如果对列表的所有访问都加上同步锁就不会有问题,但是这样做就影响了性能,尤其是对启动速度的影响。
基于性能考虑,BasicManager持有两类列表:主线程列表和非主线程列表。在访问列表时,如果是在主线程,那么直接加入主线程列表,都在主线程,不存在线程冲突的问题,不加同步锁也不会对程序造成阻塞;如果是在非主线程,则使用同步锁进行添加,由于是在非主线程,也不会造成阻塞。
到这里重用框架就基本实现了。Android应用热启动重用模型对模块生命周期进行了统一管理,使模块开发能够专注在业务功能上,实现应用的快速开发迭代。同时,模块的重用使得应用在热启动时速度得到提升。
当然时间和空间是不能兼得的,速度的提升是以牺牲空间为代价的,热启动模型中应用在退出后,仍然占用的了部分内存,因此在应用开发时需要注意,并不是所有的模块都需要作为常驻模块,进行模块的重用。建议与启动相关的模块为常驻模块,其他模块不进行重用,在释放时对静态实例进行销毁。