//
// OrderFileRecorder.h
// BasicExercise
//
// Created by DDS on 2026/6/16.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface OrderFileRecorder : NSObject
+ (void)startRecording;
+ (void)stopRecording;
+ (void)resetRecording;
+ (BOOL)isRecording;
+ (nullable NSString *)exportOrderFile;
+ (nullable NSString *)exportOrderFileWithName:(NSString *)fileName;
@end
NS_ASSUME_NONNULL_END
//OrderFileRecorder.m
#import "OrderFileRecorder.h"
#import <dlfcn.h>
#import <stdatomic.h>
#define BE_ORDER_FILE_NO_COVERAGE __attribute__((no_sanitize("coverage")))
static const uint32_t kOrderFileMaxPCCount = 300000;
static atomic_bool isOrderFileRecording = ATOMIC_VAR_INIT(false);
static atomic_bool didBootstrapOrderFileRecording = ATOMIC_VAR_INIT(false);
static atomic_uint recordedPCCount = ATOMIC_VAR_INIT(0);
static uintptr_t *recordedPCBuffer = NULL;
static uint32_t recordedPCCapacity = 0;
static __thread BOOL shouldIgnoreCurrentThread = NO;
BE_ORDER_FILE_NO_COVERAGE
static void BEOrderFilePrepareBufferIfNeeded(void) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
recordedPCCapacity = kOrderFileMaxPCCount;
recordedPCBuffer = (uintptr_t *)calloc(recordedPCCapacity, sizeof(uintptr_t));
});
}
BE_ORDER_FILE_NO_COVERAGE
static BOOL BEOrderFileShouldSkipSymbol(NSString *symbolName, NSString *imagePath) {
if (symbolName.length == 0) {
return YES;
}
if ([symbolName containsString:@"OrderFileRecorder"] ||
[symbolName containsString:@"__sanitizer_cov"] ||
[symbolName containsString:@"saveOrderFile"]) {
return YES;
}
NSString *bundlePath = [NSBundle mainBundle].bundlePath.stringByStandardizingPath;
NSString *standardizedImagePath = imagePath.stringByStandardizingPath;
if (bundlePath.length > 0 && standardizedImagePath.length > 0 && ![standardizedImagePath hasPrefix:bundlePath]) {
return YES;
}
return NO;
}
BE_ORDER_FILE_NO_COVERAGE
static NSString *BEOrderFileFormattedSymbolName(const char *symbol) {
if (symbol == NULL || symbol[0] == '\0') {
return nil;
}
NSString *symbolName = [NSString stringWithUTF8String:symbol];
if (symbolName.length == 0) {
return nil;
}
if ([symbolName hasPrefix:@"+[" ] || [symbolName hasPrefix:@"-["]) {
return symbolName;
}
if (![symbolName hasPrefix:@"_"]) {
symbolName = [@"_" stringByAppendingString:symbolName];
}
return symbolName;
}
BE_ORDER_FILE_NO_COVERAGE
static NSString *BEOrderFileExportPath(NSString *fileName) {
NSString *safeFileName = fileName.length > 0 ? fileName : @"startup.order";
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
if (documentsPath.length == 0) {
documentsPath = NSTemporaryDirectory();
}
return [documentsPath stringByAppendingPathComponent:safeFileName];
}
@implementation OrderFileRecorder
+ (void)startRecording {
BEOrderFilePrepareBufferIfNeeded();
atomic_store_explicit(&isOrderFileRecording, true, memory_order_relaxed);
}
+ (void)stopRecording {
atomic_store_explicit(&isOrderFileRecording, false, memory_order_relaxed);
}
+ (void)resetRecording {
BEOrderFilePrepareBufferIfNeeded();
atomic_store_explicit(&recordedPCCount, 0, memory_order_relaxed);
if (recordedPCBuffer != NULL && recordedPCCapacity > 0) {
memset(recordedPCBuffer, 0, sizeof(uintptr_t) * recordedPCCapacity);
}
}
+ (BOOL)isRecording {
return atomic_load_explicit(&isOrderFileRecording, memory_order_relaxed);
}
+ (NSString *)exportOrderFile {
return [self exportOrderFileWithName:@"startup.order"];
}
+ (NSString *)exportOrderFileWithName:(NSString *)fileName {
BEOrderFilePrepareBufferIfNeeded();
[self stopRecording];
shouldIgnoreCurrentThread = YES;
uint32_t count = atomic_load_explicit(&recordedPCCount, memory_order_acquire);
if (count > recordedPCCapacity) {
count = recordedPCCapacity;
}
NSMutableOrderedSet<NSString *> *orderedSymbols = [[NSMutableOrderedSet alloc] initWithCapacity:count];
for (NSInteger index = (NSInteger)count - 1; index >= 0; index--) {
uintptr_t pc = recordedPCBuffer[index];
if (pc == 0) {
continue;
}
Dl_info info = {0};
if (dladdr((void *)pc, &info) == 0 || info.dli_sname == NULL) {
continue;
}
NSString *symbolName = BEOrderFileFormattedSymbolName(info.dli_sname);
NSString *imagePath = info.dli_fname != NULL ? [NSString stringWithUTF8String:info.dli_fname] : @"";
if (symbolName.length == 0 || BEOrderFileShouldSkipSymbol(symbolName, imagePath)) {
continue;
}
[orderedSymbols addObject:symbolName];
}
NSMutableString *content = [NSMutableString string];
for (NSString *symbol in orderedSymbols) {
[content appendFormat:@"%@\n", symbol];
}
NSString *filePath = BEOrderFileExportPath(fileName);
NSError *error = nil;
BOOL success = [content writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error];
shouldIgnoreCurrentThread = NO;
if (!success || error) {
NSLog(@"order file 写入失败:%@", error);
return nil;
}
NSLog(@"order file 写入成功:%@", filePath);
return filePath;
}
@end
BE_ORDER_FILE_NO_COVERAGE
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
static uint32_t N;
if (start == stop || *start) {
return;
}
for (uint32_t *x = start; x < stop; x++) {
*x = ++N;
}
BEOrderFilePrepareBufferIfNeeded();
if (!atomic_exchange_explicit(&didBootstrapOrderFileRecording, true, memory_order_relaxed)) {
atomic_store_explicit(&recordedPCCount, 0, memory_order_relaxed);
atomic_store_explicit(&isOrderFileRecording, true, memory_order_relaxed);
}
}
BE_ORDER_FILE_NO_COVERAGE
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (guard == NULL || !*guard) {
return;
}
if (!atomic_load_explicit(&isOrderFileRecording, memory_order_relaxed)) {
return;
}
if (shouldIgnoreCurrentThread || recordedPCBuffer == NULL || recordedPCCapacity == 0) {
return;
}
uintptr_t pc = (uintptr_t)__builtin_return_address(0);
if (pc == 0) {
return;
}
uint32_t index = atomic_fetch_add_explicit(&recordedPCCount, 1, memory_order_relaxed);
if (index >= recordedPCCapacity) {
return;
}
recordedPCBuffer[index] = pc;
}
使用方法
NSString *filePath = [OrderFileRecorder exportOrderFile];
if (filePath.length > 0) {
[SVProgressHUD showToastTitle:[NSString stringWithFormat:@"order file 已导出到 %@", filePath.lastPathComponent]];
} else {
[SVProgressHUD showToastTitle:@"order file 导出失败"];
}