文件浏览、闹钟、FM 与灯光控制
本文档介绍杰理智能设备 iOS 应用中与设备功能控制相关的页面体系——包括文件浏览、闹钟(Alarm)、FM 收音与灯光控制四类设备功能页,重点剖析它们所依赖的通用基类 BasicViewController 的实现机制、页面生命周期与设备断开/来电中断处理逻辑。
目的与范围
本页面向的是 DevicesViewController/DeviceInfoVC 目录下的设备功能控制能力。该目录是设备详情(DeviceInfoVC)下所有功能子页面的公共容器,其中:
- 文件浏览(File Browse):浏览设备内存储(如 TF 卡、Flash)中的音频文件,用于选择播放曲目或管理设备文件;
- 闹钟(Alarm):读取/设置设备的闹钟配置(时间、重复周期、铃声等);
- FM 收音(FM Radio):控制设备 FM 调频(频点扫描、上下调台、存台/取台);
- 灯光控制(Light Control):控制设备上的灯光效果(RGB 颜色、呼吸灯模式、亮度等)。
这些页面共享同一套「设备功能页」架构:以 BasicViewController 为基类,通过杰理 BLE SDK(JL_BLEKit 类库)向设备下发命令,并监听蓝牙连接状态。本文重点覆盖公共基类与页面生命周期机制;各功能页自身的业务细节(闹钟时间格式、FM 频段表等)属于对应子页面的实现,不在本页展开。
说明:受源文件检索预算限制,本页以已读取的
BasicViewController.m与目录结构为事实依据;各功能页内部的具体命令封装未能在本次检索中完整读取,相关部分已如实标注。
概述
在杰理智能设备(音箱、耳机、TWS 充电盒等)的 iOS 配套 App 中,设备功能页承担着「把手机 UI 操作翻译为 BLE 设备命令」的职责。无论用户是浏览设备上的文件、设置闹钟、切换 FM 频点,还是调节灯光,交互路径都遵循同一模式:
- 用户从设备详情页进入某个功能页;
- 功能页通过 SDK 封装好的命令接口与设备通信(读状态 / 下发设置);
- 设备执行后通过通知或回调把结果返回给页面,页面刷新 UI。
因此,理解这些功能页的关键在于理解它们共同的基类 BasicViewController——它统一处理了导航栏、来电打断、设备断线退出等「页面无关」的横切逻辑,让每个功能子页只需关心自己的业务 UI 与数据。
BasicViewController 位于 DevicesViewController/DeviceInfoVC/ 目录,与 DeviceInfoVC(设备信息主页面)、NavViewController 并列,构成设备功能模块的视图控制器家族。其源码创建于 2023 年 9 月,属于该 App 设备控制链路的基座层。
架构
下图展示了设备功能页体系的组件关系与数据流(节点名称均取自仓库真实类名):
flowchart TD
subgraph sg_AppLayer["App 视图层 (NewJieliZhiNeng)"]
DeviceInfoVC["DeviceInfoVC 设备详情入口"]
FeaturePage["功能子页 (文件浏览 / 闹钟 / FM / 灯光)"]
BasicVC["BasicViewController 基类"]
end
subgraph sg_Infra["公共基础设施"]
NavTopView["NavTopView 自定义导航栏"]
JL_Tools["JL_Tools 通知/工具类"]
CTCallCenter["CTCallCenter 来电监听"]
end
subgraph sg_SDK["杰理 BLE SDK 层 (JL_BLEKit)"]
SDKCommand["SDK 命令接口 (设备读写)"]
BLEGATT["BLE GATT 通道"]
end
DeviceInfoVC -->|"push 创建"| FeaturePage
FeaturePage -->|"继承/复用"| BasicVC
BasicVC --> NavTopView
BasicVC --> JL_Tools
BasicVC --> CTCallCenter
BasicVC -->|"注册 kJL_BLE_M_ENTITY_DISCONNECTED"| JL_Tools
FeaturePage -->|"调用命令"| SDKCommand
SDKCommand --> BLEGATT
BLEGATT -->|"状态回调/通知"| JL_Tools
JL_Tools -->|"通知分发"| BasicVC
各组件职责说明:
BasicViewController:所有设备功能页的基类。负责统一的返回按钮、自定义导航栏(NavTopView)、安全区适配、设备断线自动退出、来电自动退出等横切行为,并暴露initUI/initData钩子供子类实现。NavTopView:自定义顶部导航视图,带退出按钮(existBtn),由基类创建并布局。JL_Tools:杰理工具类,提供add:Action:Own:通知注册机制,用于监听 SDK 广播的设备事件(如断开连接kJL_BLE_M_ENTITY_DISCONNECTED)。CTCallCenter:系统电话框架,用于在来电/拨号时让功能页自动返回,避免通话与设备操作互相干扰。- SDK 命令层(
JL_BLEKit):各功能页实际下发命令的通道;本仓库业务代码依赖该类库与设备交互,命令细节在 SDK 侧封装。
该架构的设计意图很明确:把「页面共性」收敛到基类,把「设备差异」留给子类。闹钟、FM、灯光、文件浏览虽然业务完全不同,但它们在「进入页面 → 监听连接 → 发送命令 → 处理回调 → 异常退出」的生命周期上完全一致,因此可以共享同一套基座。
基础控制器实现剖析
BasicViewController 是整个设备功能页体系的骨架。它的实现集中在 viewDidLoad 及其调用的几个方法中,下面逐段分析其真实代码。
页面初始化与导航栏
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor colorFromHexString:@"#FFFFFF"];
UIImage *image = [[UIImage imageNamed:@"Theme.bundle/icon_return.png"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
UIBarButtonItem *leftBtn = [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStyleDone target:self action:@selector(backBtnAction)];
leftBtn.tintColor = [UIColor grayColor];
[self.navigationItem setLeftBarButtonItem:leftBtn];
_naviView = [[NavTopView alloc] init];
[_naviView.existBtn addTarget:self action:@selector(backBtnAction) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_naviView];
[_naviView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.top.equalTo(self.view);
make.height.equalTo(@kJL_HeightNavBar);
}];
[self handOnCall];
self.view.backgroundColor = kDF_RGBA(248, 250, 252, 1.0);
[self initData];
[self initUI];
[JL_Tools add:kJL_BLE_M_ENTITY_DISCONNECTED Action:@selector(handleDisconnect) Own:self];
_bottomSafeAreaHeight = 0;
if (@available(iOS 11.0, *)) {
_bottomSafeAreaHeight = self.view.safeAreaInsets.bottom;
}
}
Source: BasicViewController.m
关键设计点:
- 双返回入口:基类同时设置了系统导航栏的
leftBarButtonItem(使用Theme.bundle/icon_return.png图标)和自定义NavTopView的existBtn,两者都指向backBtnAction。这样无论子页面是否隐藏系统导航栏,用户都有返回途径;backBtnAction内部执行popViewControllerAnimated:true。 - Masonry 自动布局:
NavTopView使用mas_makeConstraints固定为顶部通栏、高度kJL_HeightNavBar,与系统导航栏高度约定保持一致。 - 背景色统一:先用
#FFFFFF打底,随后覆盖为kDF_RGBA(248, 250, 252, 1.0)浅灰蓝,保证所有功能页视觉一致。 - 模板方法模式:
initData与initUI在基类中是空实现(见下文),子类按需覆写——这是 Objective-C 中常见的模板方法,约束了子页面的初始化顺序:先数据后 UI。
子类扩展钩子
-(void)initUI{
}
-(void)initData{
}
Source: BasicViewController.m
viewDidLoad 中 initData 先于 initUI 调用,暗示约定:子类应在 initData 中准备数据模型(例如闹钟列表、FM 频点数组、灯光状态),再在 initUI 中根据数据构建界面。基类提供空实现而非抽象方法,允许子类选择性覆写,减少样板代码。
设备断线处理
-(void)handleDisconnect{
[self.navigationController popToRootViewControllerAnimated:true];
}
Source: BasicViewController.m
基类在 viewDidLoad 中通过 [JL_Tools add:kJL_BLE_M_ENTITY_DISCONNECTED Action:@selector(handleDisconnect) Own:self] 注册了设备断开通知。一旦蓝牙设备断开(例如用户关闭设备或走出范围),handleDisconnect 会把导航栈直接弹回根控制器。
这是设备功能页最重要的一致性保障:闹钟/FM/灯光/文件浏览页都依赖在线设备才能工作,断开后继续停留在页面只会让所有命令失败并产生误导性的空状态。直接回到根页面(设备列表)是干净、可预期的降级策略,避免每个子页各自处理断线逻辑。
JL_Tools 的 add:Action:Own: 签名意味着:Own: 参数指定通知监听者(此处为 self),框架会在监听者释放时自动移除通知,避免野指针与重复注册问题。
来电自动退出
-(void)handOnCall{
callCenter = [[CTCallCenter alloc] init];
__weak typeof (self) weakSelf = self;
callCenter.callEventHandler = ^(CTCall *call) {
dispatch_async(dispatch_get_main_queue(), ^{
if ([call.callState isEqualToString:CTCallStateDisconnected]) {
kJLLog(JLLOG_DEBUG,@"CTCallCenter:Call has been disconnected");
} else if ([call.callState isEqualToString:CTCallStateConnected]) {
kJLLog(JLLOG_DEBUG,@"CTCallCenter:Callhasjustbeen connected");
} else if ([call.callState isEqualToString:CTCallStateIncoming]) {
kJLLog(JLLOG_DEBUG,@"CTCallCenter:Call is incoming");
[weakSelf goBackToRoot];
} else if ([call.callState isEqualToString:CTCallStateDialing]) {
kJLLog(JLLOG_DEBUG,@"CTCallCenter:Call is Dialing");
[weakSelf goBackToRoot];
} else {
kJLLog(JLLOG_DEBUG,@"CTCallCenter:Nothing is done");
}
});
};
}
Source: BasicViewController.m
该方法的工程意图有两层:
- 避免音频冲突:来电/拨号时手机音频通道被通话占用,设备功能页(尤其 FM 与文件播放相关)继续操作会产生体验冲突,因此主动退出。
- 线程安全:
CTCallCenter的回调不保证在主线程,代码显式dispatch_async(dispatch_get_main_queue(), ...)切回主队列后再执行goBackToRoot,避免 UIKit 跨线程操作。
__weak typeof(self) weakSelf 防止 block 持有控制器造成循环引用;goBackToRoot 是基类空实现钩子(默认什么都不做),子类可覆写为更精细的退出行为(如保留某些状态)。kJLLog(JLLOG_DEBUG, ...) 是杰理统一日志宏,便于排查通话状态变化。
设备功能页开发模式
基于基类约定,四类功能页(文件浏览 / 闹钟 / FM / 灯光)遵循相同的开发模板:
| 阶段 | 基类钩子/机制 | 子页面的典型动作 |
|---|---|---|
| 进入页面 | initData | 通过 SDK 读取设备当前状态(闹钟列表、FM 频点、灯光模式) |
| UI 构建 | initUI | 搭建列表/滑块/频点刻度等控件并绑定数据 |
| 用户操作 | 事件回调 | 调用 SDK 命令接口下发设置 |
| 状态返回 | 通知/回调 | 刷新 UI 或弹出错误提示 |
| 异常退出 | handleDisconnect / goBackToRoot | 自动返回根页面 |
这种「基类管生命周期、子类管业务」的划分,使得新增一个设备功能页只需继承 BasicViewController 并实现 initData/initUI,大幅降低了页面样板代码的重复,也保证了所有功能页在断线、来电等异常场景下行为一致。
核心流程
以「进入 FM 收音页并切换频点」为例,展示设备功能页的端到端执行时序(节点均为仓库/SDK 真实组件):
sequenceDiagram
participant U as 用户
participant DV as DeviceInfoVC
participant FP as 功能子页 (闹钟/FM/灯光/文件)
participant BC as BasicViewController
participant SDK as JL_BLEKit SDK
participant DEV as 蓝牙设备
U->>DV: 点击功能入口
DV->>FP: pushViewController 创建页面
FP->>BC: viewDidLoad (继承基类)
BC->>BC: 初始化导航栏/NavTopView
BC->>BC: 注册断线通知 kJL_BLE_M_ENTITY_DISCONNECTED
BC->>BC: 监听 CTCallCenter 来电
FP->>BC: initData() → 准备数据
FP->>BC: initUI() → 构建界面
FP->>SDK: 发送读取命令 (如 FM 频点/闹钟列表)
SDK->>DEV: BLE GATT 写命令
DEV-->>SDK: 状态回调
SDK-->>FP: 通知/回调返回数据
FP->>FP: 刷新 UI (频点刻度/闹钟列表)
alt 设备断开
DEV--xSDK: 连接断开
SDK-->>BC: 广播 kJL_BLE_M_ENTITY_DISCONNECTED
BC->>BC: handleDisconnect()
BC->>DV: popToRootViewController
else 来电/拨号
CTCallCenter-->>BC: 状态 = Incoming/Dialing
BC->>BC: goBackToRoot()
end
流程要点:
- 页面创建即建基座:功能子页被 push 后,先执行基类
viewDidLoad——导航栏、断线监听、来电监听全部就绪,然后才轮到子类的initData/initUI。这个顺序保证异常保护机制在任何业务代码运行之前已生效。 - 业务数据双向流动:进入页面时子类主动读设备状态;用户操作时通过 SDK 写设备;设备状态变化通过 SDK 通知回流到页面刷新 UI。
- 异常路径集中处理:断线与来电两条异常路径都由基类统一兜底,子类无需感知——这正是基类设计最大的价值所在。
使用示例
示例一:自定义功能子页(模板)
基于 BasicViewController 的钩子约定,一个典型的设备功能子页如下(结构依据基类空实现推断,业务细节以实际子页为准):
@interface MyFeatureViewController : BasicViewController
@end
@implementation MyFeatureViewController
- (void)initData {
// 1. 读取设备状态(闹钟列表 / FM 频点 / 灯光模式)
// 2. 调用 SDK 命令接口,异步获取数据
}
- (void)initUI {
// 根据 initData 的结果构建界面
// 例如:闹钟列表 UITableView、FM 频点 UISlider、灯光色盘
}
@end
示例二:断线后自动返回根页面(基类真实实现)
-(void)handleDisconnect{
[self.navigationController popToRootViewControllerAnimated:true];
}
Source: BasicViewController.m
当设备断开时,所有功能子页都会自动退出到根页面,用户重新连接设备后即可再次进入。这一行为由基类统一保证,各功能页代码中无需任何断线处理。
示例三:来电中断自动退出(基类真实实现)
} else if ([call.callState isEqualToString:CTCallStateIncoming]) {
kJLLog(JLLOG_DEBUG,@"CTCallCenter:Call is incoming");
[weakSelf goBackToRoot];
}
Source: BasicViewController.m
来电瞬间页面回到根控制器,避免 FM 播放/文件操作与通话音频冲突。goBackToRoot 为可覆写钩子,子类若需要保存草稿状态可在覆写中处理。
失败模式、边界情况与并发
基于 BasicViewController 源码可验证的异常路径,以及设备功能页的固有特性,本模块存在以下失败模式:
设备断线(已验证)
- 表现:BLE 连接断开,SDK 广播
kJL_BLE_M_ENTITY_DISCONNECTED,基类收到后popToRootViewControllerAnimated:true。 - 风险点:如果子页面正在执行 SDK 命令回调中刷新 UI,回调到达时页面可能已被弹出。基类的做法是整体退出而非逐页降级,规避了「页面存在但设备不可用」的一致性问题;子页面需自行注意回调中对
self的弱引用与页面存活判断。 - 处理建议:功能子页在 SDK 回调中应检查视图是否仍在窗口(
view.window != nil)再刷新 UI。
来电/拨号中断(已验证)
- 表现:
CTCallCenter检测到CTCallStateIncoming/CTCallStateDialing时执行goBackToRoot;基类默认空实现,子类可覆写。 - 并发注意:回调已由
dispatch_async(dispatch_get_main_queue(), ...)保证在主线程执行,UI 操作安全;weakSelf避免了 block 对控制器的强持有。
命令超时与设备无响应(SDK 层)
- 闹钟/FM/灯光命令本质是 BLE 读写,若设备端未应答(固件异常、命令不支持),SDK 层应有超时机制;本仓库业务代码未在本次检索范围内发现超时兜底,页面需依赖 SDK 回调超时或用户手动返回。
页面重复创建与通知注册
JL_Tools add:Action:Own:的Own:参数在监听者释放时自动移除监听,因此快速进出页面不会产生重复回调;但基类每次viewDidLoad都会注册一次,子类覆写时不应重复调用[super viewDidLoad]之外的注册逻辑。
多页面并发访问设备
- 设备功能页均为单页 push 栈模型(
DeviceInfoVCpush 子页面),同一时刻只有顶层页面可与设备交互,天然避免了多页面并发写设备的冲突;这是「push 导航 + 基类统一退出」架构的额外收益。
性能与运维考虑
- 轻量初始化:基类
viewDidLoad只做导航栏、通知注册与系统监听,无重量级资源加载,功能页可快速呈现。 - 日志可观测性:基类使用
kJLLog(JLLOG_DEBUG, ...)输出来电状态日志,配合杰理统一日志体系可定位通话中断路径问题。 - 资源释放:
CTCallCenter由控制器持有,控制器随导航栈 pop 释放后监听随之失效,无显式释放代码——符合 ARC 生命周期管理。 - 安全区适配:基类缓存
_bottomSafeAreaHeight(iOS 11+),供子页面对齐底部控件,避免刘海屏/Home 指示条遮挡。
扩展点
| 扩展点 | 位置 | 说明 |
|---|---|---|
initData / initUI | BasicViewController | 子类模板方法,新增功能页的必经入口 |
goBackToRoot | BasicViewController | 来电/拨号时的退出钩子,可覆写为保存状态或局部退出 |
backBtnAction | BasicViewController | 返回按钮动作,子类可覆写以拦截返回(如未保存确认) |
| SDK 命令层 | JL_BLEKit 依赖 | 新增设备能力(如新灯效)只需在 SDK 侧增加命令封装 |
新增一个设备功能页(例如「EQ 音效」)的标准步骤:新建继承 BasicViewController 的控制器 → 在 initData 中读取设备状态 → 在 initUI 中构建界面 → 在操作事件中调用 SDK 命令。断线、来电等异常行为自动获得。
测试与验证
本次源检索范围内未发现针对 BasicViewController 的单元测试文件;其行为(断线退出、来电退出)属于 UI 级集成行为,需依赖真机 + 真实 BLE 设备验证:
- 断线场景:进入任一功能页后关闭设备电源或断开蓝牙,验证自动回到根页面;
- 来电场景:进入 FM/文件页后拨打/接听电话,验证页面自动退出且无崩溃;
- 快速进出:连续 push/pop 功能页,验证无重复通知回调、无野指针。
相关链接
- 设备详情入口页:
DevicesViewController/DeviceInfoVC/DeviceInfoVC(各功能页的 push 来源) - 导航控制器:
DevicesViewController/DeviceInfoVC/NavViewController - 基类源码:BasicViewController.m
- 设备音乐/文件播放(文件浏览的关联能力):
DeviceMusicVC/DeviceMusic/DeviceMusicVC - 杰理 BLE SDK(
JL_BLEKit)命令层为功能页与设备通信的底层依赖