三条机制,同一个矛盾:管得住业务,又不侵入业务
前言
模块拆到二十来个之后,会出现一种很别扭的局面:这些模块的业务逻辑高度相似,页面结构几乎一样,却各自写了一遍。改一个公共交互要改二十处,漏一处就是"这个模块的按钮跟别人不一样"。
团队的第一反应是抽基类。基类抽出来之后,第二个问题来了:每个模块都有一两个"特殊情况",于是基类里长出十几个 open fun 和一堆 if。框架管得越多,业务被绑得越死;管得越少,二十个模块又开始各写各的。
我在这套四十模块的工程里处理过三件事,看似不相关,其实是同一个矛盾的三个切面:调用层怎么让二十个模块长得一样又各自不同,生命周期层怎么让一个不是 Activity 的类拥有可自动清理的生命周期,启动层怎么把热修复塞进来而不打乱插件化的初始化顺序。 结论是:收口要收在骨架上,把差异留给填空,而不是收在分支上。
一、二十多个业务模块,长得像又各不相同
这些模块本质上高度相似:
- 占满一屏(地图 + 顶部信息 + 底部操作 + 右侧工具栏);
- 连接设备,接收实时数据;
- 展示实时计算结果;
- 提供一组工具栏按钮,点击触发业务动作;
- 能存取点位、能响应语音指令、能响应工程切换。
差异也很具体:有的要罗盘、有的要图表、有的要相机预览、有的不需要工具栏、有的不支持语音。
各写各的结果,是二十份 80% 重复、20% 走样的代码。而那 20% 的走样正是 Bug 温床------改一个公共 Bug 要改二十处,还总漏。最终的解法是两个经典模式的组合:
┌───────────────────────────────────────────────────────┐
│ 接口能力组合(Interface Segregation + 角色接口) │
│ WorkMode : Module, IProjectAware, IMapAware, │
│ IViewAware, IDeviceAware, IStakeAware, │
│ ILifeCycle, IVoiceAware ... │
├───────────────────────────────────────────────────────┤
│ 模板方法(BaseWorkMode 装配骨架,子类填差异) │
│ init { 装配 header / bottomView / toolbar / renderer } │
│ onStart / onResume / onPause / onClose / onDestroy │
└───────────────────────────────────────────────────────┘
二、能力组合:用"角色接口"替代上帝接口
先看反面:如果只有一个 WorkMode 接口承载所有能力,每个模块都要实现二十个方法,其中十五个是空实现;改接口一次,二十个模块全改。
实际做法是按角色拆接口,再组合:
kotlin
// 能力接口(示意,均带默认实现)
interface IProjectAware {
fun onProjectOpen() {}
fun onProjectClose() {}
fun onCoordinateSystemChanged() {}
}
interface IMapAware {
fun getDefaultMapType(): MapType = MapType.OSM
fun onMapTileSourceChanged() {}
fun enableBoxSelect(): Boolean = false
}
interface IDeviceAware {
fun onDeviceStateChanged(state: DeviceState) {}
fun onPositionUpdated(position: Position) {}
}
interface IVoiceAware {
fun supportVoiceControl(): Boolean = false
fun canHandleVoiceCommand(cmd: VoiceCommand): Boolean = false
fun handleVoiceCommand(cmd: VoiceCommand) {}
}
interface IStakeAware {
fun onStakeNext() {}
fun onStakeLast() {}
}
然后组合出总接口:
kotlin
interface WorkMode : IModule, IProjectAware, IMapAware, IViewAware,
IDeviceAware, IStakeAware, ILifeCycle, IVoiceAware {
fun setWorkContext(context: WorkContext)
fun onFileChooseBack(path: String)
}
三个关键点:
- 每个方法都给默认实现 (Kotlin 接口默认方法)。新增能力时老模块零改动------这是这套设计能撑住二十多个模块的原因。
- 按"角色"划分,不按"功能"划分。判断标准是"这个能力会不会被某些模块裁掉"。
- 总接口只做组合,不定义新方法,避免它自己变成上帝接口。
2.1 supportXXX() 比 if (this is XXX) 更好
IVoiceAware.supportVoiceControl() 这种"自述能力"的方法,比在调用处 if (mode is IVoiceAware) 多一个好处:能力可以动态开关(比如"当前未连接设备,暂不支持语音"),而类型判断是编译期写死的。
kotlin
// 推荐:能力自述,可动态
if (mode.supportVoiceControl() && mode.canHandleVoiceCommand(cmd)) {
mode.handleVoiceCommand(cmd)
}
// 不推荐:类型判断,写死
if (mode is IVoiceAware) { /* ... */ }
三、模板方法:把"必须按顺序执行"的部分锁死
二十多个模块的公共流程(装配视图 → 注册渲染器 → 注册传感器监听 → 恢复状态 → 工具栏配置)一旦顺序错了,就会出现各种诡异问题。这类问题靠注释约束不住,只能靠模板方法 + 不可绕过的语义。
基类大致长这样:
kotlin
abstract class BaseWorkMode(
protected val context: WorkContext
) : WorkMode {
protected val lifecycleRegistry: LifecycleRegistry
protected lateinit var header: IHeaderView
protected var bottomView: IBottomView? = null
init {
lifecycleRegistry = LifecycleRegistry(workModeLifeCycleOwner)
header = createHeader()
bottomView = createBottomView() // 子类可返回 null
registerLayerRenderer(createRenderers())
}
// ---- 子类必须提供的差异点 ----
protected abstract fun createHeader(): IHeaderView
protected open fun createBottomView(): IBottomView? = null
protected open fun createRenderers(): List<LayerRenderer> = emptyList()
protected abstract fun getToolbarGroup(): ToolbarGroup
// ---- 框架锁死的流程 ----
@CallSuper
open fun onStart() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
initView()
header.onStart()
ToolbarManager.toolbarGroup = getToolbarGroup()
}
@CallSuper
open fun onResume() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
registerLayerRender()
bottomView?.onResume()
header.onResume()
SensorHub.addListener(this)
}
@CallSuper
open fun onClose() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
unRegisterLayerRender()
SensorHub.removeListener(this)
ToolbarManager.toolbarGroup = null
}
@CallSuper
open fun onDestroy() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
removeLayoutListeners()
}
}
三个细节值得抄:
@CallSuper:子类重写时编译器强制调用super,把"忘了调父类"从运行时问题变成编译期问题。createXxx()工厂方法而非initXxx()钩子:由基类决定"什么时候创建、创建后做什么",子类只负责"造什么"。子类的自由度被限制在安全范围内。- 生命周期事件由基类统一派发 :子类永远不要自己调
handleLifecycleEvent,避免出现"子类先 DESTROY、父类后 RESUME"的乱序。
四、这套设计真实遇到的两个麻烦
4.1 模式切换时的"监听器错乱"
有个模块由于历史原因,没有拆成两个 WorkMode,而是在同一个对象里用布尔值区分"采集/放样"。源码注释写得很诚实:
不得已进行的兼容:如果按既定框架多增加一个
XxxWorkMode,会遇到两个问题------
- 切换入口拿不到新类型,需要反射判断;
- 即便反射判断成功,
initObserve里 observe 的 ViewModel 是同一个,而LifecycleOwner不是同一个,会直接崩溃。
第 2 点很有代表性:同一个 LiveData 被两个不同的 LifecycleOwner 观察不是问题,但如果切换时旧的 owner 已经 DESTROY 而 LiveData 仍持有它,再 observe 就会抛 Cannot add the same observer with different lifecycles。
正确的做法恰恰是框架想让你做的:状态不同就拆成不同的 WorkMode 实例 ,切换时旧的走完整 onClose → onDestroy,新对象重新 observe。
4.2 差异化需求把基类撑爆
某个模块需要"特殊处理"时,最省事的动作是在基类加一个 open fun,然后基类开始长 if。判断标准很简单:
这个差异只属于一个模块吗?是的话,它不该进基类。
进基类的东西必须满足"至少三个模块需要",或者"所有模块都需要且顺序敏感"。其余差异用组合(注入策略对象)解决:
kotlin
// 差异用组合承载,而不是给基类加抽象方法
interface IStakeNavigator {
fun next()
fun last()
}
class BaseWorkMode(
private val navigator: IStakeNavigator? = null // 不需要的模块传 null
) : WorkMode {
override fun onStakeNext() { navigator?.next() }
override fun onStakeLast() { navigator?.last() }
}
五、lifecycleRegistry 从哪来:手写 LifecycleOwner
BaseWorkMode 里反复出现的 lifecycleRegistry 和 workModeLifeCycleOwner,是一个既不是 Activity 也不是 Fragment 的类自己实现的生命周期。
Lifecycle 的本质是:一个可以分发状态、可以注册观察者、可以在销毁时自动清理的状态机。 满足下面三个条件的"东西",都值得拥有自己的生命周期:
- 有明确的创建与销毁时机,但这个时机不是 Activity/Fragment 的时机;
- 内部持有了需要被清理的资源(协程、监听、传感器、渲染器);
- 想把"清理"自动化,而不是靠调用方记得调
close()。
WorkMode 就是典型:一个 Activity 里可以先后切换十几个 WorkMode ,Activity 还活着,但某个 WorkMode 早就该销毁了。如果它的协程和监听挂在 Activity 的 lifecycle 上:切走的 WorkMode 协程永远不会停,十几个一起跑;传感器监听越加越多,回调里更新已经不可见的 View;切回来时旧观察者还活着,回调乱序。
核心实现只有一个 owner 加一个 registry:
kotlin
class BaseWorkMode(private val context: WorkContext) : WorkMode {
private val lifecycleRegistry: LifecycleRegistry
val workModeLifeCycleOwner: LifecycleOwner
init {
workModeLifeCycleOwner = WorkModeLifeCycleOwner()
lifecycleRegistry = LifecycleRegistry(workModeLifeCycleOwner)
}
inner class WorkModeLifeCycleOwner : LifecycleOwner {
override fun getLifecycle(): Lifecycle = lifecycleRegistry
}
}
然后由外部在合适的时机派发事件:
kotlin
@CallSuper
open fun onStart() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
// ...
}
@CallSuper
open fun onResume() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
// ...
}
@CallSuper
open fun onClose() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
// ...
}
@CallSuper
open fun onDestroy() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
// ...
}
拿到自己的 owner 之后,这个普通类就能享受整套 Jetpack 生态:
kotlin
// 1) 自带协程作用域,onDestroy 自动取消
workModeLifeCycleOwner.lifecycleScope.launch(Dispatchers.IO) {
val data = repository.load()
withContext(Dispatchers.Main) { render(data) }
}
// 2) LiveData 观察,销毁自动反注册
viewModel.result.observe(workModeLifeCycleOwner) { data ->
updateUi(data)
}
// 3) 生命周期感知的监听注册
workModeLifeCycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
sensorManager.unregister(this@BaseWorkMode)
}
})
这一段的价值在于:"别忘了反注册"从一条人需要记住的规则,变成了一个自动行为。
六、手写生命周期的五个坑
6.1 LifecycleRegistry 必须在主线程操作
它内部有 mHandlingEvent / mAddingObserverCounter 这类非原子状态,从子线程调用 handleLifecycleEvent 会抛:
java.lang.IllegalStateException: Method handleLifecycleEvent must be called on the main thread
派发时机来自子线程(比如设备回调线程)时,必须切回来:
kotlin
fun dispatchDestroy() {
mainHandler.post {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
}
6.2 currentState 与 Event 不是一回事
Event 是"发生了什么",State 是"现在处于什么阶段"。混用会导致状态机错乱。判断"当前是否可用"用 currentState:
kotlin
// 正确
if (lifecycleRegistry.currentState.isAtLeast(Lifecycle.State.STARTED)) {
updateUi()
}
// 或者用 Lifecycle 提供的便捷写法
if (workModeLifeCycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
startAnimation()
}
6.3 DESTROYED 是终态,销毁的实例不可复用
一旦派发 ON_DESTROY,这个 registry 就废了,再派发任何事件都会抛异常,而且之前注册的观察者不会再收到任何回调。所以工程里有一条容易被忽略的约定:
复用实例 = 找死。一个 WorkMode 实例一旦
onDestroy,必须新建,不能复活。
这跟 registerForActivityResult 二次进入不回调是同一类根因------被销毁后复用的对象,它注册的回调已经全部注销了。
6.4 别把 Activity 的 lifecycleScope 和自己的混用
kotlin
init {
context.getActivity().lifecycleScope.launch { // 挂在 Activity 上
onResumeFlow.debounce(150).collect {
if (lifecycleRegistry.currentState == Lifecycle.State.RESUMED) {
withContext(Dispatchers.Main) { actionOnResume(flag) }
}
}
}
}
这里用 Activity 的 scope 是有意为之 (这个防抖流需要跨越 WorkMode 切换而存活),但里面必须再判一次自己的 currentState,否则会在 WorkMode 已关闭后仍然执行 actionOnResume。
判断依据 :这个协程应该跟着谁?跟着 Activity 就用 Activity 的 scope,跟着 WorkMode 就用 workModeLifeCycleOwner.lifecycleScope(绝大多数情况)。
6.5 repeatOnLifecycle 需要 STARTED,别在 CREATED 阶段收集
kotlin
// 正确:自动在 STARTED 启动、STOP 时取消
workModeLifeCycleOwner.lifecycleScope.launch {
workModeLifeCycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { render(it) }
}
}
注意它的语义是"到达该状态时启动协程,低于该状态时取消 协程(而不是挂起)"。如果收集逻辑有中间状态需要保留,得自己用 Flow.stateIn + SharingStarted 处理。
七、什么时候不该手写,以及一个可复用模板
不是所有"有生命周期"的类都需要它。下面三种情况用了反而更糟:
| 情况 | 更好的选择 |
|---|---|
| 类的生命周期完全等于宿主 Activity/Fragment | 直接用宿主的 lifecycle |
| 只是需要一个能取消的协程作用域 | CoroutineScope(SupervisorJob()) + 手动 cancel() |
| 生命周期由外部不可控事件驱动(如网络断连) | 状态机 + 手动清理,不要硬套 lifecycle 语义 |
判断标准:这个类的创建/销毁时机,是否是宿主无法感知的? 是,才需要自己一套。
kotlin
abstract class LifecycleAwareComponent : LifecycleOwner, DefaultLifecycleObserver {
private val registry = LifecycleRegistry(this)
override fun getLifecycle(): Lifecycle = registry
fun start() { registry.handleLifecycleEvent(Lifecycle.Event.ON_START) }
fun resume() { registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) }
fun pause() { registry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) }
fun stop() { registry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) }
fun destroy() {
registry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
protected fun ensureMainThread(action: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) action() else mainHandler.post(action)
}
companion object {
private val mainHandler = Handler(Looper.getMainLooper())
}
}
使用方只需要:
kotlin
class MyMode : LifecycleAwareComponent() {
fun load() {
lifecycleScope.launch(Dispatchers.IO) { /* 自动随 destroy 取消 */ }
}
}
八、热修复与插件化共存:MultiDex 顺序与两个 Application
到这里,业务层的"管得住又不侵入"已经解决了。但框架还有一个层面绕不过去:启动顺序。热修复和插件化叠在一起时,Application 的初始化代码是"能跑但顺序不能动"的。
宿主里 Application 代理类的真实结构(已脱敏)是这样:
kotlin
@DefaultLifeCycle(
application = ".HostApplication", // 真正写入 Manifest 的类名
flags = ShareConstants.TINKER_ENABLE_ALL,
loaderClass = "com.tencent.tinker.loader.TinkerLoader",
loadVerifyFlag = false
)
class HotFixApplicationLike(
application: Application?,
tinkerFlags: Int,
tinkerLoadVerifyFlag: Boolean,
applicationStartElapsedTime: Long,
result: Intent?
) : DefaultApplicationLike(
application, tinkerFlags, tinkerLoadVerifyFlag,
applicationStartElapsedTime, result
) {
override fun onBaseContextAttached(base: Context) {
super.onBaseContextAttached(base)
// 1) 必须先装 MultiDex
MultiDex.install(base)
// 2) 再初始化热修复
HotFixManager.setApplicationLike(this)
HotFixManager.setUpgradeRetryEnable(true)
HotFixManager.initHotFix(this)
// 3) 最后做需要在 attach 阶段完成的全局注入
initXmlParserProperty()
}
}
这段代码有三个时间敏感点,顺序错了不一定编译报错,但一定线上出问题。
8.1 为什么 MultiDex 必须排在热修复前面
Tinker 的原理是把补丁 dex 插到 ClassLoader 的 dex 数组最前面,它依赖能枚举到宿主的所有 dex 。如果 MultiDex.install() 还没执行,主 dex 之外的类还躺在 classes2.dex 里没被加载,此时初始化 Tinker:
- 轻则补丁加载后找不到要修复的类(补丁生效一半);
- 重则在某些机型上
ClassNotFoundException直接崩在启动阶段------而且是热修复框架自己崩的,补丁再也救不回来。
所以注解处理器生成的注释里那句 you must install multiDex whatever tinker is installed! 不是客气话。
反过来,把 Tinker 相关类手动塞进主 dex(multiDexKeepFile)理论上可以交换顺序,但不建议------主 dex 越大越容易踩 65536 的线性分配区(LinearAlloc)限制,尤其在 Android 4.x~5.x 的老设备上(这个工程要跑工业手持设备,系统版本跨度极大)。
groovy
// 不推荐的做法:为了让顺序可变而把 tinker 塞进主 dex
multiDexKeepFile file('tinker_keep.txt')
// com.tencent.tinker.loader.**
// com.tencent.tinker.lib.**
8.2 @DefaultLifeCycle 制造了"两个 Application"
这是最容易让人懵的一点。你写的是 HotFixApplicationLike(继承 DefaultApplicationLike),但 Manifest 里注册的是注解生成的 .HostApplication。
Manifest: android:name=".HostApplication"
│ (注解处理器在编译期生成)
▼
HostApplication extends TinkerApplication
│ (运行时委托)
▼
HotFixApplicationLike(你真正写逻辑的地方)
由此产生三个约定:
- 不要在
HostApplication里写任何代码 。它是编译期生成的,每次 clean 重新生成,你写的会被覆盖,所有逻辑放*ApplicationLike。 getApplication()与this不是一回事 。在ApplicationLike里,this不是Application:
kotlin
// 正确
ToastHelper.init(application)
(application as Application).registerActivityLifecycleCallbacks(callback)
// 错误:this 不是 Context 的 Application
ToastHelper.init(this as Application) // ClassCastException
- 静态持有要拿
getApplication():
kotlin
App.onCreate(application, APP_KEY, BuildConfig.DEBUG)
KVStorage.getInstance() // 内部初始化目录,依赖 Application
8.3 多进程守卫:别让每个进程都初始化一遍
工程里有多个进程(主进程、设备服务进程、远程服务进程等)。不加守卫,统计/支付/地图这些 SDK 会在每个进程各初始化一次,后果是重复上报、数据库锁竞争,以及最经典的 SQLiteDatabaseLockedException。做法是在 onCreate 第一行判进程:
kotlin
override fun onCreate() {
super.onCreate()
// 进程无关的基础初始化(在每个进程都要做)
initStrictMode()
loadNativeLibrary()
val isMainProcess = application.packageName == getCurrentProcessName()
if (!isMainProcess) {
return // 以下是主进程专属
}
initCrashHandler()
initStatistics()
initCloud()
initRouter()
}
kotlin
private fun getCurrentProcessName(): String {
val pid = android.os.Process.myPid()
val am = application
.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
val processList = am?.runningAppProcesses ?: return ""
for (info in processList) {
if (info.pid == pid) {
return info.processName
}
}
return ""
}
两个注意点:android:process 声明的进程名是 包名:xxx,不是 xxx ,所以比较要用 getPackageName().equals(processName),而不是 endsWith("xxx");Android P 之后 getRunningAppProcesses() 有权限收紧,更稳的方式是 Application.getProcessName()(API 28+)或读 /proc/self/cmdline。
九、热修复能修什么,以及启动阶段的隐藏依赖
这是很多人踩坑后才明白的一点:
| 代码位置 | 能否热修 | 说明 |
|---|---|---|
| 宿主 Application 自身 | 不能 | Application 先于补丁加载执行,补丁来不及生效 |
| 宿主 Manifest 里的四大组件 | 不能 | 新增组件必须发版(插件化框架另说) |
| 业务模块(插件)代码 | 视方案而定 | 插件由框架动态加载,通常走插件自身的更新通道 |
| 纯 Java/Kotlin 业务逻辑 | 能 | Tinker 的主战场 |
| so 库 | 能(受限) | 需注册 ABI 路径 |
| 资源(图片/布局/字符串) | 能(受限) | 资源 id 不能新增,只能改内容 |
工程里对 so 的处理:
kotlin
fun loadNativeLibrary() {
// 将补丁中的 so 注册到系统 library path
TinkerLoadLibrary.installNavitveLibraryABI(application, "arm64-v8a")
}
热修复的定位应该是业务逻辑的紧急止血,不要指望它修 Application、清单文件和资源结构;真正需要动态下发的页面,走插件化。
9.1 启动阶段还有一个"隐藏依赖":全局属性注入
这个工程在 Android 上跑 POI(读写 Excel),而 Android 的 XML 解析器实现不完整,必须在任何 XML 解析发生之前注入实现:
kotlin
private fun initXmlParserProperty() {
System.setProperty("org.apache.poi.javax.xml.stream.XMLInputFactory",
"com.fasterxml.aalto.stax.InputFactoryImpl")
System.setProperty("org.apache.poi.javax.xml.stream.XMLOutputFactory",
"com.fasterxml.aalto.stax.OutputFactoryImpl")
System.setProperty("org.apache.poi.javax.xml.stream.XMLEventFactory",
"com.fasterxml.aalto.stax.EventFactoryImpl")
}
它必须放在 onBaseContextAttached 而不是 onCreate,原因是:ContentProvider 的 onCreate 早于 Application 的 onCreate 。如果某个 ContentProvider(FileProvider,或三方 SDK 自带的初始化 Provider)间接触发了 XML 解析,System.setProperty 就来不及了。一句话规律:
凡是"影响类加载/解析行为"的注入,放
attachBaseContext(或onBaseContextAttached);凡是"业务初始化",放onCreate。
9.2 Debug 阶段的 StrictMode 是最好的提前预警
kotlin
private fun initStrictMode() {
if (!BuildConfig.DEBUG) {
return
}
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectNetwork() // 主线程网络
.detectDiskReads() // 主线程磁盘读
.detectDiskWrites() // 主线程磁盘写
.penaltyLog()
.penaltyDialog()
.penaltyDeath() // Debug 下直接崩,逼你修
.build())
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectLeakedClosableObjects() // 未 close 的流
.detectActivityLeaks() // Activity 泄漏
.penaltyLog()
.build())
}
penaltyDeath() 在 Debug 下很粗暴,但它能让"主线程读写数据库"这种问题在提测前就暴露,而不是等到线上 ANR 告警。
十、三件事串成一条链
拆分边界(变更频率 + 依赖方向)
↓
通信方案(崩溃域决定进程,调用频率决定是否下沉接口)
↓
业务抽象(能力接口组合 + 模板方法锁流程)
↓
生命周期(手写 LifecycleOwner,让清理自动化)
↓
启动治理(MultiDex → 热修复 → 多进程守卫 → 全局注入)
这条链上每个环节都在回答同一个问题:框架该管到哪一层为止。 能力接口管"你能提供什么",模板方法管"按什么顺序做",LifecycleOwner 管"什么时候清理",启动代理管"谁先初始化"。管到"顺序"就够,再往下的"具体做什么"必须留给业务------这就是"管得住又不侵入"的分界线。
落地时可以直接拿这张清单对着代码看:
- 每个能力接口的方法都有默认实现,新增能力不改老模块;
- 总接口只组合、不定义新方法;
- 公共流程方法上一律加
@CallSuper; - 子类覆写的入口是
createXxx()(造什么),不是initXxx()(什么时候做); - 生命周期事件只在基类派发,子类禁止调
handleLifecycleEvent; - 只有单个模块需要的差异,用组合注入,不进基类;
- 状态不同的场景拆成不同实例,不要用布尔值在一个对象里切换;
-
MultiDex.install()一定在 Tinker 初始化之前; - 影响类加载与 XML 解析的注入放在
onBaseContextAttached。
结论
- 收口收在骨架上,不收在分支上 :二十个模块的差异应该收敛成"填几个工厂方法",而不是基类里的一堆
if。 - 能力按角色拆接口,每个方法给默认实现:新增能力时老模块零改动,这是撑住二十多个模块的前提。
- 总接口只组合不扩方法:否则它迟早变成新的上帝接口。
@CallSuper+createXxx()+ 基类统一派发生命周期:把"忘了调父类""创建时机不对""事件乱序"三件事变成编译期或结构性约束。- 只属于一个模块的差异用组合,不进基类:进基类的门槛是"至少三个模块需要"。
Lifecycle是可观察状态机,不是 Activity 的专利 :LifecycleRegistry(owner)+getLifecycle()两行就能给普通类一套生命周期。LifecycleRegistry只在主线程派发,DESTROYED是终态:销毁的实例不能复用,否则观察者永远不会再收到回调。- 协程跟着谁就用谁的 scope :跨 WorkMode 存活用 Activity 的,其余用
workModeLifeCycleOwner.lifecycleScope。 MultiDex.install()必须早于 Tinker 初始化:否则补丁可能生效一半,或者热修复框架自己崩在启动阶段。- 热修复只做业务逻辑止血:Application、Manifest 组件、资源 id 都修不了,需要动态下发的页面走插件化。
这三件事分开看是三个技巧,合起来是一套约束:把不确定的部分(业务差异、销毁时机、初始化顺序)变成结构上不可能出错的东西。 如果你也在给多模块工程做统一的业务框架,欢迎在评论区聊聊你的"骨架"划在哪一层,尤其是那些事后才发现的侵入点。
关键词标签 :#Android #模块化 #Lifecycle #Jetpack #热修复 #Tinker #MultiDex #架构设计