热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

计算+load方法的耗时

在pre-main时期,objc会向dyld关于初始化流程的执行顺序,为了方便描述,这里我统称2、3步骤为initializers流程。可以看到,只要我们把监测代码塞进依赖动态库的initializers流程里(监测耗时库),就可以解决执行时间问题。考虑到工程内可能添加了其他动态库,我们还需要让监测耗时库的初始化函数早于这些库执行。解决了监测代码的执行问题,接下来就可以实现这些代码了,本文采用在

在 pre-main 时期,objc 会向 dyld 注册一个 init 回调 ,当 dyld 将要执行载入 image 的 initializers 流程时 (依赖的所有 image 已走完 initializers 流程时),init 回调被触发,在这个回调中,objc 会按照 父类-子类-分类 顺序调用 +load 方法。因为 +load 方法执行地足够早,并且只执行一次,所以我们通常会在这个方法中进行 method swizzling 或者自注册操作。也正是因为 +load 方法调用时间点的特殊性,导致此方法的耗时监测较为困难,而如何使监测代码先于 +load 方法执行成为解决此问题的关键点。

关于初始化流程的执行顺序, NSObject 文档 中有 以下说明 :

  1. All initializers in any framework you link to.
  2. All +load methods in your image.
  3. All C++ static initializers and C/C++ __attribute__(constructor) functions in your image.
  4. All initializers in frameworks that link to you.

为了方便描述,这里我统称 2、3 步骤为 initializers 流程。可以看到,只要我们把监测代码塞进依赖动态库的 initializers 流程里(监测耗时库),就可以解决执行时间问题 。考虑到工程内可能添加了其他动态库,我们还需要让监测耗时库的初始化函数早于这些库执行。解决了监测代码的执行问题,接下来就可以实现这些代码了,本文采用在 __attribute__(constructor) 初始化函数中 hook 所有 +load 方法来计算原 +load 执行的时间。

获取需要监测的 image

由于 dyld 加载的镜像中包含系统镜像,我们需要对这些镜像做次过滤,获取需要监测的镜像 ,也就是主 App 可执行文件和添加的自定义动态库对应的镜像

static bool isSelfDefinedImage(const char *imageName) {
    return !strstr(imageName, "/Xcode.app/") &&
    !strstr(imageName, "/Library/PrivateFrameworks/") &&
    !strstr(imageName, "/System/Library/") &&
    !strstr(imageName, "/usr/lib/");
}

static const struct mach_header **copyAllSelfDefinedImageHeader(unsigned int *outCount) {
    unsigned int imageCount = _dyld_image_count();
    unsigned int count = 0;
    const struct mach_header **mhdrList = NULL;
    
    if (imageCount > 0) {
        mhdrList = (const struct mach_header **)malloc(sizeof(struct mach_header *) * imageCount);
        for (unsigned int i = 0; i  
  

上面代码逻辑很简单,遍历 dyld 加载的镜像,过滤掉名称中包含 /Xcode.app//Library/PrivateFrameworks//System/Library//usr/lib/ 的常见系统库,剩下的就是我们添加的自定义镜像和主镜像了。

获取定义了 +load 方法的类和分类

目前我所知获取拥有 +load 类和分类的方法有两种,一种是通过 runtime api ,去读取对应镜像下所有类及其元类,并逐个遍历元类的实例方法,如果方法名称为 load ,则执行 hook 操作;一种是和 runtime 一样,直接通过 getsectiondata 函数,读取编译时期写入 mach-o 文件 DATA 段的 __objc_nlclslist__objc_nlcatlist 节 ,这两节分别用来保存 no lazy class 列表和 no lazy category 列表,所谓的 no lazy 结构,就是定义了 +load 方法的类或分类。

上文说过 objc 会向 dyld 注册一个 init 回调,其实这个注册函数还会接收一个 mapped 回调 _read_images ,dyld 会把当前已经载入或新添加的镜像信息通过回调函数传给 objc 设置程序,一般来说,除了手动 dlopen 的镜像外,在 objc 调用注册函数时,工程运行所需的镜像已经被 dyld 加载进内存了,所以 _read_images 回调会 立即被调用 , 并读取这些镜像 DATA 段中保存的类、分类、协议等信息。对于 no lazy 的类和分类, _read_images 函数会提前对关联的类做 realize 操作 ,这个操作包含了给类开辟可读写的信息存储空间、调整成员变量布局、插入分类方法属性等操作,简单来说就是让类可用 (realized)。值得注意的是,使用 objc_getClass 等查找接口,会触发对应类的 realize 操作,而正常情况下,只有我们使用某个类时,这个类才会执行上述操作,即类的懒加载。反观 +initialize ,只有首次向类发送消息时才会调用,不过两者目的不同,+initialize 更多的是提供一个入口,让开发者能在首次向类发送消息时,处理一些额外业务。

回到上面的两种方法,第一种方法需要借助 objc_copyClassNamesForImageobjc_getClass 函数,而后者会触发类的 realize 操作,也就说需要把读取镜像中访问的所有类都变成 realized 状态,当类较多时,这样做会比较明显地影响到 pre-main 的整体时间,所以本文采用了第二种方法:

static NSArray  *getNoLazyArray(const struct mach_header *mhdr) {
    NSMutableArray *noLazyArray = [NSMutableArray new];
    unsigned long bytes = 0;
    Class *clses = (Class *)getDataSection(mhdr, "__objc_nlclslist", &bytes);
    for (unsigned int i = 0; i  
  

hook 类和分类的 +load 方法

获得了拥有 +load 方法的类和分类,就可以 hook 对应的 +load 方法了。如上一节所说,no lazy 分类的方法在 _read_images 阶段就已经插入到对应类的方法列表中了,所以我们可以在元类的方法列表中拿到在类和分类中的定义的 +load 方法:

static void hookAllLoadMethods(LMLoadInfoWrapper *infoWrapper) {
    unsigned int count = 0;
    Class metaCls = object_getClass(infoWrapper.cls);
    Method *methodList = class_copyMethodList(metaCls, &count);
    for (unsigned int i = 0, j = 0; i  infoWrapper.infos.count - 1) {
                info = [[LMLoadInfo alloc] initWithClass:infoWrapper.cls];
                [infoWrapper insertLoadInfo:info];
                LMAllLoadNumber++;
            } else {
                info = infoWrapper.infos[j];
            }
            ++j;
            swizzleLoadMethod(infoWrapper.cls, method, info);
        }
    }
    free(methodList);
}

为了让 infos 列表能和类方法列表中的 +load 方法顺序一致,在构造 infoWrapper 时,我按照后编译分类-先编译分类-类次序,将类信息追加入 infos 列表中,然后在遍历元类的方法列表时,将对应的 LMLoadInfo 对象取出以设置 +load 方法执行耗时变量:

static void swizzleLoadMethod(Class cls, Method method, LMLoadInfo *info) {
retry:
    do {
        SEL hookSel = getRandomLoadSelector();
        Class metaCls = object_getClass(cls);
        IMP hookImp = imp_implementationWithBlock(^ {
            info->_start = CFAbsoluteTimeGetCurrent();
            ((void (*)(Class, SEL))objc_msgSend)(cls, hookSel);
            info->_end = CFAbsoluteTimeGetCurrent();
            if (!--LMAllLoadNumber) printLoadInfoWappers();
        });
        
        BOOL didAddMethod = class_addMethod(metaCls, hookSel, hookImp, method_getTypeEncoding(method));
        if (!didAddMethod) goto retry;
        
        info->_sel = hookSel;
        Method hookMethod = class_getInstanceMethod(metaCls, hookSel);
        method_exchangeImplementations(method, hookMethod);
    } while(0);
}

在所有的 +load 方法执行完毕后,输出工程的 +load 耗时信息。

打印所有 +load 耗时信息

基本上我们统计 +load 的耗时主要想看到两个信息:总耗时和最大耗时,所以这里我除了输出了总耗时,还按照 +load 执行时间降序打印出类和分类:

static void printLoadInfoWappers(void) {
    NSMutableArray *infos = [NSMutableArray array];
    for (LMLoadInfoWrapper *infoWrapper in LMLoadInfoWappers) {
        [infos addObjectsFromArray:infoWrapper.infos];
    }
    NSSortDescriptor *descriptor = [NSSortDescriptor sortDescriptorWithKey:@"duration" ascending:NO];
    [infos sortUsingDescriptors:@[descriptor]];
    
    CFAbsoluteTime totalDuration = 0;
    for (LMLoadInfo *info in infos) {
        totalDuration += info.duration;
    }
    printf("\n\t\t\t\t\t\t\tTotal load time: %f milliseconds", totalDuration * 1000);
    for (LMLoadInfo *info in infos) {
        NSString *clsname = [NSString stringWithFormat:@"%@", info.clsname];
        if (info.catname) clsname = [NSString stringWithFormat:@"%@(%@)", clsname, info.catname];
        printf("\n%40s load time: %f milliseconds", [clsname cStringUsingEncoding:NSUTF8StringEncoding], info.duration * 1000);
    }
    printf("\n");
}

例子 输出如下:

               Total load time: 2228.866100 milliseconds
               B(sleep_1_s) load time: 1001.139998 milliseconds
DynamicFramework(sleep_1_s) load time: 1001.088023 milliseconds
            A(sleep_100_ms) load time: 101.074934 milliseconds
         A(copy_class_list) load time: 68.153024 milliseconds
ViewController(sleep_50_ms) load time: 51.078916 milliseconds
           DynamicFramework load time: 4.286051 milliseconds
 ViewController(sleep_1_ms) load time: 1.210093 milliseconds
             ViewController load time: 0.580072 milliseconds
                          A load time: 0.254989 milliseconds

制作动态库集成至主工程

编写完监测代码,需要将其打包成动态库加入工程中,也就是 Embedded Binaries 和 Linked Frameworks And Libraries:

.app

所以如果是我们自己添加的库文件,需要将库文件添加进上面的两个列表中,否则要么 dyld 加载库镜像时出现 Library not loaded 错误,要么直接不链接这个库文件。而系统库则不需要设置 Embedded 栏 ,只需要设置 Linked 栏,因为实际设备中会预置这些库。

计算 +load 方法的耗时

以上图为例,Linked 栏中库的排列顺序,最终会体现在链接阶段命令的入参顺序上:

// Build Message

Ld ...
  .../clang ... -framework One -framework Two ... -o .../Demo.app/Demo

当参与链接的是动态库时,在生成主 App 可执行文件的 Load Commands 中,这些动态库对应的 LC_LOAD_DYLIB 排列顺序将和入参顺序一致。

计算 +load 方法的耗时

当这些动态库间不存在依赖关系时,其初始化函数的调用顺序将和 LC_LOAD_DYLIB 的排列顺序一致,否则会优先调用依赖库的初始化函数。

计算 +load 方法的耗时

因为监测耗时库不依赖其他自定义动态库,所以我们直接将监测耗时库拖入工程,并调整其至 Linked 栏首位即可。

制作 pod 集成至主工程

如果工程依赖由 CocoaPods 管理,我可能想要通过以下语句引入 +load 监测库:

pod 'A4LoadMeasure', configuration: ['Debug']

这样只有在 Debug 状态下才会引入监测库。需要注意的是 CocoaPods 引入的动态库是由 xcconfig 文件的 OTHER_LDFLAGS 设置的,我们无法通过调整其在 Linked 栏的顺序来决定链接顺序,不过 Other Linker Flags 中 -framework 指定的库优先级比 Linked 栏中的要高,所以我们只需要关心 CocoaPods 如何生成 xcconfig 的 OTHER_LDFLAGS 字段即可。

CocoaPods 在生成 Pods 工程时,会创建一个名称为 Pods-主target名 的 target (AggregateTarget),这个 target 的 xcconfig 汇集了所有 pods target 的 xcconfig ,我们看下 CocoaPods 是如何创建这个文件的:

# Pod::Generator::XCConfig::AggregateXCConfig
def generate
  ...
  @xccOnfig= Xcodeproj::Config.new(config)
  ...
  XCConfigHelper.generate_other_ld_flags(target, pod_targets, @xcconfig)
  ...
  @xcconfig
end
def save_as(path)
  generate.save_as(path)
end

# Xcodeproj::Config
def save_as(path)
  # 间接执行了 to_hash 并保存至 xcconfig 文件中
end
def to_hash(prefix = nil)
  ...
  [:libraries, :frameworks, :weak_frameworks, :force_load].each do |key|
    modifier = modifiers[key]
    sorted = other_linker_flags[key].to_a.sort
    if key == :force_load
      list += sorted.map { |l| %(#{modifier} #{l}) }
    else
      list += sorted.map { |l| %(#{modifier}"#{l}") }
    end
  end
  ...
end

可以看到,xcconfig 在保存时才对链接库进行排序,如 frameworks 会根据名称生序 排序 后再 map 成 -framework 库名 的形式保存在文件的 OTHER_LDFLAGS 字段中。所以我们只要保证监测库名比 Pods 工程引入的其他自定义动态库小就可以了,由于 0LoadMeasure、A+LoadMeasure 等非主流名称无法生成正确的 modulemap ,所以我采用 A4LoadMeasure 作为监测库名,A4 的值比 AA 等英文字母组成的名称小,针对这种情况已经基本够用了,毕竟很少会有用 A0 作为名称前缀的组件或动态库。

经过以上命名处理,开发者就可以直接通过 CocoaPods 引入监测库,而不需要进行额外的调整操作。

小结

本文讨论了使用 hook 监测 +load 执行时间方案,并结合 CocoaPods 实现了一行代码集成耗时监测的功能。详细代码可查看 A4LoadMeasure ,或者可以直接使用以下语句引入:

pod 'A4LoadMeasure', configuration: ['Debug']

以上所述就是小编给大家介绍的《计算 +load 方法的耗时》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 我们 的支持!


推荐阅读
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • PHP图片截取方法及应用实例
    本文介绍了使用PHP动态切割JPEG图片的方法,并提供了应用实例,包括截取视频图、提取文章内容中的图片地址、裁切图片等问题。详细介绍了相关的PHP函数和参数的使用,以及图片切割的具体步骤。同时,还提供了一些注意事项和优化建议。通过本文的学习,读者可以掌握PHP图片截取的技巧,实现自己的需求。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 本文介绍了PE文件结构中的导出表的解析方法,包括获取区段头表、遍历查找所在的区段等步骤。通过该方法可以准确地解析PE文件中的导出表信息。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 怎么在PHP项目中实现一个HTTP断点续传功能发布时间:2021-01-1916:26:06来源:亿速云阅读:96作者:Le ... [详细]
  • Day2列表、字典、集合操作详解
    本文详细介绍了列表、字典、集合的操作方法,包括定义列表、访问列表元素、字符串操作、字典操作、集合操作、文件操作、字符编码与转码等内容。内容详实,适合初学者参考。 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文介绍了PhysioNet网站提供的生理信号处理工具箱WFDB Toolbox for Matlab的安装和使用方法。通过下载并添加到Matlab路径中或直接在Matlab中输入相关内容,即可完成安装。该工具箱提供了一系列函数,可以方便地处理生理信号数据。详细的安装和使用方法可以参考本文内容。 ... [详细]
  • 本文介绍了一个程序,可以输出1000内能被3整除且个位数为6的所有整数。程序使用了循环和条件判断语句来筛选符合条件的整数,并将其输出。 ... [详细]
author-avatar
小佛瞌睡蓝
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有