Android 模块化收口:能力组合框架、手写 LifecycleOwner 与热修复共存

三条机制,同一个矛盾:管得住业务,又不侵入业务

前言

模块拆到二十来个之后,会出现一种很别扭的局面:这些模块的业务逻辑高度相似,页面结构几乎一样,却各自写了一遍。改一个公共交互要改二十处,漏一处就是"这个模块的按钮跟别人不一样"。

团队的第一反应是抽基类。基类抽出来之后,第二个问题来了:每个模块都有一两个"特殊情况",于是基类里长出十几个 open fun 和一堆 if。框架管得越多,业务被绑得越死;管得越少,二十个模块又开始各写各的。

我在这套四十模块的工程里处理过三件事,看似不相关,其实是同一个矛盾的三个切面:调用层怎么让二十个模块长得一样又各自不同,生命周期层怎么让一个不是 Activity 的类拥有可自动清理的生命周期,启动层怎么把热修复塞进来而不打乱插件化的初始化顺序。 结论是:收口要收在骨架上,把差异留给填空,而不是收在分支上。

一、二十多个业务模块,长得像又各不相同

这些模块本质上高度相似:

  1. 占满一屏(地图 + 顶部信息 + 底部操作 + 右侧工具栏);
  2. 连接设备,接收实时数据;
  3. 展示实时计算结果;
  4. 提供一组工具栏按钮,点击触发业务动作;
  5. 能存取点位、能响应语音指令、能响应工程切换。

差异也很具体:有的要罗盘、有的要图表、有的要相机预览、有的不需要工具栏、有的不支持语音。

各写各的结果,是二十份 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)
}

三个关键点:

  1. 每个方法都给默认实现 (Kotlin 接口默认方法)。新增能力时老模块零改动------这是这套设计能撑住二十多个模块的原因。
  2. 按"角色"划分,不按"功能"划分。判断标准是"这个能力会不会被某些模块裁掉"。
  3. 总接口只做组合,不定义新方法,避免它自己变成上帝接口。

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()
    }
}

三个细节值得抄:

  1. @CallSuper :子类重写时编译器强制调用 super,把"忘了调父类"从运行时问题变成编译期问题。
  2. createXxx() 工厂方法而非 initXxx() 钩子:由基类决定"什么时候创建、创建后做什么",子类只负责"造什么"。子类的自由度被限制在安全范围内。
  3. 生命周期事件由基类统一派发 :子类永远不要自己调 handleLifecycleEvent,避免出现"子类先 DESTROY、父类后 RESUME"的乱序。

四、这套设计真实遇到的两个麻烦

4.1 模式切换时的"监听器错乱"

有个模块由于历史原因,没有拆成两个 WorkMode,而是在同一个对象里用布尔值区分"采集/放样"。源码注释写得很诚实:

不得已进行的兼容:如果按既定框架多增加一个 XxxWorkMode,会遇到两个问题------

  1. 切换入口拿不到新类型,需要反射判断;
  2. 即便反射判断成功,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 的本质是:一个可以分发状态、可以注册观察者、可以在销毁时自动清理的状态机。 满足下面三个条件的"东西",都值得拥有自己的生命周期:

  1. 有明确的创建与销毁时机,但这个时机不是 Activity/Fragment 的时机;
  2. 内部持有了需要被清理的资源(协程、监听、传感器、渲染器);
  3. 想把"清理"自动化,而不是靠调用方记得调 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(你真正写逻辑的地方)

由此产生三个约定:

  1. 不要在 HostApplication 里写任何代码 。它是编译期生成的,每次 clean 重新生成,你写的会被覆盖,所有逻辑放 *ApplicationLike。
  2. getApplication() 与 this 不是一回事 。在 ApplicationLike 里,this 不是 Application:
kotlin 复制代码
// 正确
ToastHelper.init(application)
(application as Application).registerActivityLifecycleCallbacks(callback)

// 错误:this 不是 Context 的 Application
ToastHelper.init(this as Application)   // ClassCastException
  1. 静态持有要拿 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 #架构设计

相关推荐
恋猫de小郭3 小时前
Flutter 可以更直接地调用原生 API 了,Dart Direct Native Interop
android·前端·flutter
2501_916008893 小时前
iPhone 设备信息查看,用 KeyMob 看型号、序列号、电池健康和 UDID
android·ios·小程序·https·uni-app·iphone·webview
好的佩奇13 小时前
蓝牙之通信架构
android·flutter·蓝牙
FlightYe14 小时前
视界原理之3D视频(四):裸眼、眼镜和头显
android·linux·c++·3d·ffmpeg·音视频
诸神黄昏EX15 小时前
Android fw BatteryService
android
alexhilton15 小时前
提升 Android Compose 页面的流畅度
android·kotlin·android jetpack
撩得Android一次心动16 小时前
Android中Gradle网络下载超时、更改存放位置(解决方案)
android·gradle
HQL_seven18 小时前
【Activity】Android Activity详解
android
AI2中文网18 小时前
安卓 15/16 手机软键盘遮挡输入框、布局顶不上去?已解决:重新编译即可
android·智能手机