重构应用启动流程:打造生产级的动态任务调度框架

219 阅读5分钟

一句话总结:

设计延迟加载框架就像 “构建一张智能任务网络(DAG)” —— 先执行无依赖的关键路径任务,然后根据系统空闲、用户交互等多种“信号”自动、有序地唤醒并执行网络中的其余任务,实现“按需加载”到“智能预加载”的升华。


一、核心设计哲学:从队列到图(DAG)

高质量的延迟加载框架,其核心不应是一个简单的优先级队列,而应是一个 有向无环图(Directed Acyclic Graph, DAG)

  • 节点(Node) :代表一个独立的 Task
  • 边(Edge) :代表 Task 之间的依赖关系(例如,任务 B 依赖任务 A,则有一条从 A 指向 B 的边)。

优势:

  1. 依赖关系清晰化:从根本上解决了您提到的“依赖死锁”问题,在添加任务时即可通过图遍历检测出循环依赖。
  2. 自动唤醒机制:当一个任务(节点 A)执行完毕后,调度器可以沿着图的边,检查所有依赖它的后续任务(如节点 B、C)。一旦某个任务的所有前置依赖都已完成,它就可以被自动放入待执行队列。

二、框架架构重构

一个生产级的框架应包含以下解耦的模块:

  • 任务管理器(TaskManager & DAG) :负责维护完整的任务图谱。提供添加任务、构建依赖关系、检测循环依赖的功能。

  • 任务调度器(TaskDispatcher) :框架的大脑。它不直接执行任务,而是监听任务完成事件。当一个任务完成后,它会通知 TaskManager 更新图状态,并从 TaskManager 获取所有“入度为零”(即所有依赖已满足)的新任务,然后将这些任务分发给 执行器

  • 多维触发器(Multi-Trigger) :一个独立的模块,负责监听各类系统事件,并将事件转化为调度信号。

    • IdleTrigger:利用 Looper.myQueue().addIdleHandler,在主线程空闲时发出调度信号。这是执行非关键UI相关任务的最佳时机。
    • LifecycleTrigger:绑定 Activity/Fragment 生命周期,实现“页面可见时加载”、“页面销毁时清理”。
    • EventTrigger:监听特定事件(如“登录成功”、“网络连接”),触发相关任务。
  • 分级执行器(TieredExecutor) :取代单一的线程池。

    • 主线程执行器:用于必须在 UI 线程执行的任务。
    • CPU密集型线程池:核心数大小,用于计算密集型任务。
    • IO密集型线程池:更大尺寸的线程池,用于网络、磁盘读写等任务。
    • 根据设备性能动态调整:您提到的低端机适配策略在这里应用。

三、关键实现与进阶考量

1. 异步任务与依赖处理

现实中很多任务是异步的(如网络请求)。run() 方法应设计为可以通知调度器它何时“真正完成”。

interface ITask {
    // ...
    fun run(dispatcher: TaskDispatcher) 
}

class NetworkTask : ITask {
    override fun run(dispatcher: TaskDispatcher) {
        api.request {
            // 在网络回调成功后,才通知调度器本任务已完成
            dispatcher.notifyTaskFinished(this)
        }
    }
}

2. 优先级反转问题

思考一个场景:一个低优先级的任务 A,被一个高优先级的任务 B 所依赖。如果队列中充满了中等优先级的任务,可能会导致 A 一直无法执行,从而阻塞了高优任务 B。

解决:调度器在计算任务执行优先级时,应考虑其“子任务”的最高优先级,实现一种“优先级继承”的策略。

3. 对比 Jetpack Startup 库

任何现代启动框架的讨论都无法绕开 Google 的官方库 androidx.startup

  • 优点:使用简单,通过 Manifest 即可定义初始化顺序,解决了大部分简单的依赖问题。
  • 局限:它是一个纯粹的“启动时”框架,对于“空闲时”、“用户触发时”等动态、延迟的加载场景支持较弱。
  • 结论:我们的框架定位是 androidx.startup 的有力补充和场景延伸。可以用 androidx.startup 完成最核心、必须同步的初始化,然后将大量可延迟的任务交由我们设计的动态任务调度框架管理。

四、需要注意的问题

1. 依赖死锁

  • 解决:在 TaskManager 添加任务构建依赖边时,进行深度优先搜索(DFS)检测图中是否存在环。如果发现,应立即抛出异常,在开发阶段就阻止不合理的依赖关系。

2. 内存泄漏

  • 解决:除了 WeakReference,更推荐的方式是让 Task 实现 DefaultLifecycleObserver,通过与 LifecycleOwner 绑定,在 onDestroy 时自动清理对外部 контекст 的引用。

3. 线程安全

  • 解决:任务图的状态(如节点的完成状态)必须使用线程安全的数据结构(如 ConcurrentHashMap)来维护。对图结构的修改(如添加任务)需要通过锁来保证原子性。

4. 任务耗时监控

  • 解决:使用装饰器模式或 AOP(面向切面编程)思想,在 TaskDispatcher 分发任务时,自动为每个 ITask 包裹一层监控代理。这样既能统计耗时、捕获异常,又对任务代码无侵入。

五、新的工作流程图

graph TD
    A[App 启动] --> B{任务管理器初始化DAG};
    B --> C[分发所有入度为0的任务];

    subgraph 持续调度循环
        D[执行器执行任务] --> E{任务完成};
        E --> F[调度器收到完成通知];
        F --> G{更新DAG中任务状态};
        G --> H{查找新的入度为0的任务};
        H -- 有新任务 --> C;
    end
    
    subgraph 异步触发
        T1[IdleHandler触发] --> F;
        T2[用户操作触发] --> F;
        T3[生命周期变化触发] --> F;
    end