iOS 启动优化之自注册--attribute(section)
启动器自注册的思路来自于美团的技术文章美团外卖 iOS App 冷启动治理
Clang 提供了很多的编译器函数,它们可以完成不同的功能。其中一种就是 section() 函数,section()函数提供了二进制段的读写能力,它可以将一些编译期就可以确定的常量写入数据段。 在具体的实现中,主要分为编译期和运行时两个部分。在编译期,编译器会将标记了 attribute ((section())) 的数据写到指定的数据段中,例如写一个{key(key 代表不同的启动阶段), pointer}对到数据段。到运行时,在合适的时间节点,在根据 key 读取出函数指针,完成函数的调用。*
细节可以参考链接中实现,文章中提供了一种自注册的思路,但是隐藏了代码的实现细节,好奇心驱动下我完成了实现细节,具体思路是通过 section("__DATA, kinit"))来把函数实现注册到 **DATA 的自定义子段 ** __kinit
中,然后封装一个方法取出指定 key 值下对应的函数指针。管理类代码如下
YZKylin.h
#import <Foundation/Foundation.h>
#import <dlfcn.h>
#import <mach-o/getsect.h>
#ifndef __LP64__
#define mach_header mach_header
#else
#define mach_header mach_header_64
#endif
typedef void (*InitFunction)(void);
typedef struct {
const char *key; // 启动阶段名字
InitFunction func; // 注册的函数指针
} InitEntry;
// 封装注册宏
#define KLN_REGISTER_FUNCTION(KEY, FUNC) \
__attribute__((used, section("__DATA,__kinit"))) \
static const struct { const char *key; InitFunction func; } kInitEntry_##FUNC = { KEY, FUNC };
@interface YZKylin : NSObject
+ (instancetype)sharedInstance;
// 取出当前 key 对应的所有函数
- (NSArray<NSValue *> *)functionsForKey:(NSString *)key;
// 直接执行当前 key 下所有函数
- (void)executeFunctionsForKey:(NSString *)key;
@end
YZKylin.m
#import "YZKylin.h"
static NSString *placeHolder = @"";
@implementation YZKylin
+ (instancetype)sharedInstance {
static YZKylin *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[YZKylin alloc] init];
});
return instance;
}
// 取出当前 key 对应的所有函数
- (NSArray<NSValue *> *)functionsForKey:(NSString *)key{
Dl_info info;
dladdr((__bridge const void *)(placeHolder), &info);
const struct mach_header *header = (const struct mach_header *)info.dli_fbase;
unsigned long byteSize = 0;
const InitEntry *entries = (const InitEntry *)getsectiondata(header, "__DATA", "__kinit", &byteSize);
NSUInteger count = byteSize / sizeof(InitEntry);
NSMutableArray *result = [NSMutableArray array];
for (NSUInteger i = 0; i < count; ++i) {
const InitEntry *entry = &entries[i];
if (strcmp(entry->key, [key UTF8String]) == 0) {
[result addObject:[NSValue valueWithPointer:entry->func]];
}
}
return result;
}
// 直接执行当前 key 下所有函数
- (void)executeFunctionsForKey:(NSString *)key{
NSArray<NSValue *> *funcs = [self functionsForKey:key];
for (NSValue *value in funcs) {
InitFunction func = [value pointerValue];
if (func) {
func();
}
}
}
@end
代码细节分析
KLN_REGISTER_FUNCTION 宏定义拆解
cpp
#define KLN_REGISTER_FUNCTION(KEY, FUNC) \ __attribute__((used, section("__DATA,__my_init_section"))) \ static const struct { const char *key; InitFunction func; } kInitEntry_##FUNC = { KEY, FUNC };
你调用时假设:
cpp
KLN_REGISTER_FUNCTION("StageA", myFunc1);
kInitEntry_##FUNC 中的"##" 是拼接符号 == kInitEntry_myFunc1,展开后就变成 👇:
cpp
__attribute__((used, section("__DATA,__kinit")))
static const struct { const char *key; InitFunction func; } kInitEntry_myFunc1 = { "StageA", myFunc1 };
used
:告诉编译器 "这个变量虽然表面看没人用,但请千万不要优化掉"
-
否则编译器有时会做 "未引用数据剔除(dead code elimination)"
-
section("__DATA,__kinit")
:告诉编译器 "请把这个变量放入 Mach-O 文件的 __DATA 段下的__kinit 自定义子段"
static const struct { const char *key; InitFunction func; } kInitEntry_myFunc1 = { "StageA", myFunc1 }
定义了一个 静态常量匿名结构体
arduino
struct {
const char *key; // 关键字字符串
InitFunction func; // 函数指针
}
kInitEntry_myFunc1 = { "StageA", myFunc1 }
是这个 静态常量匿名结构体
的一个变量实例,定义然后进行赋值。
分析 functionsForKey
实现
ini
static NSString *placeHolder = @"";
Dl_info info;
dladdr((__bridge const void *)(placeHolder), &info);
dladdr 是什么?
dladdr
是一个 动态链接库接口函数 ,声明在 <dlfcn.h>
,作用是:
**根据传入的某个地址,找到这个地址所在的 Mach-O 镜像、符号等元信息。 **
cpp
dladdr((__bridge const void *)(configuration), &info);
-
参数
addr
:任意内存地址(通常是你程序中的某个全局变量/函数/对象地址) -
参数
info
:Dl_info
结构体,返回查到的动态库符号信息 -
返回值:非 0 表示查找成功
为什么传的是 (__bridge const void * )(placeHolder)?******
这是一个 全局静态的 NSString 对象,它存储在 App 主程序的 data 段。
因为它一定属于当前 App 的 Mach-O 映像 segment 内的一个地址,所以传入它可以巧妙地查找当前 App 主程序 Mach-O 的 dl_info
。
整个函数的逻辑非常清晰:根据传入的 key,遍历 __DATA,__kinit
段,找到对应 key 下注册的所有函数指针,放到数组里返回。
注册方式
在组件内部就可以导入 YZKylin
(拆分成公共组件)进行注册,而不需要写到一个臃肿的管理类中
javascript
#import "YZKylin.h"
// 定义函数
void willFinishLaunchingFunc() { NSLog(@"✅ willFinishLaunchingFunc called"); }
void didFinishLaunchingFunc() { NSLog(@"✅ didFinishLaunchingFunc called"); }
void firstVCViewDidAppearFunc() { NSLog(@"✅ firstVCViewDidAppearFunc called"); }
// 注册
KLN_REGISTER_FUNCTION("willFinishLaunch", willFinishLaunchingFunc);
KLN_REGISTER_FUNCTION("didFinishLaunching", didFinishLaunchingFunc);
KLN_REGISTER_FUNCTION("firstVCDidShow", firstVCViewDidAppearFunc);
简单写的三个注册时机分别是 willFinishLaunch
,didFinishLaunching
,firstVCDidShow
,至此,注册流程已经全部完成。
触发流程
触发流程非常简单,在对应时间点调用 executeFunctionsForKey
来触发注册函数的执行
ini
@implementation AppDelegate
-(BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions{
// // 取出 key = "didFinish" 下所有函数
// NSArray *funcs = [[YZKylin sharedInstance] functionsForKey:@"willFinishLaunch"];
// NSLog(@"找到 %lu 个函数", (unsigned long)funcs.count);
// 直接执行 key = "willFinishLaunch" 下所有函数
[[YZKylin sharedInstance] executeFunctionsForKey:@"willFinishLaunch"];
return YES;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 直接执行 key = "didFinishLaunching" 下所有函数
[[YZKylin sharedInstance] executeFunctionsForKey:@"didFinishLaunching"];
return YES;
}
@implementation ViewController
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
// 直接执行 key = "firstVCDidShow" 下所有函数
[[YZKylin sharedInstance] executeFunctionsForKey:@"firstVCDidShow"];
}
实际控制台打印如下

一句话总结
用 attribute((section("__DATA,__kinit")))
+ Clang 特性,提前把函数注册到二进制文件的某个段里,
App 启动时按阶段(key)从段里读出对应函数,并调用,做到启动阶段灵活解耦。
完结撒花~🎉✨