Browse Source

client(iOS):加原生崩溃记录写入 Documents/logs/native_crash.log;BES FileUtils 改名 BesFileUtils 避免类名冲突;补运动权限说明;构建号 1.0.2+25

Co-authored-by: Cursor <cursoragent@cursor.com>
along-test
Rodger-Wang 2 weeks ago
parent
commit
4a6d9c6cae
  1. 22
      apps/client/ios/Runner.xcodeproj/project.pbxproj
  2. 119
      apps/client/ios/Runner/AppDelegate.swift
  3. 2
      apps/client/ios/Runner/Info.plist
  4. 2
      apps/client/local_plugins/bes_ota_manager/ios/Classes/bessdk/connect/BesBTConnector.m
  5. 2
      apps/client/local_plugins/bes_ota_manager/ios/Classes/bessdk/service/TotaDemo/BesTotaDemoService.m
  6. 2
      apps/client/local_plugins/bes_ota_manager/ios/Classes/bessdk/utils/FileUtils.h
  7. 4
      apps/client/local_plugins/bes_ota_manager/ios/Classes/bessdk/utils/FileUtils.m
  8. 2
      apps/client/pubspec.yaml

22
apps/client/ios/Runner.xcodeproj/project.pbxproj

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
@ -338,7 +338,7 @@
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
@ -389,10 +389,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@ -437,10 +441,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@ -641,7 +649,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = B8AJQ7ACU7;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -835,7 +843,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = B8AJQ7ACU7;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -869,7 +877,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = B8AJQ7ACU7;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -988,7 +996,7 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
@ -997,7 +1005,7 @@
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
package = 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */;
package = 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */

119
apps/client/ios/Runner/AppDelegate.swift

@ -1,4 +1,5 @@
import Flutter
import MachO
import UIKit
@main
@ -8,7 +9,11 @@ import UIKit
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
application.applicationIconBadgeNumber = 0
NativeCrashRecorder.install()
GeneratedPluginRegistrant.register(with: self)
// 第三方 SDK(高德/杰理等)初始化时可能装自己的信号处理器把我们顶掉,
// 过一会儿再装一次;装的时候会把当时在位的处理器记下来链式调用,两边都不丢。
DispatchQueue.main.asyncAfter(deadline: .now() + 15) { NativeCrashRecorder.install() }
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
@ -33,3 +38,117 @@ import UIKit
return false
}
}
// MARK: - 原生崩溃记录(2026-09-26)
//
// 起因:车载香薰对话里 App 两次 SIGSEGV 退出,但这台 iPhone 上**一份系统崩溃报告都没留下**
// (「分析数据」里最新的还是 9-21),devicectl 只能看到 "terminated due to signal 11",
// 没有调用栈就没法定位。怀疑是某个 SDK 装了信号处理器却没把信号交还系统。
//
// 这里在崩溃那一刻把调用栈写进 Documents/logs/native_crash.log(和 App 日志同目录,
// devicectl 能直接拉),然后把信号交还给之前在位的处理器 / 系统默认行为,不改变崩溃本身。
// 调用栈是「镜像 + 偏移」形式,拿同一次构建的 dSYM 用 atos 还原成函数名。
//
// 信号处理函数里只用 write / backtrace_symbols_fd 这类 async-signal-safe 的调用,
// 不分配内存、不碰 Swift 运行时的锁。
private var crashFD: Int32 = -1
/// 调用栈缓冲:安装时就分配好,信号处理里不再申请内存
private let crashFrames = UnsafeMutablePointer<UnsafeMutableRawPointer?>.allocate(capacity: 128)
private let crashSignals: [Int32] = [SIGSEGV, SIGBUS, SIGABRT, SIGILL, SIGFPE, SIGTRAP]
/// 安装时在位的旧处理器(按信号号索引),崩溃时链式调用
private let previousHandlers = UnsafeMutablePointer<sig_t?>.allocate(capacity: 32)
private var previousExceptionHandler: (@convention(c) (NSException) -> Void)?
private func crashWrite(_ s: StaticString) {
guard crashFD >= 0 else { return }
_ = s.withUTF8Buffer { write(crashFD, $0.baseAddress, $0.count) }
}
private func crashWriteInt(_ value: Int) {
guard crashFD >= 0 else { return }
// 用栈上的元组当缓冲,不走堆分配
var buf: (UInt64, UInt64, UInt64) = (0, 0, 0)
withUnsafeMutableBytes(of: &buf) { raw in
let p = raw.bindMemory(to: UInt8.self)
var v = value < 0 ? -value : value
var i = p.count
repeat { i -= 1; p[i] = UInt8(48 + v % 10); v /= 10 } while v > 0 && i > 0
if value < 0 && i > 0 { i -= 1; p[i] = 45 }
_ = write(crashFD, p.baseAddress! + i, p.count - i)
}
}
private let crashSignalHandler: @convention(c) (Int32) -> Void = { sig in
crashWrite("\n=== native crash signal=")
crashWriteInt(Int(sig))
crashWrite(" time=")
crashWriteInt(Int(time(nil)))
crashWrite(" ===\n")
let n = backtrace(crashFrames, 128)
backtrace_symbols_fd(crashFrames, n, crashFD)
crashWrite("=== images ===\n")
// 各镜像的加载基址:atos 还原时要用(-l <基址>)
let count = _dyld_image_count()
for idx in 0..<count {
if let name = _dyld_get_image_name(idx) {
let header = _dyld_get_image_header(idx)
crashWriteInt(Int(bitPattern: header))
crashWrite(" ")
_ = write(crashFD, name, strlen(name))
crashWrite("\n")
}
}
fsync(crashFD)
// 交还:之前有人装过处理器就调它(SIG_DFL=0 / SIG_IGN=1 不算),否则恢复默认并重发信号
if let prev = previousHandlers[Int(sig)] {
let raw = unsafeBitCast(prev, to: Int.self)
if raw != 0 && raw != 1 {
prev(sig)
}
}
signal(sig, SIG_DFL)
raise(sig)
}
enum NativeCrashRecorder {
private static var initialized = false
static func install() {
if !initialized {
initialized = true
previousHandlers.initialize(repeating: nil, count: 32)
}
if crashFD < 0 {
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let dir = docs.appendingPathComponent("logs")
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
crashFD = open(dir.appendingPathComponent("native_crash.log").path,
O_WRONLY | O_CREAT | O_APPEND, 0o644)
}
let mine = unsafeBitCast(crashSignalHandler, to: Int.self)
for sig in crashSignals {
let prev = signal(sig, crashSignalHandler)
// 在位的已经是我们自己(第二次 install):别记成「旧处理器」,否则崩溃时会调到自己、无限递归
if let p = prev, unsafeBitCast(p, to: Int.self) == mine { continue }
previousHandlers[Int(sig)] = prev
}
let current = NSGetUncaughtExceptionHandler()
if current.map({ unsafeBitCast($0, to: Int.self) }) != unsafeBitCast(exceptionHandler, to: Int.self) {
previousExceptionHandler = current
NSSetUncaughtExceptionHandler(exceptionHandler)
}
}
private static let exceptionHandler: @convention(c) (NSException) -> Void = { e in
let text = "\n=== uncaught exception \(Date()) \(e.name.rawValue): \(e.reason ?? "")\n"
+ e.callStackSymbols.joined(separator: "\n") + "\n"
if crashFD >= 0, let d = text.data(using: .utf8) {
d.withUnsafeBytes { _ = write(crashFD, $0.baseAddress, d.count) }
fsync(crashFD)
}
previousExceptionHandler?(e)
}
}

2
apps/client/ios/Runner/Info.plist

@ -140,6 +140,8 @@
<string>Location access enables the app to provide location-based voice translation services, such as translating local place names and providing contextual information relevant to your current location during conversations.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Our app's voice AI interaction features (such as voice wake-up for AI, issuing commands, and real-time conversations) require microphone access. With this permission, you can interact with AI directly through speech, improving operational efficiency. We strictly protect your voice data—only used for real-time processing of interaction requests, and raw audio will not be stored.</string>
<key>NSMotionUsageDescription</key>
<string>This app uses motion and fitness data to detect when you're walking or moving, which helps optimize voice recognition accuracy and automatically adjust microphone sensitivity for better audio recording quality during different activities.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Photo library permission is required to save exported images (mind maps, statistics) to your album</string>
<key>NSPhotoLibraryUsageDescription</key>

2
apps/client/local_plugins/bes_ota_manager/ios/Classes/bessdk/connect/BesBTConnector.m

@ -334,7 +334,7 @@ typedef enum : NSUInteger {
[defaults setObject:BES_SAVE_DEFAULT_VALUE forKey:BES_SAVE_LOG_KEY];
}
if ([[defaults valueForKey:BES_SAVE_LOG_KEY] isEqual:@"YES"] && [defaults valueForKey:BES_SAVE_LOG_TITLE_KEY]) {
[FileUtils saveStringToDocumentWithName:[NSString stringWithFormat:@"%@.txt", [defaults valueForKey:BES_SAVE_LOG_TITLE_KEY]] text:text];
[BesFileUtils saveStringToDocumentWithName:[NSString stringWithFormat:@"%@.txt", [defaults valueForKey:BES_SAVE_LOG_TITLE_KEY]] text:text];
}
}

2
apps/client/local_plugins/bes_ota_manager/ios/Classes/bessdk/service/TotaDemo/BesTotaDemoService.m

@ -605,7 +605,7 @@
[defaults setObject:BES_SAVE_DEFAULT_VALUE forKey:BES_SAVE_LOG_KEY];
}
if ([[defaults valueForKey:BES_SAVE_LOG_KEY] isEqual:@"YES"] && [defaults valueForKey:BES_SAVE_LOG_TITLE_KEY]) {
[FileUtils saveStringToDocumentWithName:[NSString stringWithFormat:@"%@.txt", [defaults valueForKey:BES_SAVE_LOG_TITLE_KEY]] text:text];
[BesFileUtils saveStringToDocumentWithName:[NSString stringWithFormat:@"%@.txt", [defaults valueForKey:BES_SAVE_LOG_TITLE_KEY]] text:text];
}
}

2
apps/client/local_plugins/bes_ota_manager/ios/Classes/bessdk/utils/FileUtils.h

@ -9,7 +9,7 @@
NS_ASSUME_NONNULL_BEGIN
@interface FileUtils : NSObject
@interface BesFileUtils : NSObject
+ (NSString *)getDocumentPath;

4
apps/client/local_plugins/bes_ota_manager/ios/Classes/bessdk/utils/FileUtils.m

@ -1,5 +1,5 @@
//
// FileUtils.m
// BesFileUtils.m
// BesAll
//
// Created by 范羽 on 2021/2/25.
@ -7,7 +7,7 @@
#import "FileUtils.h"
@implementation FileUtils
@implementation BesFileUtils
+ (NSString *)getDocumentPath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

2
apps/client/pubspec.yaml

@ -1,7 +1,7 @@
name: eaimar
description: "Eaimar - AI Voice Assistant."
publish_to: "none"
version: 1.0.2+23
version: 1.0.2+25
environment:
sdk: ">=3.3.0 <4.0.0"

Loading…
Cancel
Save