美文网首页
启动优化

启动优化

作者: Shawn_Y | 来源:发表于2022-03-30 21:58 被阅读0次

启动优化

通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments)

  • DYLD_PRINT_STATISTICS设置为1
  • 如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1

目前DYLD_PRINT_STATISTICS在iOS15中失效了,可以通过Time Profiler诊断工具看

DYLD_FRAMEWORK_PATH
DYLD_FALLBACK_FRAMEWORK_PATH
DYLD_VERSIONED_FRAMEWORK_PATH
DYLD_LIBRARY_PATH
DYLD_FALLBACK_LIBRARY_PATH
DYLD_VERSIONED_LIBRARY_PATH
DYLD_IMAGE_SUFFIX
DYLD_INSERT_LIBRARIES
DYLD_PRINT_TO_FILE
DYLD_PRINT_LIBRARIES
DYLD_PRINT_LOADERS
DYLD_PRINT_SEARCHING
DYLD_PRINT_APIS
DYLD_PRINT_BINDINGS
DYLD_PRINT_INITIALIZERS
DYLD_PRINT_SEGMENTS
DYLD_PRINT_ENV
DYLD_SHARED_REGION
DYLD_SHARED_CACHE_DIR

冷启动premain分三个阶段

  1. dyld loading:apple的动态链接器,用来装载Mach-O文件(可执行文件、动态库等)
    • 尽量减少自定义动态库数量,官方建议最大不超过6个
  2. rebase/binding:偏移修正
    • rebase:每次启动使用ASLR(地址空间布局随机化)地址值插入到二进制文件的开头,目的是为了安全
    • binding:NSLog(@"123")在编译时生成Mach O文件中创建一个符号NSLog,随机指向一个地址,binding是将符号所在的地址指向真正的地址做关联。dyld_styub_binder技术找到NSLog指针地址进行调用
  3. objc setup
    • runtime在此处初始化,对class和category进⾏注册,selector唯⼀性判断
  4. load、constructor、initializer
    • 调⽤所有类的load的⽅法,初始化C&C++的静态化变量,然后调⽤ constructor函数

虚拟内存和物理内存

早期计算机是物理内存

CPU通过虚拟地址找到真实物理内存

操作系统为了解决安全问题和效率问题,抽象出了虚拟内存页(实际上是一个表)的概念。内存都是分页访问的。这里的page指的就是内存页。(就像磁盘存储的最小单位 磁盘簇,大小是4k一样)

MacOS 、linux (4K为一页)

iOS(16K为一页)

缺页中断

PageFault就是缺页中断:当app调用一个方法,发现该方法没有在内存中,此时操作系统就会立刻阻塞整个app进程,触发一个缺页中断。操作系统会从磁盘中读取这页数据到物理内存上 , 然后再将其映射到虚拟内存上 ( 如果当前内存已满 , 操作系统会通过置换页算法 找一页数据进行覆盖,这也是为什么开再多的应用也不会崩掉 , 但是之前开的应用再打开时 , 就重新启动了的根本原因 )。

假如,app启动时期需要调用 method1、method5和method6,这三个方法分布在page1、page2和page3上。每装载一个内存页page都会发生一次PageFault(缺页终端)。通常一个PageFault的处理时间是0.1ms~1ms,取0.5ms计算。这三次处理PageFault时间是 3 * 0.5ms = 1.5ms。

二进制重排

将所有启动时刻需要调用的方法排列在一起

怎么查看PageFault次数?

用Xcode的System Trace查看,搜索MainThread,Summary切换到Virtual Memory

image.png

Clang插桩

LLVM内置了代码覆盖仪表,它在函数级、基本块级和边缘级插入对用户定义函数的调用,并提供了这些回调的默认实现,在认为启动结束的位置添加代码,就能够拿到启动到指定位置调用到的所有函数符号。文档:https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs-with-guards

  1. 在目标工程 Target -> Build Settings -> Other C Flags 添加 -fsanitize-coverage=func,trace-pc-guard。(不写func会把所以跳转都hook,比如while循环)
  2. 如果有swfit代码,也要在 Other Swift Flags 添加 -sanitize-coverage=func 和 -sanitize=undefined
  3. 在代码文件中插入一下代码,要不然编译不通过
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>

//所以方法、函数、block、property的符号
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.
  if (start == stop || *start) return;  // Initialize only once.
  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}


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

//hook了方法,函数,block的调用,只要有跳转就会hook,比如while。 Other C Flags 添加 -fsanitize-coverage=func,trace-pc-guard可以解决这个问题
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { 
  //if (!*guard) return; // Duplicate the guard check. 
  //当前函数返回上一个调用的地址
  void *PC = __builtin_return_address(0); 
 
  SymbolNode * node = malloc(sizeof(SymbolNode)); 
  *node = (SymbolNode){PC,NULL}; 
 
  //入队 
  // offsetof 用在这里是为了入队添加下一个节点找到 前一个节点next指针的位置 
  OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next)); 
}
image.png
  • __builtin_return_address(0)返回上一个函数的调用地址
  1. 在启动结束的位置添加代码
//定义数组
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    
    while (YES) {//一次循环!也会被HOOK一次!!
        //offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
       SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
        
        if (node == NULL) {
            break;
        }
        Dl_info info = {0};
        dladdr(node->pc, &info);
        NSString * name = @(info.dli_sname);
        free(node);
        
        BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        //是否去重??
        [symbolNames addObject:symbolName];

    }
    //反向数组
    NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
    
    //创建一个新数组
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    NSString * name;
    //去重!
    while (name = [enumerator nextObject]) {
        if (![funcs containsObject:name]) {//数组中不包含name
            [funcs addObject:name];
        }
    }
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
    //数组转成字符串
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    //字符串写入文件
    //文件路径
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];
    //文件内容
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
  • Dl_info info = {0}; dladdr(node->pc, &info); NSString * name = @(info.dli_sname);

获取调用函数名字

  1. 在Xcode->Target->Build Settings->Linking->Order File中设置导出的文件order路径(可以将order文件复制到工程根目录)

如何验证?

导出的order文件格式如下

+[SDWebImageDownloader(CollectingMetrics) load]
+[SDWebImageDownloaderOperation(Metrics) load]
___39+[UIScrollView(MJExtension) initialize]_block_invoke
+[MMSDWebImageURLProtocol load]
+[FFFastImageViewManager load]
+[LXDBacktraceLogger load]
+[MMLikePopupManager load]
+[MMV5PopupMenuRNManager load]
……

Xcode Build产生的LinkMap文件格式如下

# Path: /Users/xxxx/Library/Developer/Xcode/DerivedData/xxxxx-hfblvrkmkyihscafanwsvjxsftmr/Build/Products/Debug-iphonesimulator/xxx.app/xxx
# Arch: x86_64
# Object files:
[  0] linker synthesized
[  1] dtrace
[  2] /Users/xxxx/Library/Developer/Xcode/DerivedData/xxxx-hfblvrkmkyihscafanwsvjxsftmr/Build/Intermediates.noindex/X x x x.build/Debug-iphonesimulator/xxx.build/Objects-normal/x86_64/main.o
[  3] /Users/xxxx/Library/Developer/Xcode/DerivedData/Xxxx-hfblvrkmkyihscafanwsvjxsftmr/Build/Intermediates.noindex/Xxxx.build/Debug-iphonesimulator/xxxx.build/Objects-normal/x86_64/NTAppDelegate.o
…………
# Sections:
# Address   Size        Segment Section
0x100002FD0 0x02022AA2  __TEXT  __text             //主程序
0x102025A72 0x00006D2C  __TEXT  __stubs        //用于 Stub 的占位代码,很多地方称之为桩代码。
0x10202C7A0 0x0000478C  __TEXT  __stub_helper    //当 Stub 无法找到真正的符号地址后的最终指向
0x102030F30 0x000BF5A8  __TEXT  __cstring         //C 语言字符串
0x1020F04D8 0x0014C990  __TEXT  __objc_methname  //Objective-C 方法名称
0x10223CE68 0x0006F010  __TEXT  __gcc_except_tab
…………
# Symbols:
# Address   Size        File  Name
0x100002FD0 0x00000120  [  2] _main
0x1000030F0 0x00000016  [  2] _sancov.module_ctor_trace_pc_guard
0x100003110 0x00000440  [  3] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100003550 0x00000120  [  3] -[AppDelegate wbSDKServiceDidFinishedNotification:]
0x100003670 0x00000190  [  3] -[AppDelegate dealUserUnloginOrUnUploaded]
0x100003800 0x00000070  [  3] -[AppDelegate configRootTabViewController:]
0x100003870 0x000009E0  [  3] +[AppDelegate setRootTabControllerBelow:]
…………

Sections:
Address Size Segment Section
0x100002FD0 0x02022AA2 __TEXT __text

这里就是主程序代码段的虚拟内存地址,0x100002FD0是地址开始位置,0x02022AA2是大小

  1. 计算order文件中Symbol所在的虚拟内存页数

    • 上面提到iOS16K为一页
    • 遍历order文件每行,在link map文件中找到对应的符号地址(SymbolLineAddr)和大小(SymbolLineSize)
    • SymbolLineAddr减去开始位置的地址,然后除以16K,得到当前行符号对应的页数,添加到数组
    • 获取数组count,这里就是所有符号所在的内存页数总和
  2. 计算Order文件中Symbol理论上所占用内存需要多少内存页数

    • 上一步骤获得了每行符号对应的内存大小,值相加
    • Sum除以16K,可以获得理论上所有符号占用的的内存页数
    • 和上一步获得的结果比较,看是否一致,如果一致说明不存在缺页中断

    打点

image.png
  1. 获取进程时间戳
#import <sys/sysctl.h> 
#import <mach/mach.h> 
+ (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc*)procInfo 
{ 
    int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid}; 
    size_t size = sizeof(*procInfo); 
    return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0; 
} 
 
+ (NSTimeInterval)processStartTime 
{ 
    struct kinfo_proc kProcInfo; 
    if ([self processInfoForPID:[[NSProcessInfo processInfo] processIdentifier] procInfo:&kProcInfo])  
    { 
        return kProcInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0; 
    }  
    else 
    { 
        return 0; 
    } 
}
  1. 获取最早的+load

通过 AAA 为前缀命名 Pod,让 +load 第一个被执行

  1. main函数
  2. finishLaunch begin
  3. finishLaunch end
  4. flow finish

相关文章

  • App启动优化(三)启动优化方案

    系列文章 App启动优化(一)冷启动和热启动 App启动优化(二)启动时间测量 App启动优化(三)启动优化方案 ...

  • android启动优化

    感谢原作者涉及:应用启动流程启动优化(闪屏页优化、MultiDex 优化、WebView启动优化、启动耗时分析方法)

  • App优化,包括启动优化、界面卡顿优化,及监控方案

    启动优化 启动优化包括冷启动优化和热启动优化。 main之前 1.减少不必要的framework,因为动态链接比较...

  • 性能优化 - 启动时间

    应用的启动分为:冷启动,暖启动和热启动。其中冷启动是我们优化的重点,优化冷启动可能会同时优化暖启动和热启动。 冷启...

  • App白屏和启动优化的一些思路

    App启动优化 App启动优化原理与技术方案 启动优化 黑白屏问题 启动页面主题设置为图片 启动页面,不要再onC...

  • 冷启动优化

    App启动分为冷启动和热启动,我们说的启动优化一般是指冷启动优化。若要想优化,首先我们必须明确启动过程。 启动过程...

  • 【高级iOS】启动时间优化

    【高级iOS】启动时间优化 【高级iOS】启动时间优化

  • 冷启动优化

    冷启动优化主要优化两个方面 Application 性能优化 App启动页性能优化业务优化不在本章优化范围内。本章...

  • APP性能优化

    一、APP启动性能优化。 APP启动主要分冷启动和热启动,主要优化冷启动。 1.尽量减少didFinishLauc...

  • 性能优化

    一、启动优化 Activity启动形式分为三种类型:热启动、冷启动、温启动。重点在冷启动。 假优化:欺骗用户,去掉...

网友评论

      本文标题:启动优化

      本文链接:https://www.haomeiwen.com/subject/ypnwjrtx.html