iOS启动优化

62 阅读7分钟

简介

iOS的启动优化一般分为main函数之前和之后,mian函数之前(pre-main)主要分为4个阶段如下

  • 动态库加载
  • rebase/binding 
  • ObjC设置
  • initializer time(执行load、c++构造函数)

我们可以通过设置运行参数DYLD_PRINT_STATISTICS打印出pre-main阶段所消耗的时间。

运行结果如下:

dylib toading time: 25.65 miTLiseconds (5.9%)
rebase/binding time: 178.54 milliseconds (41.6%)
ObjC setup time: 194.22 milliseconds (45.2%)
initializer time: 30.73 milliseconds(7.1%)

针对pre-main的优化

1.dylib loding time

针对动态库加载优化,苹果建议自定义动态库少于6个,多于6个就做合并处理。配置好库的加载路径,减少动态库查找流程。

2.rebase/binding time

1.虚拟内存概念

虚拟内存是一种计算机系统中的技术,它通过将物理内存与硬盘空间相结合,为用户提供了一个比实际物理内存容量大得多的可寻址空间。‌ 虚拟内存的实现主要依赖于内存管理单元(MMU)和页表的使用,通过这些技术,CPU生成的虚拟地址被转换为物理地址,从而实现内存的扩展。

1.虚拟地址与物理地址的转换‌:
  • 在虚拟内存系统中,CPU生成的是虚拟地址,这些地址在访问物理内存之前需要被转换为物理地址。这种转换是通过内存管理单元(MMU)和页表来实现的。页表存储了虚拟页面到物理页面的映射信息。
  • 虚拟地址由两部分组成:虚页号和页内地址。虚页号用于在页表中查找对应的物理页帧,而页内地址则指定了该页面内的具体位置。
2.‌页表与内存管理
  • 每个进程都有其自己的页表,用于将虚拟地址映射到物理地址。页表由操作系统管理,并根据需要动态更新。
  • 当CPU需要访问某个虚拟地址时,MMU会查询页表以找到对应的物理地址,然后进行数据交换。如果所需页面不在物理内存中,则通过页面置换算法将其调入。
3.‌虚拟内存的管理方式‌:
  • 虚拟内存的管理方式主要有段式页式段页式三种。这些方式根据程序的逻辑结构或物理内存的划分来管理虚拟内存。
  • 段式管理按照程序的逻辑结构划分地址空间,便于实现存储保护和动态装配。页式管理则将虚拟空间和物理空间都分成固定或可变大小的页面,便于管理和提高内存使用效率。段页式管理结合了段和页的优点,但增加了硬件和软件的复杂性。
4.‌虚拟内存的优缺点‌:
  • 优点包括能够扩展可用内存容量、简化内存管理、保护每个进程的地址空间不被破坏等。
  • 缺点包括占用硬盘空间、可能增加对硬盘的读写操作、设置不当可能影响系统稳定性等。
2.rebase原理

App 启动 rebase(在 iOS 开发语境下通常指的是对应用内存地址的重新设定基地址的操作)主要是为了解决地址空间布局随机化(ASLR)带来的问题以及优化应用在运行时的内存布局。

当系统加载应用程序时,会为应用分配一块内存区域,但每次分配的起始地址是随机的(这是 ASLR 的安全机制)。App 启动 rebase 过程中,会根据当前实际分配到的内存起始地址,调整应用程序中各种代码段、数据段等的内存地址引用,使其指向正确的实际内存位置,确保应用能够正常运行并且正确地访问所需的资源和代码

流程步骤如下:

  1. 系统启动应用程序时,首先分配一块内存空间给应用。
    • 此时应用程序的原始内存布局是基于预期的一个基地址设计的,但实际分配的内存地址与预期不同。
  1. 应用程序开始启动 rebase 操作:
    • 读取应用程序的可执行文件以及相关的数据文件等,解析其中的各种内存地址引用。
  1. 逐个修正内存地址引用:
    • 根据实际分配的内存起始地址和原始的内存地址偏移量,计算出正确的新内存地址。
    • 将应用程序中所有涉及到的代码、数据等的内存地址引用都更新为新的正确地址。
  1. 完成 rebase:
    • 确保所有的内存地址引用都已正确调整后,应用程序就可以基于新的内存布局正常运行了。此时应用程序已经适应了系统分配的实际内存地址,能够正确地访问各种资源和执行代码

在启动的时候rebase过程中很容易产生内存pagefault,那么pagefault是什么呢。

内存 PageFault(页面错误)指的是当程序试图访问一个虚拟内存页面,而该页面在物理内存中不存在或者其访问权限不符合要求时,触发的一种异常情况。频繁的 PageFault 会严重影响程序的性能,因为每次处理 PageFault 都需要进行磁盘 I/O 操作,而磁盘访问速度远远慢于内存访问速度。所以我们在app启动的时候要减少pagefault。如何减少呢?即将启动时需要的符号表放在一起即可,xcode在编译的时候会根据.order文件里面符号表进行编译,所以我们需要将启动所需要的符号生成一个order文件就行。

3.clang插桩获取启动所需要的符号表,并且生成一个order文件。
1.获取oc方法、函数、block
  1. 在xcode clang编译配置Other C Flags:-fsanitize-coverage=func,trace-pc-guard
  2. 在Other C++ Flags:-fsanitize-coverage=func,trace-pc-guard
  3. Other Swift Flags:-sanitize-coverage=func -sanitize=undefined
  4. 编写一下代码
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
#import "TraceDemo-Swift.h"
interface ViewController ()
@property(nonatomic, assign) int age;

@end

@implementation ViewController
+(void)load
{
    [SwiftTest swiftTest];
    block();
}

void(^block)(void) = ^(void){
    
    NSLog(@"block函数执行!");
};
/*
+(void)load
{
  
    
}
+(void)initialize{
    
    
}
void test(){
    
    NSLog(@"test函数执行!");
    block();
}

-(void)testsleep{
    sleep(3);
}
*/

- (void)viewDidLoad {
    
    [super viewDidLoad];
    self.age = 10;
    // Do any additional setup after loading the view.
}

//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct {
    void * pc;
    void * next;
} SYNode;

//里面反应了项目中符号的个数!!
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;
  if (start == stop || *start) return;
//  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;
}


//HOOK一切的回调函数!!
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    void *PC = __builtin_return_address(0);
    //创建结构体
    SYNode * node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    //结构体入栈
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
    
}
//生成order文件!!
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //定义数组
    NSMutableArray<NSString *> * symbleNames = [NSMutableArray array];
    
    while (YES) {//循环体内!进行了拦截!!
        SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode,next));
        
        if (node == NULL) {
            break;
        }
        
        Dl_info info;
        dladdr(node->pc, &info);
        NSString * name = @(info.dli_sname);//转字符串
        //给函数名称添加 _
//        if ([name hasPrefix:@"+["] || [name hasPrefix:@"-["]) {//OC方法 直接存
//            [symbleNames addObject:name];
//            continue;
//        }
//        [symbleNames addObject:[@"_" stringByAppendingString:name]];
        
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        [symbleNames addObject:symbolName];
          
    }
    //反向遍历数组
//    symbleNames = (NSMutableArray<NSString *> *)[[symbleNames reverseObjectEnumerator] allObjects];
//    NSLog(@"%@",symbleNames);
    NSEnumerator * em = [symbleNames reverseObjectEnumerator];
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
    NSString * name;
    while (name = [em nextObject]) {
        if (![funcs containsObject:name]) {//数组没有name
            [funcs addObject:name];
        }
    }
    //去掉自己!
    [funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
    
    //写入文件
    //1.编程字符串
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];
    NSData * file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
    
    NSLog(@"%@",funcStr);
}
@end

5. 将生成的order文件拷贝到工程目录,并且在Build Settings->Linking->Order File 添加路径 6. 将Build Settings->Link->Write Link Map File改为YES

3.ObjC setup time

oc类注册。移除废弃类,减少类的注册

4.initializer time

load函数调用以及c++构造函数的调用,减少耗时操作,如果真有耗时操作,那就放入到子线程。