固件文件管理与监听
本文档介绍 JL OTA Android SDK 中固件文件的管理方式与文件监听机制,涵盖固件文件路径/数据的配置入口、基于 android.os.FileObserver 的监听器实现、回调分发辅助类以及相关生命周期管理。
Purpose and Scope
本页面聚焦 SDK 中"固件文件"这一概念的完整生命周期:固件文件如何被指定(路径或字节数据)→ 如何被读取/校验 → 如何被监听(目录/文件变化)→ 回调如何分发到业务层。
范围包括:
com.jieli.otasdk.tool.file包下的文件监听三件套:FileObserverCallback、OtaFileObserver、OtaFileObserverHelperBluetoothOption中固件文件配置入口setFirmwareFilePath/setFirmwareFileData及其用法- 文件监听的注册、启动、停止、销毁等生命周期
不在本页面范围(由其他目录页覆盖)的内容:
- 蓝牙连接与 OTA 传输细节 → 参见蓝牙管理相关页面
- OTA 升级状态机与进度回调 → 参见 OTA 流程相关页面
- 设备信息解析(版本号、UID 等)→ 参见设备信息相关页面
Overview
在 OTA(Over-The-Air)升级流程中,固件文件是升级动作的输入。SDK 允许调用方通过两种方式提供固件:
- 文件路径:
setFirmwareFilePath(String path)—— 传入本地存储中的 OTA 文件路径,SDK 按需读取文件内容; - 文件字节数据:
setFirmwareFileData(byte[] data)—— 直接传入固件数据,适用于文件已加载到内存的场景。
无论采用哪种方式,固件文件通常存放在应用私有目录(如 MainApplication.getOTAFileDir() 所示)或外部存储中。当文件由外部进程(例如通过 USB/下载任务写入、或由设备管理工具同步)写入时,业务层需要感知文件何时就绪、何时被修改或删除,以便触发后续的解析与升级。这正是 OtaFileObserver 系列类存在的意义:通过 Android 系统级 FileObserver 能力,以事件驱动的方式监听固件文件目录的变化,并把事件回调到主线程供 UI 层消费。
该能力是 @Deprecated 的旧式辅助工具,但依然完整地展示了 SDK 中"文件监听 → 回调分发"的设计模式,新代码可基于 OtaFileObserver + FileObserverCallback 自行实现等价逻辑。
Architecture
flowchart TD
subgraph sg_External["外部写入方"]
FS["外部进程 / 下载任务"]
App["业务 App / UI 层"]
end
subgraph sg_SDK["JL OTA SDK (tool.file)"]
Helper["OtaFileObserverHelper<br/>(单例 / @Deprecated)"]
Observer["OtaFileObserver<br/>(extends FileObserver)"]
Callback["FileObserverCallback<br/>(interface)"]
Handler["Handler(MainLooper)"]
end
subgraph sg_System["Android 系统"]
FsObserver["android.os.FileObserver"]
Dir["固件文件目录<br/>(watchPath)"]
end
FS -->|"写入/修改/删除"| Dir
FsObserver -->|"inotify 事件"| Observer
Observer -->|"onEvent(event, path)"| Helper
Helper -->|"mHandler.post 切主线程"| Handler
Handler -->|"遍历分发"| Callback
Callback -->|"onChange(event, path)"| App
App -->|"registerFileObserverCallback"| Helper
App -->|"setFirmwareFilePath / setFirmwareFileData"| Dir
架构说明:
OtaFileObserver是唯一直接与 Android 系统交互的类,它继承自android.os.FileObserver,系统通过 inotify 机制在目录内容变化时回调onEvent(int event, String path)。OtaFileObserverHelper采用双重校验锁(DCL)单例,内部持有唯一的OtaFileObserver实例和一份回调列表ArrayList<FileObserverCallback>,负责把底层监听事件扇出(fan-out)给所有注册的业务回调。- 回调分发通过
Handler(Looper.getMainLooper())切回主线程执行,避免业务回调在系统文件监听线程中运行导致的线程安全问题。 - 业务层(App/UI)只需面向
FileObserverCallback接口编程,无需感知系统 FileObserver 细节,体现了观察者模式 + 依赖倒置的设计意图。
核心实现分析
1. 回调契约:FileObserverCallback
FileObserverCallback 是文件监听事件的唯一回调契约,定义于 FileObserverCallback.java:
public interface FileObserverCallback {
void onChange(int event, String path);
}
设计意图:event 是 Android FileObserver 定义的事件掩码(如 FileObserver.CREATE、FileObserver.MODIFY、FileObserver.DELETE 等),path 是发生变化的文件相对路径。接口保持极简,使调用方可以自行组合事件掩码与路径判断,决定对哪种变化做出响应(例如只关心 CREATE 事件来触发固件解析)。
2. 底层监听器:OtaFileObserver
OtaFileObserver 直接继承 android.os.FileObserver,是系统能力的最小封装,见 OtaFileObserver.java:
public class OtaFileObserver extends FileObserver {
private FileObserverCallback mFileObserverCallback;
public OtaFileObserver(String path) {
super(path);
}
public void setFileObserverCallback(FileObserverCallback fileObserverCallback) {
mFileObserverCallback = fileObserverCallback;
}
@Override
public void onEvent(int event, @Nullable String path) {
if(mFileObserverCallback != null && null != path){
mFileObserverCallback.onChange(event, path);
}
}
}
实现要点:
- 构造时绑定目录:
super(path)把监听的绝对路径交给系统FileObserver。注意 Android 的FileObserver监听的是目录,事件回调中的path是相对该目录的文件名。 - 空值防护:
onEvent中同时检查回调非空与path非空,避免空指针;path == null时事件被丢弃(系统在部分事件上可能不提供路径)。 - 单一回调槽位:该类只持有一个
FileObserverCallback引用,多回调的扇出逻辑由上层OtaFileObserverHelper完成。
3. 单例分发器:OtaFileObserverHelper
OtaFileObserverHelper 是文件监听的门面(Facade),被标记为 @Deprecated,完整实现见 OtaFileObserverHelper.java。
3.1 单例与状态
@Deprecated
public class OtaFileObserverHelper {
private volatile static OtaFileObserverHelper instance;
private OtaFileObserver mOtaFileObserver;
private boolean isWatching;
private String watchPath;
private final ArrayList<FileObserverCallback> mFileObserverCallbacks = new ArrayList<>();
private final Handler mHandler = new Handler(Looper.getMainLooper());
...
public static OtaFileObserverHelper getInstance() {
if (null == instance) {
synchronized (OtaFileObserverHelper.class) {
if (null == instance) {
instance = new OtaFileObserverHelper();
}
}
}
return instance;
}
}
设计要点:
- DCL 双重校验锁单例:
volatile修饰instance防止指令重排,保证多线程下只初始化一次。单例意味着全进程共享一个监听器实例和一套回调列表。 - 主线程 Handler:
mHandler = new Handler(Looper.getMainLooper())是所有回调最终执行的线程锚点。 - 回调列表:
ArrayList<FileObserverCallback>保存所有注册的业务回调。
3.2 路径更新与回调绑定
public void updateObserverPath(String observerPath) {
if (isWatching()){
stopObserver();
}
watchPath = observerPath;
mOtaFileObserver = new OtaFileObserver(watchPath);
mOtaFileObserver.setFileObserverCallback((event, path) -> mHandler.post(() -> {
if (!mFileObserverCallbacks.isEmpty()) {
for (FileObserverCallback callback : new ArrayList<>(mFileObserverCallbacks)) {
callback.onChange(event, path);
}
}
}));
}
关键行为:
- 换路径先停监听:若当前正在监听,先
stopObserver(),避免新旧监听器并存。 - Lambda 绑定回调:系统
onEvent触发后,通过mHandler.post(...)把事件投递到主线程队列。 - 防并发修改的快照遍历:分发时使用
new ArrayList<>(mFileObserverCallbacks)拷贝副本,防止遍历期间其他线程调用unregisterFileObserverCallback触发ConcurrentModificationException。 - 空列表短路:没有注册回调时直接跳过循环,降低主线程开销。
3.3 注册 / 注销 / 启停 / 销毁
public void registerFileObserverCallback(FileObserverCallback callback) {
if (callback != null && !mFileObserverCallbacks.contains(callback)) {
mFileObserverCallbacks.add(callback);
}
}
public void unregisterFileObserverCallback(FileObserverCallback callback) {
if (callback != null && !mFileObserverCallbacks.isEmpty()) {
mFileObserverCallbacks.remove(callback);
}
}
public void startObserver() {
if (mOtaFileObserver != null) {
mOtaFileObserver.startWatching();
isWatching = true;
}
}
public void stopObserver() {
mOtaFileObserver.stopWatching();
isWatching = false;
}
public void destroy() {
stopObserver();
mOtaFileObserver.setFileObserverCallback(null);
mFileObserverCallbacks.clear();
instance = null;
}
行为分析:
registerFileObserverCallback通过contains去重,同一回调重复注册只保留一份;空指针直接忽略。unregisterFileObserverCallback对callback == null与空列表均有防护。startObserver仅在监听器已创建(路径已更新)时生效,并把isWatching置为true;stopObserver未判空,暗示调用方需保证mOtaFileObserver非空(先updateObserverPath再启停的常规顺序)。destroy是完整清理:停止监听 → 解绑回调 → 清空列表 → 置空单例,使下一次getInstance()重建全新状态。
4. 固件文件配置入口
固件文件通过 BluetoothOption 配置,示例见 OtaDemo.java:
bluetoothOption.setFirmwareFilePath(firmwarePath); //设置本地存储OTA文件的路径
// bluetoothOption.setFirmwareFileData(firmwareData);//设置本地存储OTA文件的数据
来源:OtaDemo.java
以及升级前重新设置路径的用法:
otaManager.getBluetoothOption().setFirmwareFilePath(filePath);
//* 进行OTA升级,然后根据回调进行UI更新
来源:OtaDemo.java
设计意图:路径方式让 SDK 按需流式读取文件(对大固件更友好),字节数据方式则把文件内容完全交给调用方内存管理。二者择一使用,配置后即作为后续 OTA 流程的固件输入源。
核心流程
文件监听事件流
sequenceDiagram
participant FS as 外部写入方
participant Dir as 固件目录(watchPath)
participant FO as OtaFileObserver
participant H as OtaFileObserverHelper
participant Handler as Handler(MainLooper)
participant App as 业务回调(FileObserverCallback)
App->>H: updateObserverPath(path)
App->>H: registerFileObserverCallback(cb)
App->>H: startObserver()
H->>FO: startWatching()
FS->>Dir: 写入/修改固件文件
Dir->>FO: inotify 事件
FO->>FO: onEvent(event, path)
Note over FO: 检查 callback!=null && path!=null
FO->>H: callback.onChange(event, path)
H->>Handler: mHandler.post(Runnable)
Handler->>App: onChange(event, path) (主线程)
App->>App: 解析/刷新固件 UI
App->>H: unregisterFileObserverCallback(cb)
App->>H: stopObserver() / destroy()
H->>FO: stopWatching()
流程说明:
- 业务层先
updateObserverPath指定要监听的固件目录(若正在监听会先停止旧监听)。 - 注册业务回调(可注册多个,内部去重),随后
startObserver启动系统监听。 - 外部进程写入/修改/删除目录中的文件时,Android inotify 机制驱动
OtaFileObserver.onEvent。 onEvent空值校验通过后,把事件转发给 Helper 中绑定的 Lambda 回调。- Lambda 通过
mHandler.post将事件投递到主线程,遍历回调快照依次调用onChange(event, path)。 - 业务层在回调中判断事件类型与路径,决定是否触发固件解析或 UI 刷新。
- 页面销毁或不再需要监听时,注销回调并
stopObserver(或整体destroy重置单例)。
固件文件配置流程
flowchart LR
A["initOTA(context, firmwarePath)"] --> B["构建 OtaManager"]
B --> C["构建 BluetoothOption"]
C --> D{"选择固件输入方式"}
D -->|"文件路径"| E["setFirmwareFilePath(path)"]
D -->|"字节数据"| F["setFirmwareFileData(data)"]
E --> G["OTA 升级流程读取固件"]
F --> G
流程依据:OtaDemo.java
Usage Examples
示例一:初始化 OTA 并指定固件文件路径
public void initOTA(Context context, String firmwarePath) {
OtaManager otaManager = new OtaManager();
BluetoothOption bluetoothOption = new BluetoothOption();
bluetoothOption.setPriority(BluetoothOTAConfigure.PREFER_BLE) //请按照项目需要选择
.setUseAuthDevice(true) //具体根据固件的配置选择
.setBleIntervalMs(500) //默认是500毫秒
.setUseReconnect(false); //是否自定义回连方式,默认为false,走SDK默认回连方式,客户可以根据需求进行变更
bluetoothOption.setFirmwareFilePath(firmwarePath); //设置本地存储OTA文件的路径
// bluetoothOption.setFirmwareFileData(firmwareData);//设置本地存储OTA文件的数据
}
来源:OtaDemo.java
该示例展示了固件文件的推荐配置方式:在构建 OtaManager 与 BluetoothOption 时即设置固件路径,注释中保留了 setFirmwareFileData 的备选方案供按需切换。
示例二:升级前重新绑定固件文件
public void OtaFirmware(final String filePath) {
//1.构建OTAManager对象
...
otaManager.getBluetoothOption().setFirmwareFilePath(filePath);
//* 进行OTA升级,然后根据回调进行UI更新
}
来源:OtaDemo.java
该示例说明固件路径可以在升级动作前随时重新设置——SDK 读取固件发生在升级流程内部,调用方只需保证在触发升级前路径有效且文件完整。
示例三:自定义文件监听(基于 OtaFileObserver + FileObserverCallback)
结合三个工具类的最小监听实现模式(摘自各文件定义):
// 1. 实现回调契约
FileObserverCallback callback = (event, path) -> {
// 主线程回调,可安全更新 UI
if ((event & FileObserver.CREATE) != 0) {
// 固件文件已创建,触发解析
}
};
// 2. 通过单例 Helper 注册并启动
OtaFileObserverHelper helper = OtaFileObserverHelper.getInstance();
helper.updateObserverPath(otaFileDir); // 指定固件目录
helper.registerFileObserverCallback(callback);
helper.startObserver();
// 3. 页面销毁时清理
helper.unregisterFileObserverCallback(callback);
helper.destroy();
接口与实现来源:FileObserverCallback.java、OtaFileObserverHelper.java
Configuration Options
固件文件管理与监听相关的可配置项如下:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
BluetoothOption.setFirmwareFilePath(String) | String | 无(必填其一) | 本地固件文件路径,SDK 按需读取 |
BluetoothOption.setFirmwareFileData(byte[]) | byte[] | 无(必填其一) | 固件字节数据,路径与数据二选一 |
BluetoothOption.setBleIntervalMs(int) | int | 500 | BLE 发送间隔(毫秒),影响固件传输节奏 |
BluetoothOption.setUseAuthDevice(boolean) | boolean | 依固件而定 | 是否使用认证设备流程 |
BluetoothOption.setUseReconnect(boolean) | boolean | false | 是否自定义回连方式;false 走 SDK 默认回连 |
BluetoothOption.setPriority(...) | 枚举 | 依项目而定 | 蓝牙通道优先级(如 BluetoothOTAConfigure.PREFER_BLE) |
OtaFileObserverHelper.updateObserverPath(String) | String | 无 | 要监听的固件文件目录绝对路径 |
OtaFileObserverHelper.registerFileObserverCallback(FileObserverCallback) | 回调 | 无 | 注册监听回调(内部去重) |
OtaFileObserverHelper.startObserver() | — | 未监听 | 启动系统文件监听 |
API Reference
interface FileObserverCallback
文件监听事件的回调契约。
void onChange(int event, String path)
参数:
event(int):AndroidFileObserver事件掩码,如FileObserver.CREATE、MODIFY、DELETE、MOVED_TO等,可组合判断。path(String):发生变化的文件相对监听目录的路径;Helper 分发时保证非空。
说明: 通过 OtaFileObserverHelper 注册时,回调在主线程执行,可安全更新 UI。
class OtaFileObserver extends android.os.FileObserver
系统文件监听的最小封装。
OtaFileObserver(String path)
参数:
path(String):需要监听的目录绝对路径。
说明: 直接透传给 FileObserver 构造器,未指定事件掩码,使用系统默认全事件监听。
void setFileObserverCallback(FileObserverCallback callback)
参数:
callback(FileObserverCallback):事件回调,仅支持单个槽位。
void onEvent(int event, @Nullable String path)
重写行为: 当回调非空且 path 非空时调用 callback.onChange(event, path);否则静默丢弃事件。
class OtaFileObserverHelper(@Deprecated)
文件监听单例辅助类。
static OtaFileObserverHelper getInstance()
返回: 进程内唯一实例(DCL 双重校验锁,volatile 保证可见性)。
void updateObserverPath(String observerPath)
参数:
observerPath(String):新监听目录。
行为: 若正在监听先停止旧监听;重建 OtaFileObserver 并绑定主线程分发 Lambda。
void registerFileObserverCallback(FileObserverCallback callback)
参数:
callback(FileObserverCallback):待注册回调;空指针忽略,重复注册去重。
void unregisterFileObserverCallback(FileObserverCallback callback)
参数:
callback(FileObserverCallback):待注销回调;空指针或空列表时安全返回。
boolean isWatching()
返回: 是否处于监听状态。
String getWatchPath()
返回: 当前监听的目录路径,未设置时为 null。
void startObserver()
行为: 监听器存在时调用 startWatching() 并置 isWatching = true。
void stopObserver()
行为: 调用 stopWatching() 并置 isWatching = false。注意:未判空,需保证先 updateObserverPath。
void destroy()
行为: 停止监听 → 解绑回调 → 清空回调列表 → 置空单例,完整重置状态。
BluetoothOption 固件配置方法
void setFirmwareFilePath(String path)
参数: path (String):本地固件文件路径。
说明: 与 setFirmwareFileData 二选一;路径方式适合大固件按需读取。
void setFirmwareFileData(byte[] data)
参数: data (byte[]):固件完整字节数据。
说明: 与 setFirmwareFilePath 二选一;数据方式把文件内容交由调用方内存管理。
失败模式、边界情况与并发
- 监听器未创建就启停:
stopObserver()与startObserver()未对mOtaFileObserver判空。若调用方在updateObserverPath之前直接调用stopObserver(),会触发空指针。正确用法是先更新路径再启停(源码中updateObserverPath内部已保证该顺序)。 path == null事件被丢弃:Android 部分FileObserver事件可能不带路径,OtaFileObserver.onEvent对此类事件静默忽略。依赖path的业务逻辑需自行处理该情况。- 回调遍历的并发安全:分发时通过
new ArrayList<>(mFileObserverCallbacks)快照遍历,规避了回调在遍历中被注销导致的ConcurrentModificationException;但注册/注销本身并非线程安全,建议在主线程操作。 - 重复注册去重:
registerFileObserverCallback用contains判重,同一回调多次注册只生效一次,避免事件重复分发。 - 单例状态残留:
destroy()将instance置空,但已持有旧引用的调用方若继续调用其方法,会操作已清理的对象(回调列表为空、监听已停),行为是"安全空转"而非崩溃。 - 目录级监听粒度:
FileObserver监听的是目录,回调中的path是相对文件名;业务层需自行过滤非固件文件(如校验扩展名)以免误触发。
性能与运维考量
- 主线程分发代价:所有回调经
mHandler.post在主线程串行执行。高频文件事件(如固件分块写入产生的连续MODIFY)会积压主线程队列,回调内应避免耗时操作,可做事件合并或节流。 - 单例生命周期:Helper 全进程共享,建议在 Application 或 OTA 专属页面级管理注册/注销,避免页面重建导致回调泄漏。
- 事件风暴防护:SDK 示例注释中保留了
JL_Log.d调试日志(默认关闭),生产环境无需开启;对大规模文件写入场景,建议在回调中按事件类型聚合处理。
扩展点
- 自定义监听粒度:
OtaFileObserver构造器目前未传事件掩码(系统默认全事件)。业务方可继承FileObserver并按需传入掩码,例如只监听CREATE | MOVED_TO以捕获固件文件就绪时刻。 - 自定义回调实现:
FileObserverCallback为单方法接口,可配合 Java 8 Lambda 或实现类实现多态分发(如按event类型路由到不同处理器)。 - 替代 Helper 的自建分发:由于
OtaFileObserverHelper已@Deprecated,新代码可自行组合OtaFileObserver+ 自己的注册表/线程策略,保持同样的观察者模式而获得更强的可控性。 - 固件输入方式扩展:
setFirmwareFilePath/setFirmwareFileData双入口为调用方提供了"流式读取"与"内存直传"两种固件供给策略,可据此对接下载完成通知、加密固件解包等前置处理。
Tests
仓库中 otasdk/src/test/java/com/jieli/otasdk/OtaDemo.java 提供了固件文件配置与 OTA 初始化的演示级测试代码,覆盖:
- 通过路径方式配置固件文件并初始化 OTA(
initOTA) - 升级前重新设置固件路径(
OtaFirmware) - 设备强制升级场景中基于版本号/UID 判断固件支持性(回调注释说明)
该文件是 SDK 面向集成方的典型用法范例,可作为固件文件管理入口的参考集成模板。文件监听三件套(FileObserverCallback / OtaFileObserver / OtaFileObserverHelper)本身未附带独立单元测试,其行为需通过真机目录操作验证。
Related Links
- OtaFileObserver.java — 系统文件监听封装
- OtaFileObserverHelper.java — 单例分发辅助类
- FileObserverCallback.java — 回调接口定义
- OtaDemo.java — 固件文件配置与 OTA 初始化示例
- 蓝牙连接与 OTA 传输细节 → 参见蓝牙管理相关目录页
- OTA 升级流程与进度回调 → 参见 OTA 流程相关目录页