杰理之家 App 架构与导航
杰理之家(NewJieliZhiNeng)是杰理蓝牙 SDK 的 iOS 主应用示例,本文档深入解析其应用外壳(App Shell)与导航体系:从 AppDelegate/SceneDelegate 的启动流程、窗口与根控制器管理、MainTabBarVC 主导航壳,到品牌化开屏动画与隐私协议门禁的完整机制。
Purpose and Scope
本文档覆盖「杰理之家 App 架构与导航」这一能力主题,即应用级外壳与导航骨架:
- 应用启动入口(
AppDelegate/SceneDelegate)与各阶段的初始化顺序 - 窗口(
UIWindow)创建、根控制器(Root View Controller)切换与隐私协议门禁 - 主导航容器
MainTabBarVC及其挂载的顶层模块控制器 - 双品牌(杰理之家 / PiLink)开屏动画机制与语言环境检测
- 全局基础设施(日志、数据库、网络监测、用户登录、Bugly)的装配方式
以下内容属于兄弟页面,不在本文展开,仅在必要时交叉引用:
- 蓝牙连接与 RCSP 协议控制(
JL_RunSDK的用法见 SDK 相关文档页) - 具体功能模块(如
DeviceInfoVC设备信息、EQSettingVC音效、UpgradeVC固件升级、MultiMediaVC多媒体)的内部实现 - 网络接口层(
Http接口/与User_Http)的接口明细
本文所有结论均基于仓库源码验证;未在源码中核实的实现细节会明确标注「源码中未找到」。
Overview
杰理之家 Demo 是 iOS-JL_Bluetooth 仓库中位于 code/JieLi_Home_Demo/NewJieliZhiNeng/ 的完整 iOS 应用工程(NewJieliZhiNeng.xcworkspace)。它既是一个可运行的参考应用,也是 SDK 集成方式的活文档:开发者可以通过阅读该应用的外壳层了解一个基于杰理 SDK 的蓝牙 App 应该如何组织生命周期、导航与全局服务。
该应用外壳层有几个关键设计决策:
- 双品牌复用同一套外壳:通过宏
kJL_UI_SERIES区分「杰理之家」与「PiLink」两套品牌 UI(开屏动画、主题资源),实现一套代码、两套皮肤。 - iOS 13 分水岭:iOS 13+ 由
SceneDelegate管理窗口与根控制器,iOS 12 及以下走setupUI的传统UIWindow路径,兼容性处理被明确写进了启动方法。 - 隐私合规前置:首次启动时以
ConfirmView浮层强制用户确认隐私协议(持久化键CONMIT_PROTOCOL),确认前不进入主界面,避免合规风险。 - 基础设施集中装配:日志、数据库建表、用户登录、网络状态监测、Bugly 崩溃上报等全局服务全部在
didFinishLaunchingWithOptions中按固定顺序初始化,保证后续模块可用。
Architecture
下图展示了杰理之家 App 外壳层的组件关系与数据流:
flowchart TD
subgraph sg_Launch["启动阶段 (AppDelegate)"]
Log["JLLogManager 日志系统"]
Remote["远程事件接收<br/>beginReceivingRemoteControlEvents"]
Lang["语言检测 kJL_GET/kJL_SET"]
DB["SqliteManager createTable"]
User["User_Http shareInstance"]
Net["AFNetworkReachabilityManager"]
Bugly["Bugly 崩溃上报"]
end
subgraph sg_Window["窗口与导航壳"]
Confirm["ConfirmView 隐私协议门禁"]
TabBar["MainTabBarVC 主 TabBar 壳"]
OpenShow["OpenShowView 杰理之家开屏"]
PiLink["PiLinkShowView PiLink 开屏"]
end
subgraph sg_Modules["顶层模块 (Tab 页面)"]
M1["MultiMediaVC 多媒体"]
M2["EQSettingVC 音效设置"]
M3["DeviceInfoVC 设备信息"]
M4["UpgradeVC 固件升级"]
M5["UserProfileVC 用户中心"]
end
AppDelegate["application:didFinishLaunchingWithOptions:"] --> Log
AppDelegate --> Remote
AppDelegate --> Lang
AppDelegate --> DB
AppDelegate --> User
AppDelegate --> Net
AppDelegate --> Bugly
AppDelegate -->|"iOS 13+ 由 SceneDelegate 接管"| TabBar
AppDelegate -->|"iOS 12- 调用 setupUI"| Confirm
Confirm -->|"CONMIT_PROTOCOL == OK"| TabBar
Confirm -->|"未确认"| Blocked["停留空白控制器 tempVC"]
TabBar --> M1
TabBar --> M2
TabBar --> M3
TabBar --> M4
TabBar --> M5
OpenShow -->|"kJL_UI_SERIES == 0"| TabBar
PiLink -->|"kJL_UI_SERIES == 1"| TabBar
架构说明
AppDelegate是整个外壳的编排者:didFinishLaunchingWithOptions(AppDelegate.m L65)按「日志 → 屏幕常亮 → 远程事件 → 语言 → UI → 数据库 → 登录 → 网络 → 崩溃上报 → 通知」的顺序装配全局服务。MainTabBarVC是导航壳的根容器。从AppDelegate.m的导入列表(L9-L35)可以看到该壳直接聚合了MultiMediaVC、EQSettingVC、DeviceInfoVC、UpgradeVC、UserProfileVC等模块控制器,它们是 Tab 页面的顶层入口。- 隐私门禁处于窗口与主壳之间:未确认协议时根控制器被设置为一个空白
UIViewController(tempVC),ConfirmView作为窗口子视图浮于其上;确认后才调用initData切换至真正的MainTabBarVC根控制器。 - 品牌差异点集中在开屏:
kJL_UI_SERIES == 0播放杰理之家OpenShowView动画,== 1播放PiLinkShowView动画,其余外壳逻辑完全共享。
应用入口与启动流程
1. 启动方法的完整时序
所有初始化都集中在 application:didFinishLaunchingWithOptions:(AppDelegate.m L65-L142)。它按固定顺序执行以下阶段,每个阶段的顺序都有设计意图:
阶段 A:日志系统先行
/*--- 记录NSLOG ---*/
[JLLogManager setLog:true IsMore:false Level:JLLOG_DEBUG];
[JLLogManager clearLog];
[JLLogManager logWithTimestamp:true];
[JLLogManager saveLogAsFile:true];
kJLLog(JLLOG_DEBUG, @"app version:%@,build version:%@",[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],[NSBundle mainBundle].infoDictionary[@"CFBundleVersion"]);
Source: AppDelegate.m L67-L73
日志是第一个初始化的服务,因为后续所有启动阶段的排查都依赖它;同时记录 App 的 CFBundleShortVersionString(市场版本号)与 CFBundleVersion(构建号),为线上问题定位提供版本锚点。
阶段 B:系统级行为配置
/*--- 设置屏幕常亮 ---*/
[UIApplication sharedApplication].idleTimerDisabled = YES;
/*--- 远程事件接收---*/
[application beginReceivingRemoteControlEvents];
Source: AppDelegate.m L75-L79
idleTimerDisabled = YES 使屏幕在 App 运行期间不自动息屏——这是蓝牙音箱/耳机控制类 App 的典型需求(用户在操作设备时屏幕不应熄灭)。beginReceivingRemoteControlEvents 用于接收锁屏/耳机线控的远程控制事件,是媒体控制类功能(MultiMediaVC)的前提。
阶段 C:语言环境归一化
/*--- 检测当前语言 ---*/
if ([kJL_GET isEqualToString:@"en"]) {
kJL_SET("en");
}else if([kJL_GET isEqualToString:@"ja"]){
kJL_SET("ja");
}else if ([kJL_GET isEqualToString:@"zh-Hans"]){
kJL_SET("zh-Hans");
}else{
kJL_SET("");
}
Source: AppDelegate.m L81-L90
这里对 SDK 内部的语言偏好(kJL_GET/kJL_SET 为杰理 SDK 的偏好读写宏)做白名单归一化:只保留 en、ja、zh-Hans 三种受支持语言,其余回退为空字符串(走默认语言)。这样 SDK 内部的协议提示、UI 文案与系统语言保持一致,避免出现未翻译的中间状态。
阶段 D:UI 分支(iOS 版本分水岭)
if (@available(iOS 13.0, *)) {
// iOS 13+ 使用 SceneDelegate 管理窗口与根控制器
} else {
[self setupUI];
}
Source: AppDelegate.m L92-L97
iOS 13 引入了 UIScene 生命周期,窗口创建从 AppDelegate 移交给 SceneDelegate。此处用 @available 做运行时兼容:新版系统跳过 setupUI(由 Scene 管理),旧版系统仍走经典 UIWindow 路径。注意此时开屏动画也做了同样的分支(L100-L114),避免新旧两套 UI 体系同时操作窗口造成冲突。
阶段 E:全局服务装配
紧接着依次初始化数据库、用户体系、网络监测、播放器与崩溃上报(L116-L132):
| 顺序 | 服务 | 调用 | 作用 |
|---|---|---|---|
| 1 | 数据库 | [[SqliteManager sharedInstance] createTable] | 创建设备图片、DHA 等本地数据表 |
| 2 | 用户登录 | [User_Http shareInstance] | 初始化用户 HTTP 会话与登录态 |
| 3 | 网络监测 | [[AFNetworkReachabilityManager sharedManager] startMonitoring] | 监听网络可达性,供网络播放等模块使用 |
| 4 | 播放器 | [NetworkPlayer sharedMe] | 初始化网络音频播放器单例 |
| 5 | 崩溃上报 | [Bugly startWithAppId:@"12d9f973f4"] | 启动腾讯 Bugly 崩溃采集 |
Bugly 的条件编译是一个值得注意的工程细节(L36-L43):某些 CocoaPods 渠道的 Bugly 二进制只含真机架构,模拟器链接会失败,因此用 __has_include(<Bugly/Bugly.h>) 做条件导入并定义 HAS_BUGLY 宏;模拟器构建时跳过启动并打印提示日志,避免 Debug 场景编译崩溃。
阶段 F:收尾
[self addNote];
if (@available(iOS 15.0, *)) {
[UITableView appearance].sectionHeaderTopPadding = 0;
}
[SwiftHelper createFolds];
return YES;
Source: AppDelegate.m L134-L141
addNote 注册全局通知(详见下文「通知中心」),iOS 15 的 sectionHeaderTopPadding = 0 消除了系统默认的分组表头留白,SwiftHelper createFolds 为 Swift 桥接代码准备文件夹结构。
2. 窗口与根控制器管理(setupUI)
setupUI(AppDelegate.m L144-L160)是 iOS 12 及以下的窗口装配路径,其逻辑也代表了根控制器切换的完整模式:
-(void)setupUI{
self.window =[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
cmView = [[ConfirmView alloc] init];
NSString *key = [JL_Tools getUserByKey:@"CONMIT_PROTOCOL"];
if ([key isEqualToString:@"OK"]) {
[self initData];
}else{
tempVC =[[UIViewController alloc] init];
self.window.rootViewController = tempVC;
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
[self.window addSubview:cmView];
}
...
}
Source: AppDelegate.m L144-L160
设计意图解读:
- 隐私协议门禁:通过
JL_Tools getUserByKey:@"CONMIT_PROTOCOL"读取持久化确认标记。已确认(OK)则直接initData初始化主界面;未确认则先挂一个纯白底的空UIViewController作为根控制器,再把ConfirmView作为窗口子视图浮层展示协议内容。这种「根控制器占位 + 窗口级浮层」的组合比「present 一个协议页」更早介入,用户在启动瞬间就只能看到协议确认界面,符合隐私合规要求。 ConfirmView是实例变量而非局部变量(接口区ConfirmView *cmView;,L49),确保浮层在窗口生命周期内持续存活,且后续可在其他方法中(如协议确认回调)访问并移除。FindPhoneView的懒创建(L157-L160):查找手机视图同样挂在窗口层,初始hidden = YES,说明它是「全局悬浮能力」——由蓝牙设备侧触发(查找手机功能)时再显示,不属于 Tab 导航的一部分。
3. 主导航壳:MainTabBarVC
MainTabBarVC 是导航骨架的根容器(AppDelegate.m L29 导入、L46 实例变量),在 AppDelegate 中作为 mainVC 持有,保证其生命周期与 App 一致。
从启动文件导入的控制器可以还原该 Tab 壳挂载的顶层模块:
| 模块控制器 | 职责(按命名与工程目录推断) | 对应仓库目录 |
|---|---|---|
MultiMediaVC | 多媒体播放主界面(蓝牙音乐) | DeviceMusicVC/ |
EQSettingVC | 音效均衡器设置 | App设置/ 相关模块 |
DeviceInfoVC | 设备信息与功能控制入口 | DevicesViewController/DeviceInfoVC/ |
UpgradeVC | 固件升级 | UpgradeVC/ 相关模块 |
UserProfileVC | 用户中心/个人资料 | UserProfileVC 相关模块 |
PrivacyPolicyVC | 隐私政策页面(协议确认后展示) | 隐私政策相关模块 |
注:
MainTabBarVC.m的内部 Tab 组装代码未在本页读取范围内,以上模块归属依据AppDelegate.m的导入列表与仓库目录结构(README.md 的工程目录树)推断;如需 Tab 的精确顺序与图标配置,请直接阅读MainTabBarVC.m源文件。
这种「外壳持有容器、容器聚合模块」的结构带来两个直接收益:
- 模块间解耦:各功能 VC 只与
MainTabBarVC发生组装关系,彼此不互相引用;新增一个 Tab 只需要在MainTabBarVC中注册,不影响其他模块。 - 全局能力挂窗口而非挂 Tab:
ConfirmView、FindPhoneView、OpenShowView等全局浮层都直接挂在UIWindow上而不是某个 Tab 页内,保证它们在任何导航层级上都可见可交互——这是「应用级能力」与「页面级能力」在架构上的清晰区分。
4. 双品牌开屏动画机制
开屏动画是品牌差异的集中体现(AppDelegate.m L100-L114):
if(kJL_UI_SERIES == 0){ //杰理之家
/*--- 开启动画 ---*/
[OpenShowView startOpenAnimation];
}
if(kJL_UI_SERIES == 1){ //PiLink
/*--- 开启动画 ---*/
CGRect rect = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height);
PiLinkShowView *piLinkShowView = [[PiLinkShowView alloc] initWithFrame:rect];
UIWindow *win = [DFUITools getWindow];
[win addSubview:piLinkShowView];
}
Source: AppDelegate.m L100-L114
kJL_UI_SERIES 是杰理 SDK 提供的品牌系列宏(0 = 杰理之家,1 = PiLink)。两种品牌的开屏实现略有差异:杰理之家使用 OpenShowView 的类方法 startOpenAnimation(动画视图自管理),PiLink 则显式创建全屏 PiLinkShowView 并添加到 DFUITools getWindow 返回的窗口上。这个差异表明品牌定制点被收敛在开屏这一处,外壳的其余部分(导航、模块)完全不感知品牌差异。
核心流程:从进程启动到主界面呈现
下图按时间线展示了 App 从 main 进入后,外壳层各组件如何协作将用户带到 MainTabBarVC 主界面:
sequenceDiagram
participant OS as iOS 系统
participant AD as AppDelegate
participant SD as SceneDelegate (iOS 13+)
participant CM as ConfirmView
participant TBC as MainTabBarVC
participant SVC as 全局服务
OS->>AD: application:didFinishLaunchingWithOptions:
AD->>AD: 初始化 JLLogManager(日志先行)
AD->>AD: idleTimerDisabled=YES / 接收远程事件
AD->>AD: 语言检测归一化 (kJL_GET/kJL_SET)
alt iOS 13+
AD->>SD: 由 SceneDelegate 管理窗口与根控制器
else iOS 12-
AD->>AD: setupUI() 创建 UIWindow
AD->>CM: 读取 CONMIT_PROTOCOL
alt 已确认 (== OK)
AD->>TBC: initData() 设置根控制器
TBC-->>AD: 主界面呈现
else 未确认
AD->>AD: 根控制器 = 空白 tempVC
AD->>CM: addSubview 展示协议浮层
CM-->>AD: 用户确认后 → initData()
end
end
AD->>SVC: SqliteManager createTable / User_Http / 网络监测 / NetworkPlayer / Bugly
AD->>AD: addNote 注册全局通知
AD-->>OS: return YES
流程要点
- 日志先行:任何可能出错的阶段之前,日志系统必须就绪,这是启动可诊断性的基础。
- UI 分支只发生一次:iOS 13+ 与旧版系统在窗口创建上完全分流,但全局服务装配(阶段 E)不分流——两套 UI 路径共享同一批服务初始化,避免逻辑重复。
- 门禁是异步完成的:
ConfirmView是浮层而非模态页,用户确认后由initData把真正的根控制器换上。这解释了为什么mainVC是AppDelegate的成员变量——它必须在确认回调发生时才被创建并赋值,而不是在启动瞬间。 - 开屏动画与门禁并存:在旧版系统路径中,
OpenShowView/PiLinkShowView的开屏动画叠加在协议浮层之上(startOpenAnimation在setupUI之后调用),形成「品牌开屏 → 协议确认 → 主界面」的视觉序列。
使用示例
以下示例均摘自实际源码,展示如何在自己的杰理 SDK 应用中复刻这套外壳模式。
示例 1:最小启动骨架(日志 + 语言 + 全局服务)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
/*--- 记录NSLOG ---*/
[JLLogManager setLog:true IsMore:false Level:JLLOG_DEBUG];
[JLLogManager clearLog];
[JLLogManager logWithTimestamp:true];
[JLLogManager saveLogAsFile:true];
/*--- 设置屏幕常亮 ---*/
[UIApplication sharedApplication].idleTimerDisabled = YES;
/*--- 检测当前语言 ---*/
if ([kJL_GET isEqualToString:@"en"]) {
kJL_SET("en");
}else if([kJL_GET isEqualToString:@"ja"]){
kJL_SET("ja");
}else if ([kJL_GET isEqualToString:@"zh-Hans"]){
kJL_SET("zh-Hans");
}else{
kJL_SET("");
}
/*--- 创建数据库 ---*/
[[SqliteManager sharedInstance] createTable];
/*--- 用户登录 && 日志 ---*/
[User_Http shareInstance];
/*--- 网络监测 ---*/
[[AFNetworkReachabilityManager sharedManager] startMonitoring];
[NetworkPlayer sharedMe];
return YES;
}
Source: AppDelegate.m L65-L125
说明:这是外壳层的「必选动作」清单——日志、常亮、语言、数据库、用户体系、网络监测。集成时按此顺序执行即可保证后续模块的依赖可用。
示例 2:隐私协议门禁模式
-(void)setupUI{
self.window =[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
cmView = [[ConfirmView alloc] init];
NSString *key = [JL_Tools getUserByKey:@"CONMIT_PROTOCOL"];
if ([key isEqualToString:@"OK"]) {
[self initData];
}else{
tempVC =[[UIViewController alloc] init];
self.window.rootViewController = tempVC;
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
[self.window addSubview:cmView];
}
}
Source: AppDelegate.m L144-L156
说明:门禁的关键模式是「占位根控制器 + 窗口级浮层」。JL_Tools getUserByKey: 负责持久化读取,确认标记存于本地偏好;未确认时用户只能看到协议浮层,任何 Tab 界面都不可达。集成时注意 cmView 需为成员变量,以便在确认回调中移除。
示例 3:双品牌开屏切换
if(kJL_UI_SERIES == 0){ //杰理之家
/*--- 开启动画 ---*/
[OpenShowView startOpenAnimation];
}
if(kJL_UI_SERIES == 1){ //PiLink
/*--- 开启动画 ---*/
CGRect rect = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height);
PiLinkShowView *piLinkShowView = [[PiLinkShowView alloc] initWithFrame:rect];
UIWindow *win = [DFUITools getWindow];
[win addSubview:piLinkShowView];
}
Source: AppDelegate.m L103-L113
说明:通过 kJL_UI_SERIES 宏在同一套外壳内切换品牌。若你的产品需要定制开屏,只需在 AppDelegate 这一处替换品牌视图,导航与模块层无需改动。
示例 4:Bugly 的模拟器安全集成
// Bugly 在某些版本的 CocoaPods 提供的二进制仅包含真机架构,模拟器链接会失败。
// 通过 __has_include 进行条件导入,避免在 Debug/模拟器下编译报错。
#if __has_include(<Bugly/Bugly.h>)
#import <Bugly/Bugly.h>
#define HAS_BUGLY 1
#else
#define HAS_BUGLY 0
#endif
...
#if HAS_BUGLY
[Bugly startWithAppId:@"12d9f973f4"];
#else
// 模拟器或未集成 Bugly 的构建不启动 Bugly,避免链接/编译问题
NSLog(@"Bugly is not available for this build (simulator or Debug). Skipping Bugly startup.");
#endif
Source: AppDelegate.m L36-L43 与 L127-L132
说明:当第三方 SDK 的二进制分架构提供时,__has_include 条件编译是标准的防御手段——头文件不存在即视为未集成,运行时优雅降级为日志提示,保证模拟器与真机构建都不中断。
配置选项
外壳层的可配置项集中在启动方法与偏好存储中,均已在源码中验证:
| 配置项 | 类型 | 默认值/取值 | 来源位置 | 说明 |
|---|---|---|---|---|
kJL_UI_SERIES | 宏(整数) | 0(杰理之家)/ 1(PiLink) | AppDelegate.m L103-L107 | 品牌系列开关,决定开屏动画类型 |
CONMIT_PROTOCOL | 偏好键(字符串) | 未设置(首次启动) | AppDelegate.m L147 | 隐私协议确认标记,"OK" 表示已确认;由 JL_Tools getUserByKey: 读取 |
kJL_GET / kJL_SET | SDK 偏好宏 | en / ja / zh-Hans / "" | AppDelegate.m L82-L90 | SDK 语言偏好读写;白名单外的语言回退为空字符串 |
CFBundleShortVersionString | Info.plist | 由工程配置决定 | AppDelegate.m L73 | 市场版本号,启动时写入日志 |
CFBundleVersion | Info.plist | 由工程配置决定 | AppDelegate.m L73 | 构建号,启动时写入日志 |
| Bugly AppID | 常量字符串 | "12d9f973f4" | AppDelegate.m L128 | 崩溃上报 AppID,HAS_BUGLY 为 0 时不启动 |
| 日志级别 | 枚举 | JLLOG_DEBUG | AppDelegate.m L68 | 启动日志级别,可调为 JLLOG_INFO 等以降低输出 |
说明:
kJL_UI_SERIES、kJL_GET/kJL_SET的具体定义位于杰理 SDK 头文件(JL_RunSDK.h导入链)中,本页未读取其定义文件;以上取值依据AppDelegate.m中的使用方式确认。
API 参考(外壳层入口)
application:didFinishLaunchingWithOptions:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
- 位置:AppDelegate.m L65-L142
- 说明:App 完成启动的入口,负责日志、系统行为、语言、UI 分支、数据库、登录、网络、播放器、崩溃上报、通知注册等全部全局初始化。
- 参数
application(UIApplication *):当前应用实例。launchOptions(NSDictionary *):启动选项字典(如推送、URL 唤起来源)。
- 返回值(
BOOL):YES表示启动处理完成。源码恒返回YES(L141),NO仅在异常中断时使用。
setupUI
-(void)setupUI
- 位置:AppDelegate.m L144-L160
- 说明:iOS 12 及以下的窗口装配路径。创建
UIWindow,根据CONMIT_PROTOCOL标记决定直接进入initData还是先展示ConfirmView隐私协议浮层。 - 返回:无(
void)。
addNote(通知注册)
- 位置:在启动流程末尾调用(AppDelegate.m L134)
- 说明:集中注册 App 级通知监听。具体注册的通知列表位于
AppDelegate.m后半部分(本页读取范围之外),其作用是把蓝牙事件、网络变化等广播到各模块。
全局服务单例入口
| 单例 | 调用 | 生命周期 |
|---|---|---|
SqliteManager | [[SqliteManager sharedInstance] createTable] | App 级,数据库表创建 |
User_Http | [User_Http shareInstance] | App 级,用户会话 |
NetworkPlayer | [NetworkPlayer sharedMe] | App 级,网络音频播放 |
AFNetworkReachabilityManager | [[AFNetworkReachabilityManager sharedManager] startMonitoring] | App 级,网络状态监测 |
失败模式、边界情况与并发
隐私协议未确认(首次启动)
- 现象:根控制器为空白
tempVC,ConfirmView浮层展示协议。 - 风险:若用户既不确认也不退出,App 停留在门禁态,蓝牙等后续流程不会启动——这是设计预期(合规优先)。
- 恢复:确认后调用
initData完成主界面初始化;标记持久化在CONMIT_PROTOCOL,二次启动直接放行。
iOS 13+ 与旧版系统的窗口竞争
- 现象:iOS 13+ 下
SceneDelegate负责窗口;若旧代码路径(setupUI)在新系统被误调用,会出现两个窗口争夺根控制器的冲突。 - 防护:源码用
@available(iOS 13.0, *)显式分流(L93-L97、L100-L102),开屏动画也随分支隔离,避免叠加到错误的窗口。
模拟器 / Debug 构建下 Bugly 缺失
- 现象:部分 CocoaPods 渠道的 Bugly 二进制仅含真机架构,模拟器链接失败。
- 防护:
__has_include条件编译 +HAS_BUGLY宏(L36-L43),未集成时打印日志降级,不影响启动。
语言环境未覆盖
- 现象:系统语言不在
en/ja/zh-Hans白名单内。 - 处理:回退为空字符串(L88-L89),SDK 走默认语言文案,避免半翻译状态。
并发与线程
- 启动阶段全部为主线程同步执行(
didFinishLaunchingWithOptions本身在主线程),数据库建表、网络监测启动等均为轻量同步操作,未发现异步并发风险。 FindPhoneView、ConfirmView等全局浮层挂在窗口层,其显示/隐藏由蓝牙回调(异步线程)触发时,源码通过hidden属性切换,未在本页读取范围中发现跨线程直接操作 UI 的情况;若自行扩展此类全局浮层,建议将回调派发回主队列再操作视图。
启动时序依赖
- 服务装配顺序有隐式依赖:日志 → 数据库 → 登录 → 网络。例如
User_Http依赖SqliteManager建表完成,NetworkPlayer依赖网络监测启动。扩展时新增全局服务应插入到合理位置,不可随意打乱顺序。
性能与运维考虑
启动性能
- 启动路径短平快:
didFinishLaunchingWithOptions中的全局服务(日志、建表、登录、网络监测)均为轻量同步初始化,无网络阻塞等待——User_Http shareInstance只建立会话,真正的登录请求在后续流程异步发出。这保证了冷启动不会被 I/O 拖慢。 - 屏幕常亮的代价:
idleTimerDisabled = YES(L76)意味着 App 前台运行时系统不会自动息屏,功耗会上升。这是蓝牙控制类 App 的有意取舍,但发布前应确认产品需求是否真的需要全程常亮,必要时改为仅在连接设备时启用。 - 日志文件化:
saveLogAsFile:true会把日志落盘,长期运行会积累日志文件;源码在启动时调用clearLog清理上一次的日志(L69),避免无界增长。生产环境可考虑按需降级日志级别(JLLOG_DEBUG→JLLOG_INFO)进一步减负。
可观测性
- 版本锚点:启动日志记录
CFBundleShortVersionString与CFBundleVersion(L73),配合日志时间戳(logWithTimestamp:true),线上问题可精确回溯到版本与时间点。 - 崩溃上报:Bugly 在真机构建中启动(
HAS_BUGLY守卫),崩溃现场自动上报;模拟器构建优雅跳过。 - 网络可达性:
AFNetworkReachabilityManager持续监测,是网络播放与 HTTP 接口的可用性前提。
扩展点
新增 Tab 模块
在 MainTabBarVC 中注册新的控制器即可加入导航壳(Tab 组装代码见 MainTabBarVC.m)。新模块应遵循现有模块的职责边界(如 MultiMediaVC、EQSettingVC),只与外壳发生组装关系,不与其他 Tab 直接耦合。
双品牌定制
kJL_UI_SERIES 是品牌定制的唯一开关:开屏动画(OpenShowView/PiLinkShowView)已按品牌分流(L103-L113)。如需新增品牌(如 kJL_UI_SERIES == 2),在该分支添加对应的开屏视图即可,外壳其余部分无需改动。
全局浮层能力
ConfirmView(协议)、FindPhoneView(查找手机)演示了「窗口级浮层」的挂载模式:需要全 App 可见的能力(蓝牙事件弹窗、全局提示)都应挂到 UIWindow 而非某个 Tab 页面,这样不受导航栈影响。新能力可复制此模式:成员变量持有视图 + addSubview 到窗口 + hidden 控制显隐。
启动阶段插入自定义服务
按「日志 → 数据库 → 登录 → 网络」的依赖顺序,在 didFinishLaunchingWithOptions 中插入新的全局服务(如埋点 SDK、推送注册)。注意保持顺序依赖,并在日志系统之后插入,保证新服务可被日志覆盖。
测试与验证
本页读取范围内未发现针对外壳层(AppDelegate/MainTabBarVC)的单元测试文件;code/JieLi_Home_Demo/DbTest/ 目录包含 DbTest.m,表明数据库层(SqliteManager)具备独立测试入口。外壳层逻辑(启动顺序、门禁分支、语言归一化)建议通过以下方式验证:
- 真机 + 模拟器双构建:验证
HAS_BUGLY条件编译在两种环境下的行为(模拟器跳过 Bugly,真机启动)。 - 首次启动/二次启动:验证
CONMIT_PROTOCOL门禁分支——首次展示ConfirmView,确认后二次启动直接进入MainTabBarVC。 - iOS 13/14 与 iOS 12 对比:验证
SceneDelegate与setupUI两套窗口路径均能正确呈现主界面。 - 语言切换:在系统设置中切换
en/ja/zh-Hans/其他语言,验证kJL_SET的归一化结果与 SDK 文案表现。
相关链接
- 杰理之家 SDK(iOS)README:仓库总览、工程目录树与 SDK 集成说明
- AppDelegate.m:外壳层核心源码(本页主要依据)
- AppDelegate.h:AppDelegate 接口声明
- MainTabBarVC:主导航壳(Tab 组装源码,位于
NewJieliZhiNeng目录) - SqliteManager.m:全局数据库建表服务
- 兄弟页面:设备连接与 RCSP 协议控制、设备信息模块(
DeviceInfoVC)、固件升级模块(UpgradeVC)、多媒体模块(MultiMediaVC)各自的功能细节请查阅对应目录页面