杰理 SDK 文档中心
首页
首页
  • 概述与快速开始

    • 仓库概览
    • 运行环境与 SDK 集成
    • 工程结构与目录导航
  • 核心 SDK 架构

    • SDK 库体系与模块划分
    • 蓝牙连接与 RCSP 协议
    • 广播包解析与设备认证
    • 日志助手与调试支持
  • 设备功能模块

    • OTA 固件升级
    • 表盘管理与自定义表盘
    • 图像转换工具
    • 资源打包
    • 音频编解码
    • 健康与运动数据同步
    • 消息通知与实用设备功能
  • 宜动健康示例应用

    • 应用架构与页面导航
    • 健康界面与数据可视化
    • 设备连接与数据同步
    • 登录注册与用户中心
    • AI 云服务与语音交互
    • 本地数据库与持久化
    • 多语言国际化
  • 测试与调试

    • SDKTestHelper 功能测试工具
    • 音频编解码示例工程
    • 调试技巧与问题排查
  • 文档与资源

    • 在线文档与版本历史
    • 第三方框架与依赖管理

应用架构与页面导航

杰理健康(JieliJianKang)iOS 应用采用「AppDelegate 统一入口 + 根控制器动态切换 + UITabBarController 嵌套 UINavigationController」的经典 Objective-C 架构:启动时依次经历声明页、登录页,最终进入以 4 个 Tab 为主的健康主界面,并通过 JLApplicationDelegate.navigationController 提供全局页面导航能力。

Purpose and Scope

本页面向开发者说明杰理健康 App 的整体应用架构与页面导航机制,覆盖:

  • AppDelegate 的启动流程与全局初始化(日志、崩溃上报、语言检测、网络监测、SDK 配置)
  • 根控制器(rootViewController)的三种状态切换:声明页 → 登录页 → 主界面
  • 主界面 UITabBarController + 4 个 UINavigationController 的容器结构
  • BridgeHelper 提供的全局导航访问入口与页面跳转实践

与页面导航相关的其他子系统(如 BLE 设备通信 JL_BLEKit、AI 云服务 AIClound、HTTP 客户端 BasicHttp/JLWatchHttp、自定义控件 JLSwiperView 等)各有独立的职责边界,属于兄弟主题页面,不在本页展开;本页仅在导航链路中提及它们被挂载的位置。

Overview

杰理健康是杰理科技(Jieli-Tech)出品的健康管理 iOS 应用,工程位于仓库 code/JL_Health/JieliJianKang 目录,全部界面层代码使用 Objective-C 编写,并集成了多个 SDK/框架:

集成项用途
JL_BLEKit / JL_RunSDK蓝牙设备连接与运动数据
AIKIT.frameworkAI 语音/对话能力(Audio、Chat)
IFlyMSC(讯飞)语音识别(ISR)与合成(TTS)
AMapFoundationKit(高德)地图/定位服务
JLLogHelper本地日志管理
Bugly崩溃监控上报
DFUnits杰理基础工具库
AFNetworking网络请求与可达性监测

应用启动时的核心设计意图是**“先声明、后登录、再进主界面”的分阶段根控制器切换**:把用户协议声明、账户登录、主功能界面分别建模为独立的根控制器状态,由 AppDelegate 统一裁决切换时机,避免业务页面直接处理合规与登录逻辑。

Architecture

下图展示了杰理健康 App 的整体分层架构与页面容器关系:

flowchart TD
    subgraph sg_Launch["启动阶段 (AppDelegate)"]
        A["application:didFinishLaunchingWithOptions:"]
        A --> B["全局初始化<br/>日志/崩溃/语言/网络/SDK"]
        A --> C{"切换根控制器"}
    end

    subgraph sg_Root["根控制器状态"]
        D["JLStatementViewController<br/>用户声明页"]
        E["LoginVC<br/>登录页"]
        F["TabBarVC<br/>主界面 (UITabBarController)"]
    end

    subgraph sg_Main["主界面 Tab 结构"]
        F --> G["nvc_1 → HealthVC<br/>健康页"]
        F --> H["nvc_2 → SportVC<br/>运动页"]
        F --> I["nvc_3 → DeviceSearchVC<br/>设备页"]
        F --> J["nvc_4 → MyVC<br/>我的页"]
    end

    subgraph sg_Global["全局导航入口"]
        K["BridgeHelper.getNavigationController"]
        K --> L["main_nvc<br/>主 UINavigationController"]
    end

    C -->|"未同意声明"| D
    D -->|"同意后回调 delegate"| C
    C -->|"未登录"| E
    E -->|"登录成功 delegate"| C
    C -->|"已登录/已同意"| F
    L -.-> F

架构说明:

  • AppDelegate 作为唯一入口与状态机:didFinishLaunchingWithOptions: 完成全部全局初始化后,根据声明/登录状态把 window.rootViewController 分别设置为声明页、登录页或主界面(见 AppDelegate.m)。
  • 声明页与登录页通过 delegate 回调回流:AppDelegate 同时实现了 LoginDelegate 与 JLStatementViewControllerDelegate,子页面完成动作后回调 AppDelegate,由 AppDelegate 再次裁决下一个根控制器,形成闭环(见 AppDelegate.m)。
  • 主界面 = TabBar 外套 NavigationController:4 个业务根页面各自包一层 UINavigationController 成为 Tab,整个 UITabBarController 再被包进一个主 UINavigationController(main_nvc),使 App 级页面(如声明、登录)可以整体压栈呈现(见 AppDelegate.m)。
  • 全局导航通过 BridgeHelper 暴露:BridgeHelper 提供类方法返回 JLApplicationDelegate.navigationController,任意模块(含 JavaScript Bridge)都能拿到主导航栈执行 push/pop(见 BridgeHelper.m)。

启动流程详解

全局初始化(didFinishLaunchingWithOptions)

application:didFinishLaunchingWithOptions: 是 App 的启动入口,按顺序完成以下初始化工作:

  1. iOS 15 兼容:清除 UITableView 的 section header 顶部内边距(setSectionHeaderTopPadding:0.0),适配新系统默认样式。
  2. 屏幕常亮:设置 idleTimerDisabled = YES,健康/运动场景下防止息屏。
  3. 强制左到右布局:semanticContentAttribute = ForceLeftToRight,统一多语言环境下的排版方向。
  4. 日志系统:先 clearLog 清空旧日志,再 saveLogAsFile:true 落盘,开启 JLLOG_DEBUG 级别与时间戳(见 AppDelegate.m)。
  5. 崩溃上报:[Bugly startWithAppId:@"7a7c17c3ee"] 启动腾讯 Bugly。
  6. 语言检测:读取系统语言 kJL_GET,按前缀映射到 App 支持的语言(en-GB、zh-Hans、ja、ko、fr、de、it、pt-PT、es、sv、pl、ru、tr、vi、he、th、ar、id、ms、fa),无法识别时回退为 "auto"(见 AppDelegate.m)。
  7. 令牌刷新:[[User_Http shareInstance] refreshAccessToken] 预刷新登录令牌。
  8. 网络监测:启动 AFNetworkReachabilityManager 可达性监听,供全局无网提示(NoNetView)使用。
  9. 高德 SDK:配置 AMap 的 apiKey。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    if (@available(iOS 15.0, *)) {
        [[UITableView appearance] setSectionHeaderTopPadding:0.0];
    }
    /*--- 设置屏幕常亮 ---*/
    [UIApplication sharedApplication].idleTimerDisabled = YES;

    [UIView appearance].semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;

    /*--- 记录NSLOG ---*/
    [JLLogManager clearLog];
    [JLLogManager saveLogAsFile:true];
    [JLLogManager setLog:true IsMore:false Level:JLLOG_DEBUG];
    [JLLogManager logWithTimestamp:true];

    [Bugly startWithAppId:@"7a7c17c3ee"];

    kJLLog(JLLOG_DEBUG, @"当前的语言:%@",[LanguageCls checkLanguage]);
    /*--- 检测当前语言 ---*/
    if ([kJL_GET hasPrefix:@"en-GB"]) {
        kJL_SET("en-GB");
    }else if ([kJL_GET hasPrefix:@"zh-Hans"]){
        kJL_SET("zh-Hans");
    }else if ([kJL_GET hasPrefix:@"ja"]){
        kJL_SET("ja");
    }else if ([kJL_GET hasPrefix:@"ko"]){
        kJL_SET("ko");
    }else if ([kJL_GET hasPrefix:@"fr"]){
        kJL_SET("fr");
    }
    ...
}

Source: AppDelegate.m

这段初始化的设计意图是把“环境准备”与“界面呈现”解耦:日志、崩溃、网络、SDK 等基础设施在业务界面出现之前就绪,后续任何页面都可以假定这些能力可用,无需自行判断。

根控制器切换(三态裁决)

初始化完成后,AppDelegate 依据合规与登录状态选择根控制器:

状态根控制器触发条件
声明未同意JLStatementViewController首次启动或声明过期
已声明未登录LoginVC用户未登录/令牌失效
已登录TabBarVC(主界面)登录成功
self.window.rootViewController = statementVC;
...
self.window.rootViewController = self->loginVC;
...
self.window.rootViewController = mainVC;

Source: AppDelegate.m

这种“整窗替换根控制器”的做法比连续 push 更彻底:切换后旧的层级完全释放,避免登录态下残留声明页/登录页的内存与返回栈污染。AppDelegate 通过协议回调(LoginDelegate、JLStatementViewControllerDelegate)接收子页面的完成事件,再决定下一状态。

主界面导航结构

主界面是一个 UITabBarController,其 4 个 Tab 分别承载健康、运动、设备、我的四大模块:

UINavigationController *nvc_1 = [[UINavigationController alloc] initWithRootViewController:vc_1];
UINavigationController *nvc_2 = [[UINavigationController alloc] initWithRootViewController:vc_2];
UINavigationController *nvc_3 = [[UINavigationController alloc] initWithRootViewController:vc_3];
UINavigationController *nvc_4 = [[UINavigationController alloc] initWithRootViewController:vc_4];
...
for (int i = 0 ; i < arr_vc.count; i++) {
    UINavigationController *nvc = arr_vc[i];
    /*--- TabBarItem的名字 ---*/
    ...
}
...
UINavigationController *main_nvc = [[UINavigationController alloc] initWithRootViewController:tabBarVC];
self.navigationController = main_nvc;

Source: AppDelegate.m

根据文件头的 import 列表(HealthVC.h、SportVC.h、DeviceSearchVC.h、MyVC.h),4 个 Tab 根控制器为:健康页 HealthVC、运动页 SportVC、设备搜索页 DeviceSearchVC、我的页 MyVC。

两级导航的设计意图:

  • Tab 内导航:每个 Tab 自带 UINavigationController,业务页面在模块内 push/pop,返回键与手势天然可用。
  • App 级导航:tabBarVC 外套 main_nvc 并保存在 self.navigationController,使与 Tab 无关的全局页面(如声明页、登录页、设置)可以整体覆盖在 TabBar 之上,且通过 JLApplicationDelegate.navigationController 全局可达。

BridgeHelper:全局导航访问

BridgeHelper 是跨模块获取主导航栈的桥接层(也服务于 JS Bridge 场景):

+(UINavigationController *)getNavigationController {
    return JLApplicationDelegate.navigationController;
}

Source: BridgeHelper.m

业务代码(如运动详情页在本地运动开始/结束时返回根页)即通过导航栈完成页面回收:

- (void)startLocalSport {
    [self.navigationController popToRootViewControllerAnimated:YES];
    return;
}

Source: JLSportDetailViewController.m

核心流程:冷启动到主界面

下图按时间顺序展示 App 从冷启动到进入主界面的完整链路,以及页面间通过 delegate 回调驱动的状态迁移:

sequenceDiagram
    participant OS as iOS (UIApplication)
    participant AD as AppDelegate
    participant INFRA as 基础设施<br/>(日志/Bugly/语言/网络/SDK)
    participant ST as JLStatementViewController
    participant LG as LoginVC
    participant TB as TabBarVC + 4×Nav

    OS->>AD: application:didFinishLaunchingWithOptions:
    activate AD
    AD->>INFRA: 日志/崩溃/语言/令牌/网络初始化
    INFRA-->>AD: 就绪
    AD->>ST: window.rootViewController = statementVC
    deactivate AD
    ST-->>AD: delegate: 声明已同意
    activate AD
    AD->>LG: window.rootViewController = loginVC
    deactivate AD
    LG-->>AD: delegate: 登录成功
    activate AD
    AD->>TB: window.rootViewController = mainVC
    deactivate AD
    Note over TB: 主界面内 Tab 切换与<br/>模块内 push/pop 导航

流程要点:

  1. 环境优先:一切 UI 呈现前完成基础设施初始化,保证日志/崩溃/网络能力随时可用。
  2. 声明门禁:用户协议未同意时,主界面不可达——这是合规要求在前端的第一道闸门。
  3. 登录门禁:声明通过后若未登录,根控制器切换为 LoginVC;登录成功经 LoginDelegate 回调后进入主界面。
  4. 主界面自洽:进入 TabBarVC 后,日常导航完全由 Tab 内 UINavigationController 与 main_nvc 承接,无需再经过 AppDelegate。

使用示例

示例一:启动期根控制器切换(AppDelegate)

声明页/登录页完成后的根控制器替换逻辑集中在 AppDelegate 内部:

if (用户未同意声明) {
    JLStatementViewController *statementVC = [[JLStatementViewController alloc] init];
    statementVC.delegate = self;
    self.window.rootViewController = statementVC;
    tempVC = [[UIViewController alloc] init];
} else if (用户未登录) {
    self->loginVC = [[LoginVC alloc] init];
    self->loginVC.delegate = self;
    self.window.rootViewController = self->loginVC;
    self.window.backgroundColor = [UIColor whiteColor];
} else {
    self.window.rootViewController = mainVC;
    self.window.backgroundColor = [UIColor whiteColor];
}

Source: AppDelegate.m

示例二:全局导航栈获取(BridgeHelper)

任意模块需要跳转全局页面时,通过 BridgeHelper 取得主导航栈:

+(UINavigationController *)getNavigationController {
    return JLApplicationDelegate.navigationController;
}

Source: BridgeHelper.m

示例三:模块内导航返回根页(运动页)

运动场景开始/结束时将页面栈回收到根控制器,避免用户在运动过程中被其他页面干扰:

- (void)startLocalSport {
    [self.navigationController popToRootViewControllerAnimated:YES];
    return;
}

- (void)finishLocalSport {
    [self.navigationController popToRootViewControllerAnimated:YES];
    return;
}

Source: JLSportDetailViewController.m

配置选项

应用架构层的主要配置点集中在 AppDelegate 启动阶段:

配置项类型默认/取值说明
Bugly App IDstring7a7c17c3ee崩溃上报服务标识,启动即注册
屏幕常亮BOOLYES(idleTimerDisabled)健康/运动场景防息屏
布局方向enumUISemanticContentAttributeForceLeftToRight强制 LTR,统一多语言排版
日志级别enumJLLOG_DEBUG写入文件的日志级别(setLog:IsMore:Level:)
日志落盘BOOLtrue(saveLogAsFile:)是否保存日志到本地文件
日志时间戳BOOLtrue(logWithTimestamp:)日志是否带时间戳
语言回退string"auto"系统语言不在支持列表时使用
网络监测classAFNetworkReachabilityManager启动即开始可达性监听
令牌刷新methodrefreshAccessToken启动时预刷新登录令牌

失败模式、边界情况与并发

根控制器切换的时序风险

声明页/登录页通过 delegate 异步回调 AppDelegate,而 window.rootViewController 的替换是同步赋值。设计上依赖“单回调、单入口”的顺序模型:声明必须先于登录、登录必须先于主界面。若回调在窗口未就绪时触发,可能出现切换竞态——代码中通过 tempVC 占位控制器兜底,保证窗口始终有根控制器可渲染。

语言检测的边界

语言映射采用前缀匹配(hasPrefix:),因此 zh-Hans-CN、en-GB@region 等带扩展后缀的系统语言也能正确归类;无法匹配的语言(如 fil、hr)统一回退 "auto",由 LanguageCls 按默认规则处理,避免因语言缺失导致界面文案空白。

无网络场景

AFNetworkReachabilityManager 全局监听网络状态,配合 CustomView/无网络/NoNetView.m 展示全局无网提示;令牌刷新(refreshAccessToken)在网络异常时失败后由登录/HTTP 层重试,架构层不阻塞启动。

日志与崩溃上报的副作用

每次启动 clearLog 会丢弃上一次运行的日志文件——这是以“保证磁盘占用有界”为代价换取的可诊断性;JLLogManager saveLogAsFile:true 在文件 I/O 繁忙时可能产生轻微启动延迟,属于可接受的权衡。

并发与内存

主界面由 4 个常驻 Tab 组成,各 Tab 根控制器随 TabBar 一并常驻内存;模块内页面依赖 UINavigationController 的 push/pop 生命周期自动释放。整窗替换根控制器(而非叠加 push)确保登录/声明页在切换后即被释放,是控制内存峰值的关键手段。

扩展点

  • 新增 Tab 模块:在 AppDelegate 的 arr_vc 数组中追加新的根控制器并包一层 UINavigationController,按既有循环统一设置 TabBarItem 名称即可,无需改动容器架构。
  • 新增全局页面:通过 BridgeHelper getNavigationController 取得 main_nvc 后 pushViewController,可整体覆盖 TabBar。
  • 多语言扩展:在启动语言前缀映射链中追加 else if 分支,并在 LanguageCls 中补充资源即可支持新语言。
  • 桥接层:Bridge/BridgeHelper.m 是 JS Bridge 与原生导航的汇合点,新增 Web 与原生互跳能力时应在此扩展。

相关链接

  • AppDelegate.m(启动与导航核心)
  • BridgeHelper.m(全局导航入口)
  • JLSportDetailViewController.m(导航栈使用示例)
  • NoNetView.m(全局无网提示视图)
  • AIClound.m(AI 云服务模块)
  • BasicHttp.m(HTTP 客户端基类)
Next
健康界面与数据可视化