iOS 启动优化之自注册--attribute(section)

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:任意内存地址(通常是你程序中的某个全局变量/函数/对象地址)

  • 参数 infoDl_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);

简单写的三个注册时机分别是 willFinishLaunchdidFinishLaunchingfirstVCDidShow,至此,注册流程已经全部完成。

触发流程

触发流程非常简单,在对应时间点调用 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)从段里读出对应函数,并调用,做到启动阶段灵活解耦。

完结撒花~🎉✨

相关推荐
Lx3522 小时前
EXPLAIN工具:查询执行计划分析与索引诊断
sql·mysql·性能优化
桦说编程3 小时前
写时复制COW核心原理解读
java·性能优化·函数式编程
DemonAvenger4 小时前
Go缓存设计:权衡内存使用与性能
性能优化·架构·go
2501_915106324 小时前
数据差异的iOS性能调试:设备日志导出和iOS文件管理
websocket·http·macos·ios·https·udp·cocoa
cxks-从新开始4 小时前
在 Mac 上配置 Charles,抓取 iOS 手机端接口请求
macos·ios·智能手机
Mrdaliang12 小时前
Linux系统性能优化
linux·运维·性能优化
前端小菜嘤12 小时前
性能优化相关
性能优化
笨手笨脚の12 小时前
系统性能优化-2 CPU
redis·nginx·性能优化·numa·系统调优·cpu对性能的影响
W说编程18 小时前
算法导论第十四章 B树与B+树:海量数据的守护者
c语言·数据结构·b树·算法·性能优化
玺同学18 小时前
从卡顿到流畅:前端渲染性能深度解析与实战指南
前端·javascript·性能优化